Exosphere is a set of small, modular, self-hostable community tools built on the AT Protocol.
app.exosphere.site
1import { globalStyle, style } from "@vanilla-extract/css";
2import { vars } from "./theme.css.ts";
3
4globalStyle("html, body", {
5 backgroundColor: vars.color.surface,
6 margin: 0,
7});
8
9// Mobile-first breakpoints (min-width)
10const bp = {
11 sm: "screen and (min-width: 480px)",
12 md: "screen and (min-width: 768px)",
13};
14
15// ---- Root ----
16
17export const themeRoot = style({
18 minHeight: "100vh",
19 display: "flex",
20 flexDirection: "column",
21 fontFamily: vars.font.body,
22 lineHeight: 1.6,
23 color: vars.color.text,
24 backgroundColor: vars.color.bg,
25 transition: "background-color 0.2s, color 0.2s",
26});
27
28// ---- Layout ----
29
30export const container = style({
31 maxWidth: "640px",
32 marginInline: "auto",
33 paddingInline: vars.space.md,
34 paddingBlockEnd: vars.space.xl,
35});
36
37export const stack = style({
38 display: "flex",
39 flexDirection: "column",
40 gap: vars.space.md,
41});
42
43export const stackSm = style({
44 display: "flex",
45 flexDirection: "column",
46 gap: vars.space.sm,
47});
48
49export const stackLg = style({
50 display: "flex",
51 flexDirection: "column",
52 gap: vars.space.lg,
53});
54
55export const cluster = style({
56 display: "flex",
57 flexWrap: "wrap",
58 gap: vars.space.sm,
59});
60
61export const row = style({
62 display: "flex",
63 alignItems: "center",
64 justifyContent: "space-between",
65});
66
67export const section = style({
68 marginBlockStart: vars.space.lg,
69 display: "flex",
70 flexDirection: "column",
71 gap: vars.space.md,
72});
73
74// ---- Header ----
75
76export const header = style({
77 borderBlockEnd: `1px solid ${vars.color.border}`,
78 backgroundColor: vars.color.surface,
79 paddingBlock: vars.space.sm,
80 marginBlockEnd: vars.space.lg,
81 boxShadow: `0 1px 3px ${vars.color.shadow}, 0 1px 2px ${vars.color.shadow}`,
82 transition: "background-color 0.2s, border-color 0.2s, box-shadow 0.2s",
83 "@media": {
84 [bp.sm]: {
85 paddingBlock: vars.space.md,
86 marginBlockEnd: vars.space.xl,
87 },
88 },
89});
90
91export const headerInner = style({
92 marginInline: "auto",
93 paddingInline: vars.space.lg,
94 display: "flex",
95 alignItems: "center",
96 justifyContent: "space-between",
97});
98
99export const headerTitle = style({
100 fontFamily: vars.font.heading,
101 fontWeight: 700,
102 fontSize: "1rem",
103 color: vars.color.text,
104 letterSpacing: "-0.01em",
105 ":hover": { textDecoration: "none" },
106 "@media": {
107 [bp.sm]: {
108 fontSize: "1.125rem",
109 },
110 },
111});
112
113export const headerNav = style({
114 display: "flex",
115 alignItems: "center",
116 gap: vars.space.sm,
117 "@media": {
118 [bp.sm]: {
119 gap: vars.space.md,
120 },
121 },
122});
123
124// ---- Cards ----
125
126export const card = style({
127 backgroundColor: vars.color.surface,
128 border: `1px solid ${vars.color.border}`,
129 borderRadius: vars.radius.lg,
130 paddingBlock: vars.space.md,
131 paddingInline: vars.space.lg,
132 boxShadow: `0 1px 3px ${vars.color.shadow}, 0 1px 2px ${vars.color.shadow}`,
133 transition: "background-color 0.2s, border-color 0.2s, box-shadow 0.2s",
134 ":hover": {
135 backgroundColor: vars.color.surfaceHover,
136 },
137 "@media": {
138 [bp.sm]: {
139 paddingBlock: vars.space.lg,
140 paddingInline: vars.space.xl,
141 },
142 },
143});
144
145export const cardLink = style({
146 display: "flex",
147 flexDirection: "column",
148 gap: vars.space.xs,
149 textDecoration: "none",
150 color: "inherit",
151 backgroundColor: vars.color.surface,
152 border: `1px solid ${vars.color.border}`,
153 borderRadius: vars.radius.lg,
154 paddingBlock: vars.space.md,
155 paddingInline: vars.space.lg,
156 boxShadow: `0 1px 3px ${vars.color.shadow}, 0 1px 2px ${vars.color.shadow}`,
157 transition: "border-color 0.15s, box-shadow 0.2s, background-color 0.2s, transform 0.15s",
158 ":hover": {
159 backgroundColor: vars.color.surfaceHover,
160 borderColor: vars.color.primary,
161 boxShadow: `0 4px 12px ${vars.color.shadowStrong}, 0 2px 4px ${vars.color.shadow}`,
162 transform: "translateY(-1px)",
163 textDecoration: "none",
164 },
165 "@media": {
166 [bp.sm]: {
167 paddingBlock: vars.space.lg,
168 paddingInline: vars.space.xl,
169 },
170 },
171});
172
173export const cardNarrow = style({
174 backgroundColor: vars.color.surface,
175 border: `1px solid ${vars.color.border}`,
176 borderRadius: vars.radius.lg,
177 paddingBlock: vars.space.lg,
178 paddingInline: vars.space.xl,
179 maxWidth: "400px",
180 marginInline: "auto",
181 boxShadow: `0 1px 3px ${vars.color.shadow}, 0 1px 2px ${vars.color.shadow}`,
182 transition: "background-color 0.2s, border-color 0.2s, box-shadow 0.2s",
183 "@media": {
184 [bp.sm]: {
185 paddingBlock: vars.space.xl,
186 paddingInline: vars.space.xxl,
187 },
188 },
189});
190
191export const cardFlat = style({
192 borderBlockStart: `1px solid ${vars.color.border}`,
193 paddingBlock: vars.space.sm,
194 "@media": {
195 [bp.sm]: {
196 paddingBlock: vars.space.md,
197 },
198 },
199});
200
201// ---- Buttons ----
202
203const btnBase = {
204 boxSizing: "border-box" as const,
205 display: "inline-flex" as const,
206 alignItems: "center" as const,
207 justifyContent: "center" as const,
208 borderRadius: vars.radius.sm,
209 fontWeight: 500,
210 lineHeight: 1.5,
211 cursor: "pointer",
212 fontFamily: vars.font.body,
213 minBlockSize: "44px",
214 whiteSpace: "nowrap" as const,
215 transition: "background-color 0.15s, opacity 0.15s, box-shadow 0.15s, transform 0.1s",
216};
217
218export const button = style({
219 ...btnBase,
220 paddingBlock: vars.space.sm,
221 paddingInline: vars.space.lg,
222 border: "none",
223 fontSize: "0.875rem",
224 backgroundColor: vars.color.primary,
225 color: "#fff",
226 boxShadow: `0 1px 2px ${vars.color.shadow}`,
227 ":hover": {
228 backgroundColor: vars.color.primaryHover,
229 textDecoration: "none",
230 boxShadow: `0 2px 6px ${vars.color.shadowStrong}`,
231 },
232 ":disabled": { opacity: 0.5, cursor: "not-allowed" },
233 ":active": { transform: "scale(0.98)" },
234});
235
236export const buttonSecondary = style({
237 ...btnBase,
238 paddingBlock: vars.space.sm,
239 paddingInline: vars.space.lg,
240 border: `1px solid ${vars.color.border}`,
241 fontSize: "0.875rem",
242 backgroundColor: vars.color.surface,
243 color: vars.color.text,
244 ":hover": {
245 backgroundColor: vars.color.bg,
246 textDecoration: "none",
247 borderColor: vars.color.primary,
248 },
249 ":active": { transform: "scale(0.98)" },
250});
251
252export const buttonDanger = style({
253 ...btnBase,
254 paddingBlock: "6px",
255 paddingInline: vars.space.md,
256 border: "none",
257 fontSize: "0.75rem",
258 minBlockSize: "36px",
259 backgroundColor: vars.color.danger,
260 color: "#fff",
261 ":hover": { opacity: 0.9 },
262 ":active": { transform: "scale(0.98)" },
263});
264
265// ---- Forms ----
266
267export const formStack = style({
268 display: "flex",
269 flexDirection: "column",
270 gap: vars.space.lg,
271});
272
273export const label = style({
274 display: "block",
275 fontSize: "0.875rem",
276 fontWeight: 500,
277 marginBlockEnd: vars.space.xs,
278});
279
280export const labelHint = style({
281 fontWeight: 400,
282 color: vars.color.textMuted,
283});
284
285const inputBase = {
286 boxSizing: "border-box" as const,
287 width: "100%" as const,
288 paddingBlock: "10px",
289 paddingInline: vars.space.md,
290 borderRadius: vars.radius.sm,
291 border: `1px solid ${vars.color.border}`,
292 fontSize: "1rem",
293 lineHeight: 1.5,
294 color: vars.color.text,
295 backgroundColor: vars.color.surface,
296 outline: "none" as const,
297 fontFamily: vars.font.body,
298 transition: "border-color 0.15s, box-shadow 0.15s",
299};
300
301const inputFocus = {
302 borderColor: vars.color.primary,
303 boxShadow: `0 0 0 3px ${vars.color.focusRing}`,
304};
305
306export const input = style({
307 ...inputBase,
308 ":focus": inputFocus,
309});
310
311export const select = style({
312 ...inputBase,
313 ":focus": inputFocus,
314});
315
316export const textarea = style({
317 ...inputBase,
318 resize: "vertical" as const,
319 minHeight: "80px",
320 fontFamily: vars.font.body,
321 ":focus": inputFocus,
322});
323
324// ---- Typography ----
325
326export const pageTitle = style({
327 fontFamily: vars.font.heading,
328 fontSize: "1.25rem",
329 fontWeight: 700,
330 letterSpacing: "-0.02em",
331 "@media": {
332 [bp.sm]: {
333 fontSize: "1.5rem",
334 },
335 },
336});
337
338export const sectionTitle = style({
339 fontFamily: vars.font.heading,
340 fontSize: "1rem",
341 fontWeight: 600,
342 letterSpacing: "-0.01em",
343 "@media": {
344 [bp.sm]: {
345 fontSize: "1.125rem",
346 },
347 },
348});
349
350export const cardTitle = style({
351 fontFamily: vars.font.heading,
352 fontSize: "1rem",
353 fontWeight: 600,
354 letterSpacing: "-0.01em",
355 "@media": {
356 [bp.sm]: {
357 fontSize: "1.0625rem",
358 },
359 },
360});
361
362export const cardHeading = style({
363 fontFamily: vars.font.heading,
364 fontSize: "1.125rem",
365 fontWeight: 600,
366 marginBlockEnd: vars.space.sm,
367 letterSpacing: "-0.01em",
368 "@media": {
369 [bp.sm]: {
370 fontSize: "1.25rem",
371 },
372 },
373});
374
375export const muted = style({
376 color: vars.color.textMuted,
377 fontSize: "0.8125rem",
378});
379
380export const description = style({
381 color: vars.color.textMuted,
382 fontSize: "0.875rem",
383 lineHeight: 1.5,
384 display: "-webkit-box",
385 WebkitLineClamp: 2,
386 WebkitBoxOrient: "vertical",
387 overflow: "hidden",
388});
389
390export const subsectionTitle = style({
391 fontSize: "0.875rem",
392 fontWeight: 500,
393 color: vars.color.textMuted,
394});
395
396export const collapsibleHeading = style({
397 display: "flex",
398 alignItems: "center",
399 gap: vars.space.sm,
400 cursor: "pointer",
401 userSelect: "none",
402 background: "none",
403 border: "none",
404 padding: 0,
405 fontFamily: vars.font.heading,
406 fontSize: "1rem",
407 fontWeight: 600,
408 color: vars.color.text,
409 ":hover": {
410 color: vars.color.primary,
411 },
412});
413
414export const chevron = style({
415 display: "inline-block",
416 fontSize: "0.75rem",
417 transition: "transform 0.15s",
418});
419
420export const chevronExpanded = style({
421 transform: "rotate(180deg)",
422});
423
424export const introText = style({
425 color: vars.color.textMuted,
426 fontSize: "0.875rem",
427 lineHeight: 1.6,
428 marginBlockEnd: vars.space.lg,
429});
430
431export const badge = style({
432 display: "inline-block",
433 paddingBlock: "2px",
434 paddingInline: vars.space.sm,
435 borderRadius: vars.radius.sm,
436 fontSize: "0.75rem",
437 fontWeight: 500,
438 backgroundColor: vars.color.primaryLight,
439 color: vars.color.primary,
440});
441
442globalStyle(`${badge}[data-status="not-planned"]`, {
443 backgroundColor: vars.color.dangerLight,
444 color: vars.color.danger,
445});
446
447globalStyle(`${badge}[data-status="approved"]`, {
448 backgroundColor: vars.color.primaryLight,
449 color: vars.color.primary,
450});
451
452globalStyle(`${badge}[data-status="in-progress"]`, {
453 backgroundColor: vars.color.warningLight,
454 color: vars.color.warning,
455});
456
457globalStyle(`${badge}[data-status="done"]`, {
458 backgroundColor: vars.color.successLight,
459 color: vars.color.success,
460});
461
462export const errorText = style({
463 color: vars.color.danger,
464 fontSize: "0.875rem",
465});
466
467// ---- Misc ----
468
469export const metaRow = style({
470 display: "flex",
471 flexWrap: "wrap",
472 alignItems: "center",
473 gap: vars.space.md,
474 fontSize: "0.875rem",
475});
476
477export const buttonInline = style({
478 background: "none",
479 border: "none",
480 color: vars.color.primary,
481 fontSize: "0.875rem",
482 fontWeight: 500,
483 fontFamily: vars.font.body,
484 cursor: "pointer",
485 paddingBlock: 0,
486 paddingInline: vars.space.sm,
487 ":hover": { textDecoration: "underline" },
488});
489
490export const buttonDangerInline = style({
491 background: "none",
492 border: "none",
493 color: vars.color.danger,
494 fontSize: "0.875rem",
495 fontWeight: 500,
496 fontFamily: vars.font.body,
497 cursor: "pointer",
498 paddingBlock: 0,
499 paddingInline: vars.space.sm,
500 ":hover": { textDecoration: "underline" },
501});
502
503export const inlineTag = style({
504 marginInlineStart: vars.space.sm,
505});
506
507// ---- Responsive utilities ----
508
509export const hiddenMobile = style({
510 display: "none",
511 "@media": {
512 [bp.md]: {
513 display: "initial",
514 },
515 },
516});
517
518// ---- Theme toggle ----
519
520// ---- Tab navigation ----
521
522export const tabNav = style({
523 display: "flex",
524 gap: vars.space.xs,
525 borderBlockEnd: `1px solid ${vars.color.border}`,
526 paddingBlockEnd: vars.space.xs,
527});
528
529export const tabNavLink = style({
530 paddingBlock: vars.space.xs,
531 paddingInline: vars.space.md,
532 borderRadius: vars.radius.sm,
533 fontSize: "0.875rem",
534 fontWeight: 500,
535 color: vars.color.textMuted,
536 textDecoration: "none",
537 transition: "color 0.15s, background-color 0.15s",
538 ":hover": {
539 color: vars.color.text,
540 backgroundColor: vars.color.surfaceHover,
541 textDecoration: "none",
542 },
543});
544
545export const tabNavActive = style({
546 paddingBlock: vars.space.xs,
547 paddingInline: vars.space.md,
548 borderRadius: vars.radius.sm,
549 fontSize: "0.875rem",
550 fontWeight: 500,
551 color: vars.color.primary,
552 backgroundColor: vars.color.primaryLight,
553});
554
555// ---- Theme toggle ----
556
557export const themeToggle = style({
558 display: "inline-flex",
559 alignItems: "center",
560 border: `1px solid ${vars.color.border}`,
561 borderRadius: vars.radius.sm,
562 overflow: "hidden",
563 padding: "2px",
564 gap: "2px",
565 backgroundColor: vars.color.bg,
566 transition: "background-color 0.2s, border-color 0.2s",
567});
568
569export const themeToggleBtn = style({
570 display: "inline-flex",
571 alignItems: "center",
572 justifyContent: "center",
573 inlineSize: "28px",
574 blockSize: "28px",
575 border: "none",
576 borderRadius: "6px",
577 cursor: "pointer",
578 backgroundColor: "transparent",
579 color: vars.color.textMuted,
580 padding: 0,
581 transition: "background-color 0.15s, color 0.15s",
582 ":hover": { color: vars.color.text },
583});
584
585export const themeToggleBtnActive = style({
586 backgroundColor: vars.color.surface,
587 color: vars.color.text,
588 boxShadow: `0 1px 2px ${vars.color.shadow}`,
589});
590
591// ---- Footer ----
592
593export const mainContent = style({
594 flex: 1,
595});
596
597export const footer = style({
598 borderBlockStart: `1px solid ${vars.color.border}`,
599 paddingBlock: vars.space.lg,
600 marginBlockStart: vars.space.xxl,
601 transition: "background-color 0.2s, border-color 0.2s",
602});
603
604export const footerInner = style({
605 maxWidth: "640px",
606 marginInline: "auto",
607 paddingInline: vars.space.md,
608 display: "flex",
609 flexWrap: "wrap",
610 alignItems: "center",
611 justifyContent: "center",
612 gap: vars.space.sm,
613 fontSize: "0.8125rem",
614 color: vars.color.textMuted,
615});
616
617export const footerSep = style({
618 color: vars.color.border,
619});
620
621export const footerLink = style({
622 color: vars.color.textMuted,
623 textDecoration: "none",
624 transition: "color 0.15s",
625 ":hover": {
626 color: vars.color.primary,
627 },
628});