Skip to content
1,166 linesCodeBlameRaw
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 /*
15 * Every colour is a pair: light-dark() picks the light palette or the
16 * dark one by the element's color-scheme, which follows the Appearance
17 * setting (lib/theme.ts, set on <html> below). Something drawn dark on
18 * purpose in either theme, such as a product screenshot, sets
19 * `scheme-dark` and its tokens stay dark.
20 */
21 --color-bg: light-dark(var(--g1t-light-bg), var(--g1t-bg));
22 /*
23 * The app's frame (components/shell.tsx, components/ui/sidebar.tsx):
24 * the page sits flat against the sidebar, a hairline between; the sidebar
25 * is the shell's surface, and the rail, the narrow column of icons at
26 * the window's edge, is a step apart: white in the light theme, near
27 * black in the dark one. The current row or icon is a muted fill.
28 */
29 --color-shell: light-dark(#efefed, #0b0b0d);
30 --color-sidebar: var(--color-shell);
31 --color-sidebar-accent: light-dark(#e1e1de, #1c1c20);
32 --color-sidebar-border: light-dark(#e0e0dd, #1d1d22);
33 --color-rail: light-dark(#ffffff, #050507);
34 --color-surface: light-dark(var(--g1t-light-surface), var(--g1t-surface));
35 --color-raised: light-dark(var(--g1t-light-raised), var(--g1t-raised));
36 --color-line: light-dark(var(--g1t-light-line), var(--g1t-line));
37 --color-line-strong: light-dark(var(--g1t-light-line-strong), var(--g1t-line-strong));
38
39 --color-fg: light-dark(var(--g1t-light-fg), var(--g1t-fg));
40 --color-fg-soft: light-dark(var(--g1t-light-fg-soft), var(--g1t-fg-soft));
41 /* A primary button's hover: brighter than fg on dark, softer on light. */
42 --color-fg-hover: light-dark(#3a3a41, #ffffff);
43 --color-muted: light-dark(var(--g1t-light-muted), var(--g1t-muted));
44 --color-faint: light-dark(var(--g1t-light-faint), var(--g1t-faint));
45
46 --color-accent: light-dark(var(--g1t-light-accent), var(--g1t-accent));
47 --color-accent-hover: light-dark(var(--g1t-light-accent-hover), var(--g1t-accent-hover));
48 --color-accent-dim: light-dark(var(--g1t-light-accent-dim), var(--g1t-accent-dim));
49 --color-accent-low: light-dark(var(--g1t-light-accent-low), var(--g1t-accent-low));
50 /* The accent at its palest on dark, a middle lavender on light: the headline's gradient. */
51 --color-accent-high: light-dark(#8b76e0, #d9d1ff);
52
53 --color-success: light-dark(var(--g1t-light-success), var(--g1t-success));
54 --color-success-dim: light-dark(var(--g1t-light-success-dim), var(--g1t-success-dim));
55 --color-success-low: light-dark(var(--g1t-light-success-low), var(--g1t-success-low));
56
57 --color-info: light-dark(var(--g1t-light-info), var(--g1t-info));
58 --color-merged: light-dark(var(--g1t-light-merged), var(--g1t-merged));
59 --color-warn: light-dark(var(--g1t-light-warn), var(--g1t-warn));
60 --color-danger: light-dark(var(--g1t-light-danger), var(--g1t-danger));
61
62 --animate-fade-up: fade-up 0.6s ease-out both;
63 --animate-dash: dash 1.2s linear infinite;
64 /* A template's creature, on hover: a small, quick wiggle (components/agent-avatar.tsx). */
65 --animate-wiggle: wiggle 0.45s ease-in-out;
66
67 /* Menus, popovers and dialogs (components/ui): in quickly, out quicker. */
68 --animate-pop-in: pop-in 0.16s cubic-bezier(0.16, 1, 0.3, 1);
69 --animate-pop-out: pop-out 0.1s ease-in forwards;
70 --animate-fade-in: fade-in 0.16s ease-out;
71 --animate-fade-out: fade-out 0.12s ease-in forwards;
72 --animate-dialog-in: dialog-in 0.2s cubic-bezier(0.16, 1, 0.3, 1);
73 --animate-dialog-out: dialog-out 0.12s ease-in forwards;
74 /* Sheets (components/ui/sheet.tsx) slide in from their edge. */
75 --animate-sheet-in-right: sheet-in-right 0.24s cubic-bezier(0.16, 1, 0.3, 1);
76 --animate-sheet-out-right: sheet-out-right 0.16s ease-in forwards;
77 --animate-sheet-in-left: sheet-in-left 0.24s cubic-bezier(0.16, 1, 0.3, 1);
78 --animate-sheet-out-left: sheet-out-left 0.16s ease-in forwards;
79}
80
81@keyframes sheet-in-bottom {
82 from {
83 transform: translateY(100%);
84 }
85}
86
87@keyframes sheet-in-right {
88 from {
89 transform: translateX(100%);
90 }
91}
92
93@keyframes sheet-out-right {
94 to {
95 transform: translateX(100%);
96 }
97}
98
99@keyframes sheet-in-left {
100 from {
101 transform: translateX(-100%);
102 }
103}
104
105@keyframes sheet-out-left {
106 to {
107 transform: translateX(-100%);
108 }
109}
110
111@keyframes pop-in {
112 from {
113 opacity: 0;
114 transform: scale(0.96) translateY(-2px);
115 }
116}
117
118@keyframes pop-out {
119 to {
120 opacity: 0;
121 transform: scale(0.96);
122 }
123}
124
125@keyframes fade-in {
126 from {
127 opacity: 0;
128 }
129}
130
131@keyframes fade-out {
132 to {
133 opacity: 0;
134 }
135}
136
137@keyframes dialog-in {
138 from {
139 opacity: 0;
140 transform: translateY(6px) scale(0.97);
141 }
142}
143
144@keyframes dialog-out {
145 to {
146 opacity: 0;
147 transform: translateY(6px) scale(0.97);
148 }
149}
150
151/* Never from nothing: the first paint already shows the words. */
152@keyframes fade-up {
153 from {
154 opacity: 0.4;
155 transform: translateY(8px);
156 }
157}
158
159@keyframes wiggle {
160 25% {
161 transform: rotate(-8deg);
162 }
163 60% {
164 transform: rotate(6deg);
165 }
166}
167
168@keyframes dash {
169 to {
170 stroke-dashoffset: -16;
171 }
172}
173
174/*
175 * An agent's face (components/agent-face.tsx). Its colours are the face's
176 * own body colour (set on the element, a light-dark() pair) and these: the
177 * ink of its eyes and mouth, a second ink for what sits outside the head,
178 * the rim around its head, a highlight, the glow of a visor or a working
179 * face, and two accents for a bow and a spark. It moves with transforms and opacity only: a blink and a breath
180 * on its own beat (the delays are set per face from its seed), a pulse
181 * while it works, a bounce when it is happy, a z when it sleeps. The
182 * reduced-motion rule below stops every one of them, and the state's
183 * still pose (narrowed or closed eyes) is a plain transform, so it holds.
184 */
185.agent-face {
186 --face-ink: #1b1b22;
187 /* What sits outside the head (antenna, hat, headphones, a z) reads on the page, not the body. */
188 --face-ink-out: light-dark(#1b1b22, #e9e9ee);
189 --face-rim: color-mix(in oklab, var(--face-body), #000 24%);
190 --face-light: #ffffff;
191 --face-glow: light-dark(#0d7a52, #86efc4);
192 --face-ribbon: light-dark(#6b52c8, #b6a8ff);
193 --face-spark: light-dark(#d4a90f, #f2dc72);
194 display: inline-block;
195 vertical-align: middle;
196}
197.agent-face .face-bob,
198.agent-face .face-eyes,
199.agent-face .face-pupils,
200.agent-face .face-glow,
201.agent-face .face-bulb,
202.agent-face .face-spark,
203.agent-face .face-z {
204 transform-box: fill-box;
205 transform-origin: center;
206}
207.agent-face .face-bob {
208 animation: face-bob 4.2s ease-in-out infinite;
209 animation-delay: var(--face-bob-delay, 0s);
210}
211.agent-face .face-eyes {
212 animation: face-blink var(--face-blink-period, 5s) ease-in-out infinite;
213 animation-delay: var(--face-blink-delay, 0s);
214}
215.agent-face .face-pupils {
216 transition: transform 0.25s ease-out;
217}
218/* A glance toward the pointer, on hover only. */
219.agent-face:hover .face-pupils,
220a:hover > .agent-face .face-pupils,
221button:hover > .agent-face .face-pupils {
222 transform: translateX(2.5px);
223}
224.agent-face .face-glow {
225 opacity: 0;
226}
227.agent-face .face-spark {
228 animation: face-twinkle 2.8s ease-in-out infinite;
229 animation-delay: var(--face-bob-delay, 0s);
230}
231/* Working: eyes narrowed, no blink, a soft pulse around the head and in the bulb. */
232.agent-face[data-state="working"] .face-eyes {
233 animation: none;
234 transform: scaleY(0.55);
235}
236.agent-face[data-state="working"] .face-glow {
237 animation: face-pulse 1.6s ease-in-out infinite;
238}
239.agent-face[data-state="working"] .face-bulb {
240 animation: face-bulb 1.6s ease-in-out infinite;
241}
242/* Asleep: eyes shut, a slower breath, a z drifting up. */
243.agent-face[data-state="asleep"] .face-eyes {
244 animation: none;
245 transform: scaleY(0.1);
246}
247.agent-face[data-state="asleep"] .face-bob {
248 animation-duration: 6s;
249}
250.agent-face[data-state="asleep"] .face-z {
251 animation: face-zz 2.6s ease-in-out infinite;
252}
253.agent-face[data-state="asleep"] .face-z-2 {
254 animation-delay: 1.3s;
255}
256/* Happy: a bounce, twice, then still. */
257.agent-face[data-state="happy"] .face-bob {
258 animation: face-happy 0.7s cubic-bezier(0.34, 1.56, 0.64, 1) 2;
259}
260@keyframes face-blink {
261 0%,
262 91%,
263 100% {
264 transform: scaleY(1);
265 }
266 94%,
267 96% {
268 transform: scaleY(0.08);
269 }
270}
271@keyframes face-bob {
272 0%,
273 100% {
274 transform: translateY(0);
275 }
276 50% {
277 transform: translateY(-1.6px);
278 }
279}
280@keyframes face-pulse {
281 0%,
282 100% {
283 opacity: 0.15;
284 }
285 50% {
286 opacity: 0.55;
287 }
288}
289@keyframes face-bulb {
290 0%,
291 100% {
292 opacity: 0.6;
293 transform: scale(1);
294 }
295 50% {
296 opacity: 1;
297 transform: scale(1.15);
298 }
299}
300@keyframes face-twinkle {
301 0%,
302 100% {
303 opacity: 0.75;
304 transform: scale(0.9) rotate(0deg);
305 }
306 50% {
307 opacity: 1;
308 transform: scale(1.1) rotate(20deg);
309 }
310}
311@keyframes face-zz {
312 0% {
313 opacity: 0;
314 transform: translate(0, 4px);
315 }
316 30% {
317 opacity: 1;
318 }
319 100% {
320 opacity: 0;
321 transform: translate(5px, -9px);
322 }
323}
324@keyframes face-happy {
325 0%,
326 100% {
327 transform: translateY(0) scale(1);
328 }
329 40% {
330 transform: translateY(-6px) scale(1.06);
331 }
332}
333
334/*
335 * Appearance (lib/theme.ts): with no choice, or Auto, the page follows
336 * the system; a choice of Light or Dark is data-theme on <html>.
337 */
338html {
339 color-scheme: light dark;
340 scroll-behavior: smooth;
341 /* A tap shows the control's own active state, not the browser's grey flash. */
342 -webkit-tap-highlight-color: transparent;
343 /* Phones: no text inflation on rotation. */
344 -webkit-text-size-adjust: 100%;
345 text-size-adjust: 100%;
346}
347html[data-theme="light"] {
348 color-scheme: light;
349}
350html[data-theme="dark"] {
351 color-scheme: dark;
352}
353
354/*
355 * The app's frame (components/shell.tsx), set on <body data-frame="app">:
356 * the page fills the window beside the rail and the sidebar, and on a
357 * phone it fills the window above the bottom bar, flat, as on a computer.
358 * What sticks to the top or the bottom of a page reads these:
359 * --frame-top what is above the page: a phone's status bar, where it overlaps
360 * --topbar-h that gap plus the top bar: where sticky things sit
361 * --tabbar-h what the bottom takes: on a phone, the bar and the home indicator under it
362 * --page-h the height left for a page that fills the window (Chat)
363 * The public frame keeps a plain header 3.5rem tall and nothing at the bottom.
364 */
365:root {
366 --frame-top: 0px;
367 --tabbar-h: 0px;
368 --topbar-h: 3.5rem;
369 --page-h: calc(100dvh - var(--topbar-h) - var(--tabbar-h));
370}
371body[data-frame="app"] {
372 --topbar-h: calc(var(--frame-top) + 3.5rem);
373 --page-h: calc(100dvh - var(--topbar-h) - var(--tabbar-h));
374}
375/*
376 * The desktop app's own title bar (apps/desktop, src/frame.rs), drawn over
377 * the top of the window on <html data-titlebar>: what is above the page is
378 * the bar, whatever the frame. --titlebar-h is for frames that do not
379 * read --frame-top (the public header, the standalone frame). The bar
380 * pushes any page down by itself; g1t's pages make room their own way.
381 */
382:root {
383 --titlebar-h: 0px;
384}
385html[data-titlebar] {
386 --titlebar-h: 36px;
387 padding-top: 0;
388}
389html[data-titlebar] body {
390 --frame-top: var(--titlebar-h);
391}
392@media (max-width: 767px) {
393 body[data-frame="app"] {
394 --frame-top: env(safe-area-inset-top);
395 /* The bottom bar: 56px tall along the bottom edge, and the home indicator's room under it. */
396 --tabbar-h: calc(3.5rem + env(safe-area-inset-bottom));
397 }
398}
399
400/*
401 * Phones: a field under 16px makes iOS zoom the page when it is focused, so
402 * every field is at least that; and nothing scrolls the page sideways.
403 */
404@media (max-width: 767px) {
405 input:not([type="checkbox"]):not([type="radio"]):not([type="range"]),
406 textarea,
407 select {
408 font-size: max(16px, 1em) !important;
409 }
410 body {
411 overflow-x: clip;
412 }
413}
414
415body {
416 @apply bg-bg font-sans text-fg antialiased;
417}
418body[data-frame="app"] {
419 @apply bg-shell;
420}
421
422::selection {
423 background: color-mix(in srgb, var(--color-accent) 35%, transparent);
424}
425
426/* In the base layer, so a component can opt out with a utility. */
427@layer base {
428 :focus-visible {
429 outline: 2px solid var(--color-accent);
430 outline-offset: 2px;
431 }
432
433 /*
434 * A grid's items may shrink below their content. Without this, one long
435 * line that does not wrap (a command, a commit hash, an address) widens
436 * the whole column, and on a phone the page scrolls sideways.
437 */
438 .grid > * {
439 min-width: 0;
440 }
441
442 /*
443 * Headlines are set in Bricolage Grotesque, its optical size following
444 * the font size; small section labels that happen to be headings stay in
445 * the text face. A utility on the element still wins.
446 */
447 :where(h1, h2),
448 :where(h3):is(.text-xl, .text-2xl, .text-3xl, .text-4xl) {
449 font-family: var(--font-display);
450 font-optical-sizing: auto;
451 }
452 :where(h2):is(.text-xs, .text-sm, .text-base) {
453 font-family: var(--font-sans);
454 }
455
456 /* Checkboxes, radios and ranges in g1t's own colour, not the browser's. */
457 :root {
458 accent-color: var(--color-accent);
459 }
460
461 /*
462 * Number fields have no spin buttons: the arrow keys still step them, and
463 * their digits line up in columns.
464 */
465 input[type="number"] {
466 -moz-appearance: textfield;
467 appearance: textfield;
468 font-variant-numeric: tabular-nums;
469 }
470 input[type="number"]::-webkit-inner-spin-button,
471 input[type="number"]::-webkit-outer-spin-button {
472 -webkit-appearance: none;
473 appearance: none;
474 margin: 0;
475 }
476
477 /*
478 * Date and time fields: the picker's icon is drawn by the browser in the
479 * page's color-scheme (set on <html> above), so it is right in both
480 * themes; it sits quiet until hovered. The part being edited is marked
481 * in the accent, not the system's blue.
482 */
483 input:is([type="date"], [type="time"], [type="datetime-local"], [type="month"], [type="week"]) {
484 font-variant-numeric: tabular-nums;
485 }
486 ::-webkit-calendar-picker-indicator {
487 cursor: pointer;
488 border-radius: 0.25rem;
489 opacity: 0.55;
490 transition: opacity 150ms;
491 }
492 ::-webkit-calendar-picker-indicator:hover,
493 ::-webkit-calendar-picker-indicator:focus-visible {
494 opacity: 1;
495 }
496 ::-webkit-datetime-edit-fields-wrapper {
497 padding: 0;
498 }
499 ::-webkit-datetime-edit-text {
500 color: var(--color-faint);
501 }
502 ::-webkit-datetime-edit-year-field:focus,
503 ::-webkit-datetime-edit-month-field:focus,
504 ::-webkit-datetime-edit-day-field:focus,
505 ::-webkit-datetime-edit-hour-field:focus,
506 ::-webkit-datetime-edit-minute-field:focus,
507 ::-webkit-datetime-edit-second-field:focus,
508 ::-webkit-datetime-edit-millisecond-field:focus,
509 ::-webkit-datetime-edit-meridiem-field:focus {
510 border-radius: 0.1875rem;
511 background-color: color-mix(in srgb, var(--color-accent) 25%, transparent);
512 color: var(--color-fg);
513 }
514
515 /* A colour field is a rounded swatch with a hairline, not a grey button. */
516 input[type="color"] {
517 -webkit-appearance: none;
518 appearance: none;
519 cursor: pointer;
520 padding: 0.1875rem;
521 border: 1px solid var(--color-line);
522 border-radius: 0.375rem;
523 background-color: var(--color-bg);
524 }
525 input[type="color"]:hover {
526 border-color: var(--color-line-strong);
527 }
528 input[type="color"]::-webkit-color-swatch-wrapper {
529 padding: 0;
530 }
531 input[type="color"]::-webkit-color-swatch {
532 border: none;
533 border-radius: 0.25rem;
534 }
535 input[type="color"]::-moz-color-swatch {
536 border: none;
537 border-radius: 0.25rem;
538 }
539
540 /* A search field's clear button is the browser's own: g1t's fields don't show it. */
541 input[type="search"]::-webkit-search-cancel-button,
542 input[type="search"]::-webkit-search-decoration {
543 -webkit-appearance: none;
544 appearance: none;
545 }
546
547 /*
548 * A <summary> that keeps the browser's disclosure triangle has it quiet,
549 * in the faint text colour; one that draws its own hides it with list-none.
550 */
551 summary {
552 cursor: pointer;
553 }
554 summary::marker {
555 color: var(--color-faint);
556 }
557 summary::-webkit-details-marker {
558 color: var(--color-faint);
559 }
560}
561
562@media (prefers-reduced-motion: reduce) {
563 *,
564 *::before,
565 *::after {
566 animation: none !important;
567 scroll-behavior: auto !important;
568 }
569}
570
571/* Rendered markdown: READMEs, briefs and docs. */
572.prose > :first-child {
573 margin-top: 0;
574 padding-top: 0;
575}
576.prose {
577 @apply leading-7 text-fg;
578}
579.prose > * + * {
580 @apply mt-4;
581}
582.prose h1 {
583 @apply mt-0 text-3xl font-semibold tracking-tight;
584}
585.prose h2 {
586 @apply mt-10 border-b border-line pb-2 text-xl font-semibold tracking-tight;
587}
588.prose h3 {
589 @apply mt-8 text-base font-semibold;
590}
591.prose p,
592.prose li {
593 @apply text-[0.9375rem] text-fg/90;
594}
595.prose a {
596 @apply text-accent underline decoration-accent/40 underline-offset-4 hover:decoration-accent;
597}
598.prose ul {
599 @apply list-disc space-y-1.5 pl-6;
600}
601.prose ol {
602 @apply list-decimal space-y-1.5 pl-6;
603}
604.prose code {
605 @apply rounded bg-raised px-1.5 py-0.5 font-mono text-[0.85em];
606}
607.prose pre {
608 @apply overflow-x-auto rounded-lg border border-line bg-surface p-4 font-mono text-sm leading-6;
609}
610.prose pre code {
611 @apply bg-transparent p-0;
612}
613.prose blockquote {
614 @apply border-l-2 border-line-strong pl-4 text-muted;
615}
616.prose table {
617 @apply w-full border-collapse text-sm;
618}
619.prose th {
620 @apply border-b border-line-strong px-3 py-2 text-left font-medium text-muted;
621}
622.prose td {
623 @apply border-b border-line px-3 py-2 align-top;
624}
625.prose hr {
626 @apply border-line;
627}
628.prose h4 {
629 @apply mt-6 text-sm font-semibold;
630}
631.prose li.task-list-item {
632 @apply list-none;
633}
634.prose ul.contains-task-list {
635 @apply pl-1;
636}
637.prose kbd {
638 @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)];
639}
640.prose del {
641 @apply text-muted;
642}
643.prose details {
644 @apply rounded-lg border border-line px-4 py-2;
645}
646.prose summary {
647 @apply cursor-pointer font-medium;
648}
649.prose img {
650 @apply inline max-w-full;
651}
652.prose section[data-footnotes] {
653 @apply mt-10 border-t border-line pt-4 text-sm text-muted;
654}
655.prose a[data-footnote-ref] {
656 @apply no-underline;
657}
658
659/* Syntax-highlighted code from Shiki. */
660.shiki {
661 @apply bg-transparent! font-mono text-sm leading-6;
662}
663.shiki code {
664 counter-reset: line;
665 display: block;
666}
667.shiki .line::before {
668 counter-increment: line;
669 content: counter(line);
670 @apply mr-5 inline-block w-10 text-right text-faint select-none;
671}
672
673/*
674 * Artwork (components/art.tsx). Every piece loops on its drawing's
675 * `--dur` and starts at its own `--delay`, so the parts of one drawing
676 * stay in step. Only transforms, opacity and stroke offsets move. The
677 * resting style of each class is the finished picture: that is what
678 * prefers-reduced-motion shows, since the rule above stops every
679 * animation.
680 */
681.art-flow {
682 animation: dash 1.6s linear infinite;
683}
684
685/* Something travelling along a track (pathLength 100). Unseen at rest. */
686.art-packet {
687 opacity: 0;
688 stroke-dasharray: 4 200;
689 stroke-dashoffset: 4;
690 animation: art-travel var(--dur, 3s) cubic-bezier(0.45, 0, 0.35, 1) infinite;
691 animation-delay: var(--delay, 0s);
692}
693@keyframes art-travel {
694 0% {
695 stroke-dashoffset: 4;
696 opacity: 0;
697 }
698 3% {
699 opacity: 1;
700 }
701 32% {
702 opacity: 1;
703 }
704 36%,
705 100% {
706 stroke-dashoffset: -100;
707 opacity: 0;
708 }
709}
710
711/* A face that lights up for most of the loop. Lit at rest. */
712.art-lit {
713 opacity: 0.85;
714 animation: art-lit var(--dur, 8s) ease-in-out infinite both;
715 animation-delay: var(--delay, 0s);
716}
717@keyframes art-lit {
718 0%,
719 4% {
720 opacity: 0;
721 }
722 12%,
723 70% {
724 opacity: 0.9;
725 }
726 88%,
727 100% {
728 opacity: 0;
729 }
730}
731
732/* A short flash, as a check runs over a layer. Unseen at rest. */
733.art-scan {
734 opacity: 0;
735 animation: art-scan var(--dur, 8s) ease-out infinite both;
736 animation-delay: var(--delay, 0s);
737}
738@keyframes art-scan {
739 0% {
740 opacity: 0;
741 }
742 4% {
743 opacity: 1;
744 }
745 16%,
746 100% {
747 opacity: 0;
748 }
749}
750
751/* A line that draws itself, then fades. Drawn at rest. */
752.art-draw {
753 stroke-dasharray: 100;
754 stroke-dashoffset: 0;
755 animation: art-draw var(--dur, 8s) ease-in-out infinite both;
756 animation-delay: var(--delay, 0s);
757}
758@keyframes art-draw {
759 0% {
760 stroke-dashoffset: 100;
761 opacity: 1;
762 }
763 14%,
764 70% {
765 stroke-dashoffset: 0;
766 opacity: 1;
767 }
768 88%,
769 100% {
770 stroke-dashoffset: 0;
771 opacity: 0;
772 }
773}
774
775/* Something that appears, stays, and goes. Shown at rest. */
776.art-pop {
777 animation: art-pop var(--dur, 8s) ease-out infinite both;
778 animation-delay: var(--delay, 0s);
779}
780@keyframes art-pop {
781 0% {
782 opacity: 0;
783 transform: translateY(4px);
784 }
785 6%,
786 72% {
787 opacity: 1;
788 transform: none;
789 }
790 86%,
791 100% {
792 opacity: 0;
793 transform: none;
794 }
795}
796
797/* A layer dropping onto a stack. In place at rest. */
798.art-drop {
799 animation: art-drop var(--dur, 7s) cubic-bezier(0.2, 0.9, 0.3, 1.2) infinite both;
800 animation-delay: var(--delay, 0s);
801}
802@keyframes art-drop {
803 0% {
804 opacity: 0;
805 transform: translateY(-26px);
806 }
807 8%,
808 74% {
809 opacity: 1;
810 transform: none;
811 }
812 88%,
813 100% {
814 opacity: 0;
815 transform: none;
816 }
817}
818
819/* The layer that fails: drops, shakes, and is sent back. */
820.art-eject {
821 animation: art-eject var(--dur, 7s) ease-in-out infinite both;
822 animation-delay: var(--delay, 0s);
823}
824@keyframes art-eject {
825 0% {
826 opacity: 0;
827 transform: translateY(-26px);
828 }
829 8%,
830 30% {
831 opacity: 1;
832 transform: none;
833 }
834 33% {
835 transform: translateX(-3px);
836 }
837 36% {
838 transform: translateX(3px);
839 }
840 39% {
841 transform: translateX(-2px);
842 }
843 42% {
844 opacity: 1;
845 transform: none;
846 }
847 56%,
848 100% {
849 opacity: 0;
850 transform: translate(-40px, -22px);
851 }
852}
853
854/* A layer rising out of the one below it. In place at rest. */
855.art-rise {
856 animation: art-rise var(--dur, 7s) cubic-bezier(0.2, 0.8, 0.3, 1) infinite both;
857 animation-delay: var(--delay, 0s);
858}
859@keyframes art-rise {
860 0% {
861 opacity: 0;
862 transform: translateY(14px);
863 }
864 10%,
865 70% {
866 opacity: 1;
867 transform: none;
868 }
869 86%,
870 100% {
871 opacity: 0;
872 transform: none;
873 }
874}
875
876/* A soft glow breathing around a point. */
877.art-pulse {
878 transform-box: fill-box;
879 transform-origin: center;
880 animation: art-pulse var(--dur, 4s) ease-in-out infinite;
881 animation-delay: var(--delay, 0s);
882}
883@keyframes art-pulse {
884 0%,
885 100% {
886 transform: scale(0.8);
887 opacity: 1;
888 }
889 50% {
890 transform: scale(1.5);
891 opacity: 0.35;
892 }
893}
894
895/* Drawings off-screen hold still (components/art.tsx, Live). */
896[data-paused] * {
897 animation-play-state: paused !important;
898}
899/*
900 * Each drawing on its own layer, so a frame of its motion repaints only the
901 * drawing, not the page around it or the blurred header passing over it.
902 */
903[data-live] {
904 will-change: transform;
905}
906
907/* The landing page's steps, lit in turn along their line. */
908/* The ring is drawn once and only fades and grows, so the compositor runs it. */
909.flow-step::after {
910 content: "";
911 position: absolute;
912 inset: 0;
913 border-radius: inherit;
914 box-shadow: 0 0 0 6px color-mix(in srgb, var(--color-accent) 18%, transparent);
915 opacity: 0;
916 pointer-events: none;
917 animation: flow-step 10s ease-in-out infinite both;
918 animation-delay: var(--delay, 0s);
919}
920@keyframes flow-step {
921 0%,
922 100% {
923 opacity: 0;
924 transform: scale(0.82);
925 }
926 6% {
927 opacity: 1;
928 transform: none;
929 }
930 16% {
931 opacity: 0;
932 transform: none;
933 }
934}
935.flow-runner {
936 animation: flow-runner 10s cubic-bezier(0.45, 0, 0.35, 1) infinite both;
937}
938@keyframes flow-runner {
939 0% {
940 transform: translateX(0);
941 opacity: 0;
942 }
943 4%,
944 76% {
945 opacity: 1;
946 }
947 80%,
948 100% {
949 transform: translateX(100%);
950 opacity: 0;
951 }
952}
953@media (prefers-reduced-motion: reduce) {
954 .flow-runner {
955 display: none;
956 }
957}
958
959/* The sidebar trading one far-side menu for another, in place. */
960@keyframes g1t-swap {
961 from {
962 opacity: 0;
963 transform: translateX(10px);
964 }
965 to {
966 opacity: 1;
967 transform: none;
968 }
969}
970
971/*
972 * Behind the landing page's opening: the same dark gray, with a faint
973 * lavender and peach glow low on the horizon and a few stars.
974 */
975.dusk {
976 background:
977 radial-gradient(1px 1px at 12% 18%, rgb(255 255 255 / 0.35), transparent),
978 radial-gradient(1px 1px at 27% 8%, rgb(255 255 255 / 0.25), transparent),
979 radial-gradient(1px 1px at 63% 12%, rgb(255 255 255 / 0.3), transparent),
980 radial-gradient(1.5px 1.5px at 90% 9%, rgb(255 255 255 / 0.35), transparent),
981 radial-gradient(60% 35% at 50% 100%, rgb(255 189 140 / 0.08), transparent 70%),
982 radial-gradient(80% 45% at 50% 100%, rgb(182 168 255 / 0.1), transparent 75%),
983 var(--color-bg);
984}
985/* A planet rising behind the horizon, its lit edge catching the glow. */
986.dusk-planet {
987 background: radial-gradient(circle at 70% 30%, rgb(182 168 255 / 0.12), rgb(255 189 140 / 0.04) 45%, transparent 70%);
988 box-shadow: inset 14px -8px 50px rgb(255 214 186 / 0.07);
989}
990
991/*
992 * On a phone, every field's text is at least 16px: iOS zooms the page into
993 * a field set smaller as soon as it is focused. Unlayered, so it wins over
994 * a field's text-sm utility.
995 */
996@media (max-width: 639px) {
997 input:not([type="checkbox"], [type="radio"], [type="range"], [type="color"], [type="file"]),
998 textarea,
999 select {
1000 font-size: max(16px, 1em);
1001 }
1002}
1003
1004/* A card's Markdown preview in chat (components/chat/card.tsx): the same Markdown, smaller and tighter. */
1005.card-md .prose {
1006 @apply text-[0.8125rem] leading-relaxed;
1007}
1008.card-md .prose > * + * {
1009 @apply mt-2;
1010}
1011.card-md .prose p,
1012.card-md .prose li {
1013 @apply text-[0.8125rem] text-fg/85;
1014}
1015.card-md .prose h1,
1016.card-md .prose h2,
1017.card-md .prose h3,
1018.card-md .prose h4 {
1019 @apply mt-3 border-0 pb-0 text-sm font-semibold;
1020}
1021.card-md .prose ul,
1022.card-md .prose ol {
1023 @apply space-y-0.5 pl-5;
1024}
1025.card-md .prose pre {
1026 @apply p-2.5 text-xs leading-5;
1027}
1028
1029/*
1030 * The Docs page editor (components/docs/editor.tsx) is BlockNote with its
1031 * shadcn components. Their classes are generated from the package, and
1032 * shadcn's token names map onto g1t's. `accent` and `muted` mean
1033 * something else in shadcn (a hover fill, a quiet fill), so inside the
1034 * editor they are redefined; g1t's own blocks there use the g1t tokens
1035 * directly.
1036 */
1037@source "../../../node_modules/@blocknote/shadcn/dist";
1038
1039@theme {
1040 --color-background: var(--color-bg);
1041 --color-foreground: var(--color-fg);
1042 --color-popover: var(--color-surface);
1043 --color-popover-foreground: var(--color-fg);
1044 --color-card: var(--color-surface);
1045 --color-card-foreground: var(--color-fg);
1046 --color-primary: var(--color-accent);
1047 --color-primary-foreground: var(--color-bg);
1048 --color-secondary: var(--color-raised);
1049 --color-secondary-foreground: var(--color-fg);
1050 --color-muted-foreground: var(--color-muted);
1051 --color-accent-foreground: var(--color-fg);
1052 --color-destructive: var(--color-danger);
1053 --color-destructive-foreground: var(--color-bg);
1054 --color-border: var(--color-line);
1055 --color-input: var(--color-line);
1056 --color-ring: var(--color-accent);
1057}
1058
1059.bn-shadcn {
1060 --color-accent: var(--color-raised);
1061 --color-muted: var(--color-raised);
1062 --bn-colors-editor-background: transparent;
1063 --bn-colors-editor-text: var(--color-fg);
1064 --bn-colors-menu-background: var(--color-surface);
1065 --bn-colors-menu-text: var(--color-fg);
1066 --bn-colors-hovered-background: var(--color-raised);
1067 --bn-colors-side-menu: var(--color-faint);
1068 --bn-colors-border: var(--color-line);
1069 --bn-border-radius: 8px;
1070}
1071
1072.g1t-editor .bn-editor {
1073 font-family: var(--g1t-font-sans);
1074 font-size: 1rem;
1075 line-height: 1.65;
1076 background: transparent;
1077 color: var(--color-fg);
1078}
1079
1080/* The drag handle and + sit in the margin, so the text lines up with the title. */
1081@media (min-width: 768px) {
1082 .g1t-editor .bn-container {
1083 margin-inline: -54px;
1084 }
1085}
1086
1087.g1t-editor .bn-block-content[data-content-type="heading"] h1,
1088.g1t-editor .bn-block-content[data-content-type="heading"] h2,
1089.g1t-editor .bn-block-content[data-content-type="heading"] h3 {
1090 font-family: var(--g1t-font-display);
1091 letter-spacing: -0.012em;
1092}
1093
1094.g1t-editor .bn-inline-content code {
1095 font-family: var(--g1t-font-mono);
1096 font-size: 0.875em;
1097 padding: 0.1em 0.35em;
1098 border-radius: 4px;
1099 background: var(--color-raised);
1100}
1101
1102.g1t-editor .bn-block-content[data-content-type="codeBlock"] {
1103 border-radius: 10px;
1104 background: var(--color-surface);
1105 border: 1px solid var(--color-line);
1106}
1107
1108.g1t-editor .bn-thread-mark {
1109 background: color-mix(in srgb, var(--color-warn) 22%, transparent);
1110 border-bottom: 2px solid color-mix(in srgb, var(--color-warn) 60%, transparent);
1111}
1112
1113.g1t-editor a {
1114 color: var(--color-accent);
1115}
1116
1117/* A page read before the editor loads: the same measure as the editor. */
1118.docs-read .prose {
1119 font-size: 1rem;
1120 line-height: 1.65;
1121}
1122
1123/* The chat composer (components/chat/editor.tsx): formatting as messages show it. */
1124.chat-editor {
1125 white-space: pre-wrap;
1126 word-break: break-word;
1127 min-height: 1.5rem;
1128}
1129.chat-editor p.is-editor-empty:first-child::before {
1130 content: attr(data-placeholder);
1131 float: left;
1132 height: 0;
1133 pointer-events: none;
1134 color: var(--color-faint);
1135}
1136.chat-editor strong {
1137 @apply font-semibold text-fg;
1138}
1139.chat-editor s {
1140 @apply text-muted;
1141}
1142.chat-editor a {
1143 @apply text-accent underline underline-offset-2;
1144}
1145.chat-editor code {
1146 @apply rounded-[4px] border border-line bg-raised px-1 py-px font-mono text-[0.84em] text-fg-soft;
1147}
1148.chat-editor pre {
1149 @apply my-1 rounded-md border border-line bg-bg px-3 py-2 font-mono text-[0.8125rem] leading-relaxed text-fg-soft;
1150}
1151.chat-editor pre code {
1152 @apply border-0 bg-transparent p-0 text-[1em];
1153}
1154.chat-editor ul {
1155 @apply list-disc pl-5 marker:text-faint;
1156}
1157.chat-editor ol {
1158 @apply list-decimal pl-5 marker:text-faint;
1159}
1160.chat-editor li > p {
1161 @apply pl-0.5;
1162}
1163.chat-editor blockquote {
1164 @apply my-0.5 border-l-2 border-line-strong pl-3 text-muted;
1165}
1166