| 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, as in apps/web. */ |
| 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 | /* |
| 12 | * Every colour is a pair: light-dark() picks the light palette or the |
| 13 | * dark one by the page's color-scheme, which follows the Appearance |
| 14 | * switch (lib/theme.ts), as on g1t.sh. |
| 15 | */ |
| 16 | --color-bg: light-dark(var(--g1t-light-bg), var(--g1t-bg)); |
| 17 | --color-surface: light-dark(var(--g1t-light-surface), var(--g1t-surface)); |
| 18 | --color-raised: light-dark(var(--g1t-light-raised), var(--g1t-raised)); |
| 19 | --color-line: light-dark(var(--g1t-light-line), var(--g1t-line)); |
| 20 | --color-line-strong: light-dark(var(--g1t-light-line-strong), var(--g1t-line-strong)); |
| 21 | |
| 22 | --color-fg: light-dark(var(--g1t-light-fg), var(--g1t-fg)); |
| 23 | --color-fg-soft: light-dark(var(--g1t-light-fg-soft), var(--g1t-fg-soft)); |
| 24 | /* A primary button's hover: brighter than fg on dark, softer on light. */ |
| 25 | --color-fg-hover: light-dark(#3a3a41, #ffffff); |
| 26 | --color-muted: light-dark(var(--g1t-light-muted), var(--g1t-muted)); |
| 27 | --color-faint: light-dark(var(--g1t-light-faint), var(--g1t-faint)); |
| 28 | |
| 29 | --color-accent: light-dark(var(--g1t-light-accent), var(--g1t-accent)); |
| 30 | --color-accent-hover: light-dark(var(--g1t-light-accent-hover), var(--g1t-accent-hover)); |
| 31 | --color-accent-dim: light-dark(var(--g1t-light-accent-dim), var(--g1t-accent-dim)); |
| 32 | |
| 33 | --color-success: light-dark(var(--g1t-light-success), var(--g1t-success)); |
| 34 | --color-success-dim: light-dark(var(--g1t-light-success-dim), var(--g1t-success-dim)); |
| 35 | |
| 36 | --color-info: light-dark(var(--g1t-light-info), var(--g1t-info)); |
| 37 | --color-merged: light-dark(var(--g1t-light-merged), var(--g1t-merged)); |
| 38 | --color-warn: light-dark(var(--g1t-light-warn), var(--g1t-warn)); |
| 39 | --color-danger: light-dark(var(--g1t-light-danger), var(--g1t-danger)); |
| 40 | } |
| 41 | |
| 42 | /* |
| 43 | * Appearance (lib/theme.ts): with no choice, or Auto, the page follows |
| 44 | * the system; a choice of Light or Dark is data-theme on <html>, drawn by |
| 45 | * the server, so a page never shows the other theme first. |
| 46 | */ |
| 47 | html { |
| 48 | color-scheme: light dark; |
| 49 | } |
| 50 | html[data-theme="light"] { |
| 51 | color-scheme: light; |
| 52 | } |
| 53 | html[data-theme="dark"] { |
| 54 | color-scheme: dark; |
| 55 | } |
| 56 | |
| 57 | body { |
| 58 | @apply bg-bg font-sans text-fg antialiased; |
| 59 | } |
| 60 | |
| 61 | ::selection { |
| 62 | background: color-mix(in srgb, var(--color-accent) 35%, transparent); |
| 63 | } |
| 64 | |
| 65 | @layer base { |
| 66 | :focus-visible { |
| 67 | outline: 2px solid var(--color-accent); |
| 68 | outline-offset: 2px; |
| 69 | } |
| 70 | |
| 71 | .grid > * { |
| 72 | min-width: 0; |
| 73 | } |
| 74 | |
| 75 | /* Page titles in the headline face, as on g1t.sh. */ |
| 76 | :where(h1) { |
| 77 | font-family: var(--font-display); |
| 78 | font-optical-sizing: auto; |
| 79 | } |
| 80 | |
| 81 | :root { |
| 82 | accent-color: var(--color-accent); |
| 83 | } |
| 84 | |
| 85 | /* Figures line up in columns of money. */ |
| 86 | .tabular { |
| 87 | font-variant-numeric: tabular-nums; |
| 88 | } |
| 89 | } |
| 90 | |
| 91 | /* |
| 92 | * A long-running button says it is running with CSS alone, before any |
| 93 | * script has loaded: a clicked button keeps focus while the old page waits |
| 94 | * for the response (components/progress.tsx lets go of it once a <Form>'s |
| 95 | * answer is in). `:focus-visible` is left out, so tabbing to it does not |
| 96 | * read as running. |
| 97 | */ |
| 98 | .run-button .run-busy, |
| 99 | .run-button + .run-note { |
| 100 | display: none; |
| 101 | } |
| 102 | .run-button:focus:not(:focus-visible) .run-idle { |
| 103 | display: none; |
| 104 | } |
| 105 | .run-button:focus:not(:focus-visible) .run-busy { |
| 106 | display: inline-flex; |
| 107 | align-items: center; |
| 108 | gap: 0.5rem; |
| 109 | } |
| 110 | .run-button:focus:not(:focus-visible) + .run-note { |
| 111 | display: inline; |
| 112 | } |
| 113 | .run-spinner { |
| 114 | width: 0.8rem; |
| 115 | height: 0.8rem; |
| 116 | border-radius: 9999px; |
| 117 | border: 2px solid currentColor; |
| 118 | border-right-color: transparent; |
| 119 | animation: run-spin 0.8s linear infinite; |
| 120 | } |
| 121 | @keyframes run-spin { |
| 122 | to { |
| 123 | transform: rotate(360deg); |
| 124 | } |
| 125 | } |
| 126 | @media (prefers-reduced-motion: reduce) { |
| 127 | .run-spinner { |
| 128 | animation: none; |
| 129 | } |
| 130 | } |
| 131 | |
| 132 | /* |
| 133 | * Every other submit button says it is working the same way: a clicked |
| 134 | * button keeps focus while the page waits for the answer, so it dims |
| 135 | * and shows a spinner. The page that comes back is |
| 136 | * fresh (every action redirects), and says what happened. Nothing may |
| 137 | * stop pointer events here: focus comes on mouse-down, before the click. |
| 138 | */ |
| 139 | form button[type="submit"]:not(.run-button):focus:not(:focus-visible) { |
| 140 | opacity: 0.75; |
| 141 | } |
| 142 | form button[type="submit"]:not(.run-button):focus:not(:focus-visible)::before { |
| 143 | content: ""; |
| 144 | flex: none; |
| 145 | width: 0.8rem; |
| 146 | height: 0.8rem; |
| 147 | border-radius: 9999px; |
| 148 | border: 2px solid currentColor; |
| 149 | border-right-color: transparent; |
| 150 | animation: run-spin 0.8s linear infinite; |
| 151 | } |
| 152 | form button[type="submit"]:not(.run-button):focus:not(:focus-visible) > svg { |
| 153 | display: none; |
| 154 | } |
| 155 | @media (prefers-reduced-motion: reduce) { |
| 156 | form button[type="submit"]:focus:not(:focus-visible)::before { |
| 157 | animation: none; |
| 158 | } |
| 159 | } |
| 160 | |
| 161 | /* |
| 162 | * The credit and terms forms (components/billing.tsx) show a field only for the |
| 163 | * choice it belongs to, with CSS alone: the custom amount and the slug for |
| 164 | * Custom, a refund's details for Refund, the expiry for anything else, and |
| 165 | * the date for "On a date". Where `:has()` is not supported (or without |
| 166 | * CSS) every field shows, and the server ignores the ones that do not apply. |
| 167 | */ |
| 168 | @supports selector(:has(*)) { |
| 169 | .credit-form .when-custom, |
| 170 | .credit-form .when-refund, |
| 171 | .credit-form .when-date, |
| 172 | .terms-form .when-custom { |
| 173 | display: none; |
| 174 | } |
| 175 | .credit-form:has(input[name="preset"][value="custom"]:checked) .when-custom, |
| 176 | .terms-form:has(input[name="preset"][value="custom"]:checked) .when-custom, |
| 177 | .credit-form:has(input[name="kind"][value="refund"]:checked) .when-refund, |
| 178 | .credit-form:has(input[name="expires"][value="date"]:checked) .when-date { |
| 179 | display: block; |
| 180 | } |
| 181 | .credit-form:has(input[name="kind"][value="refund"]:checked) .when-not-refund { |
| 182 | display: none; |
| 183 | } |
| 184 | } |