globals.css 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903
  1. @import "tailwindcss";
  2. @import "tw-animate-css";
  3. @custom-variant dark (&:is(.dark *));
  4. @theme inline {
  5. --color-background: var(--background);
  6. --color-foreground: var(--foreground);
  7. --color-card: var(--card);
  8. --color-card-foreground: var(--card-foreground);
  9. --color-popover: var(--popover);
  10. --color-popover-foreground: var(--popover-foreground);
  11. --color-primary: var(--primary);
  12. --color-primary-foreground: var(--primary-foreground);
  13. --color-secondary: var(--secondary);
  14. --color-secondary-foreground: var(--secondary-foreground);
  15. --color-muted: var(--muted);
  16. --color-muted-foreground: var(--muted-foreground);
  17. --color-accent: var(--accent);
  18. --color-accent-foreground: var(--accent-foreground);
  19. --color-destructive: var(--destructive);
  20. --color-destructive-foreground: var(--destructive-foreground);
  21. --color-border: var(--border);
  22. --color-card-edge: var(--card-edge);
  23. --color-input: var(--input);
  24. --color-ring: var(--ring);
  25. --color-chart-1: var(--chart-1);
  26. --color-chart-2: var(--chart-2);
  27. --color-chart-3: var(--chart-3);
  28. --color-chart-4: var(--chart-4);
  29. --color-chart-5: var(--chart-5);
  30. --color-sidebar: var(--sidebar);
  31. --color-sidebar-foreground: var(--sidebar-foreground);
  32. --color-sidebar-primary: var(--sidebar-primary);
  33. --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
  34. --color-sidebar-accent: var(--sidebar-accent);
  35. --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
  36. --color-sidebar-border: var(--sidebar-border);
  37. --color-sidebar-ring: var(--sidebar-ring);
  38. --radius-sm: calc(var(--radius) - 4px);
  39. --radius-md: calc(var(--radius) - 2px);
  40. --radius-lg: var(--radius);
  41. --radius-xl: calc(var(--radius) + 4px);
  42. --font-sans: var(--font-geist-sans);
  43. --font-mono: var(--font-geist-mono);
  44. --font-display: var(--font-display-active);
  45. }
  46. /* ── Light: warm cream + amber/gold primary ── */
  47. :root {
  48. --radius: 0.625rem;
  49. --background: oklch(0.98 0.008 85);
  50. --foreground: oklch(0.18 0.02 75);
  51. --card: oklch(0.995 0.004 85);
  52. --card-foreground: oklch(0.18 0.02 75);
  53. --popover: oklch(0.995 0.004 85);
  54. --popover-foreground: oklch(0.18 0.02 75);
  55. --primary: oklch(0.72 0.17 75);
  56. --primary-foreground: oklch(0.16 0.03 75);
  57. --secondary: oklch(0.94 0.02 85);
  58. --secondary-foreground: oklch(0.30 0.03 75);
  59. --muted: oklch(0.95 0.01 85);
  60. --muted-foreground: oklch(0.52 0.03 75);
  61. --accent: oklch(0.93 0.04 80);
  62. --accent-foreground: oklch(0.25 0.03 75);
  63. --destructive: oklch(0.55 0.22 27);
  64. --destructive-foreground: oklch(0.98 0 0);
  65. --border: oklch(0.91 0.02 85);
  66. --input: oklch(0.91 0.02 85);
  67. --ring: oklch(0.72 0.17 75);
  68. --chart-1: oklch(0.72 0.17 75);
  69. --chart-2: oklch(0.65 0.15 160);
  70. --chart-3: oklch(0.60 0.18 30);
  71. --chart-4: oklch(0.60 0.12 280);
  72. --chart-5: oklch(0.55 0.14 340);
  73. --sidebar: oklch(0.97 0.01 80);
  74. --sidebar-foreground: oklch(0.18 0.02 75);
  75. --sidebar-primary: oklch(0.72 0.17 75);
  76. --sidebar-primary-foreground: oklch(0.16 0.03 75);
  77. --sidebar-accent: oklch(0.93 0.04 80);
  78. --sidebar-accent-foreground: oklch(0.25 0.03 75);
  79. --sidebar-border: oklch(0.91 0.02 85);
  80. --sidebar-ring: oklch(0.72 0.17 75);
  81. }
  82. /* ── Dark: deep charcoal + vibrant amber/gold ── */
  83. .dark {
  84. --background: oklch(0.12 0.01 70);
  85. --foreground: oklch(0.93 0.01 85);
  86. --card: oklch(0.16 0.012 70);
  87. --card-foreground: oklch(0.93 0.01 85);
  88. --popover: oklch(0.16 0.012 70);
  89. --popover-foreground: oklch(0.93 0.01 85);
  90. --primary: oklch(0.78 0.16 80);
  91. --primary-foreground: oklch(0.14 0.02 70);
  92. --secondary: oklch(0.22 0.015 70);
  93. --secondary-foreground: oklch(0.90 0.01 85);
  94. --muted: oklch(0.20 0.01 70);
  95. --muted-foreground: oklch(0.58 0.02 80);
  96. --accent: oklch(0.24 0.03 75);
  97. --accent-foreground: oklch(0.90 0.01 85);
  98. --destructive: oklch(0.55 0.22 27);
  99. --destructive-foreground: oklch(0.98 0 0);
  100. --border: oklch(0.25 0.015 70);
  101. --input: oklch(0.25 0.015 70);
  102. --ring: oklch(0.78 0.16 80);
  103. --chart-1: oklch(0.78 0.16 80);
  104. --chart-2: oklch(0.65 0.15 160);
  105. --chart-3: oklch(0.60 0.18 30);
  106. --chart-4: oklch(0.60 0.12 280);
  107. --chart-5: oklch(0.55 0.14 340);
  108. --sidebar: oklch(0.14 0.012 70);
  109. --sidebar-foreground: oklch(0.93 0.01 85);
  110. --sidebar-primary: oklch(0.78 0.16 80);
  111. --sidebar-primary-foreground: oklch(0.14 0.02 70);
  112. --sidebar-accent: oklch(0.24 0.03 75);
  113. --sidebar-accent-foreground: oklch(0.90 0.01 85);
  114. --sidebar-border: oklch(0.25 0.015 70);
  115. --sidebar-ring: oklch(0.78 0.16 80);
  116. }
  117. /* ── Theme presets ───────────────────────────────────────────────────────
  118. Each preset is applied on top of its base mode class (`light`/`dark`), so
  119. `dark:` utilities and the `.dark`-scoped rules further down keep working.
  120. These blocks must stay after `:root` and `.dark`: they carry the same
  121. specificity, so source order is what makes them win. ── */
  122. /* ── Graphite (light): neutral grey + steel blue ── */
  123. .theme-graphite {
  124. --background: oklch(0.98 0.002 250);
  125. --foreground: oklch(0.20 0.01 250);
  126. --card: oklch(1 0 0);
  127. --card-foreground: oklch(0.20 0.01 250);
  128. --popover: oklch(1 0 0);
  129. --popover-foreground: oklch(0.20 0.01 250);
  130. --primary: oklch(0.52 0.13 250);
  131. --primary-foreground: oklch(0.99 0 0);
  132. --secondary: oklch(0.95 0.005 250);
  133. --secondary-foreground: oklch(0.30 0.012 250);
  134. --muted: oklch(0.96 0.004 250);
  135. --muted-foreground: oklch(0.50 0.015 250);
  136. --accent: oklch(0.93 0.02 250);
  137. --accent-foreground: oklch(0.26 0.02 250);
  138. --destructive: oklch(0.55 0.22 27);
  139. --destructive-foreground: oklch(0.99 0 0);
  140. --border: oklch(0.91 0.006 250);
  141. --input: oklch(0.91 0.006 250);
  142. --ring: oklch(0.52 0.13 250);
  143. --chart-1: oklch(0.52 0.13 250);
  144. --chart-2: oklch(0.62 0.12 190);
  145. --chart-3: oklch(0.72 0.15 75);
  146. --chart-4: oklch(0.55 0.14 300);
  147. --chart-5: oklch(0.58 0.16 20);
  148. --sidebar: oklch(0.965 0.004 250);
  149. --sidebar-foreground: oklch(0.20 0.01 250);
  150. --sidebar-primary: oklch(0.52 0.13 250);
  151. --sidebar-primary-foreground: oklch(0.99 0 0);
  152. --sidebar-accent: oklch(0.93 0.02 250);
  153. --sidebar-accent-foreground: oklch(0.26 0.02 250);
  154. --sidebar-border: oklch(0.91 0.006 250);
  155. --sidebar-ring: oklch(0.52 0.13 250);
  156. }
  157. /* ── Ocean (light): cool blue paper + marine blue ── */
  158. .theme-ocean {
  159. --background: oklch(0.98 0.012 220);
  160. --foreground: oklch(0.20 0.03 235);
  161. --card: oklch(0.995 0.006 220);
  162. --card-foreground: oklch(0.20 0.03 235);
  163. --popover: oklch(0.995 0.006 220);
  164. --popover-foreground: oklch(0.20 0.03 235);
  165. --primary: oklch(0.52 0.12 215);
  166. --primary-foreground: oklch(0.99 0.005 220);
  167. --secondary: oklch(0.93 0.025 215);
  168. --secondary-foreground: oklch(0.28 0.04 230);
  169. --muted: oklch(0.95 0.015 215);
  170. --muted-foreground: oklch(0.50 0.035 225);
  171. --accent: oklch(0.91 0.045 205);
  172. --accent-foreground: oklch(0.26 0.05 230);
  173. --destructive: oklch(0.55 0.22 27);
  174. --destructive-foreground: oklch(0.99 0 0);
  175. --border: oklch(0.90 0.025 215);
  176. --input: oklch(0.90 0.025 215);
  177. --ring: oklch(0.52 0.12 215);
  178. --chart-1: oklch(0.52 0.12 215);
  179. --chart-2: oklch(0.65 0.13 175);
  180. --chart-3: oklch(0.60 0.14 260);
  181. --chart-4: oklch(0.70 0.13 90);
  182. --chart-5: oklch(0.58 0.16 340);
  183. --sidebar: oklch(0.96 0.018 215);
  184. --sidebar-foreground: oklch(0.20 0.03 235);
  185. --sidebar-primary: oklch(0.52 0.12 215);
  186. --sidebar-primary-foreground: oklch(0.99 0.005 220);
  187. --sidebar-accent: oklch(0.91 0.045 205);
  188. --sidebar-accent-foreground: oklch(0.26 0.05 230);
  189. --sidebar-border: oklch(0.90 0.025 215);
  190. --sidebar-ring: oklch(0.52 0.12 215);
  191. }
  192. /* ── Forest (light): soft sage + deep green ── */
  193. .theme-forest {
  194. --background: oklch(0.98 0.008 140);
  195. --foreground: oklch(0.20 0.02 150);
  196. --card: oklch(0.995 0.004 140);
  197. --card-foreground: oklch(0.20 0.02 150);
  198. --popover: oklch(0.995 0.004 140);
  199. --popover-foreground: oklch(0.20 0.02 150);
  200. --primary: oklch(0.50 0.12 150);
  201. --primary-foreground: oklch(0.98 0.01 140);
  202. --secondary: oklch(0.93 0.02 145);
  203. --secondary-foreground: oklch(0.28 0.03 150);
  204. --muted: oklch(0.95 0.012 145);
  205. --muted-foreground: oklch(0.50 0.025 150);
  206. --accent: oklch(0.92 0.04 145);
  207. --accent-foreground: oklch(0.26 0.04 150);
  208. --destructive: oklch(0.55 0.22 27);
  209. --destructive-foreground: oklch(0.99 0 0);
  210. --border: oklch(0.90 0.02 145);
  211. --input: oklch(0.90 0.02 145);
  212. --ring: oklch(0.50 0.12 150);
  213. --chart-1: oklch(0.50 0.12 150);
  214. --chart-2: oklch(0.62 0.13 100);
  215. --chart-3: oklch(0.60 0.14 200);
  216. --chart-4: oklch(0.68 0.15 65);
  217. --chart-5: oklch(0.55 0.15 25);
  218. --sidebar: oklch(0.96 0.014 145);
  219. --sidebar-foreground: oklch(0.20 0.02 150);
  220. --sidebar-primary: oklch(0.50 0.12 150);
  221. --sidebar-primary-foreground: oklch(0.98 0.01 140);
  222. --sidebar-accent: oklch(0.92 0.04 145);
  223. --sidebar-accent-foreground: oklch(0.26 0.04 150);
  224. --sidebar-border: oklch(0.90 0.02 145);
  225. --sidebar-ring: oklch(0.50 0.12 150);
  226. }
  227. /* ── Midnight (dark): deep navy + sky blue ── */
  228. .theme-midnight {
  229. --background: oklch(0.16 0.03 260);
  230. --foreground: oklch(0.93 0.015 250);
  231. --card: oklch(0.20 0.035 260);
  232. --card-foreground: oklch(0.93 0.015 250);
  233. --popover: oklch(0.20 0.035 260);
  234. --popover-foreground: oklch(0.93 0.015 250);
  235. --primary: oklch(0.70 0.14 245);
  236. --primary-foreground: oklch(0.15 0.04 260);
  237. --secondary: oklch(0.26 0.035 260);
  238. --secondary-foreground: oklch(0.90 0.015 250);
  239. --muted: oklch(0.24 0.03 260);
  240. --muted-foreground: oklch(0.65 0.03 250);
  241. --accent: oklch(0.30 0.05 255);
  242. --accent-foreground: oklch(0.92 0.02 250);
  243. --destructive: oklch(0.55 0.22 27);
  244. --destructive-foreground: oklch(0.98 0 0);
  245. --border: oklch(0.30 0.04 260);
  246. --input: oklch(0.30 0.04 260);
  247. --ring: oklch(0.70 0.14 245);
  248. --chart-1: oklch(0.70 0.14 245);
  249. --chart-2: oklch(0.72 0.13 190);
  250. --chart-3: oklch(0.70 0.14 300);
  251. --chart-4: oklch(0.78 0.14 85);
  252. --chart-5: oklch(0.68 0.16 350);
  253. --sidebar: oklch(0.18 0.032 260);
  254. --sidebar-foreground: oklch(0.93 0.015 250);
  255. --sidebar-primary: oklch(0.70 0.14 245);
  256. --sidebar-primary-foreground: oklch(0.15 0.04 260);
  257. --sidebar-accent: oklch(0.30 0.05 255);
  258. --sidebar-accent-foreground: oklch(0.92 0.02 250);
  259. --sidebar-border: oklch(0.30 0.04 260);
  260. --sidebar-ring: oklch(0.70 0.14 245);
  261. }
  262. /* ── Carbon (dark): true neutral black + mint ── */
  263. .theme-carbon {
  264. --background: oklch(0.15 0 0);
  265. --foreground: oklch(0.94 0 0);
  266. --card: oklch(0.19 0 0);
  267. --card-foreground: oklch(0.94 0 0);
  268. --popover: oklch(0.19 0 0);
  269. --popover-foreground: oklch(0.94 0 0);
  270. --primary: oklch(0.72 0.15 165);
  271. --primary-foreground: oklch(0.16 0.02 165);
  272. --secondary: oklch(0.25 0 0);
  273. --secondary-foreground: oklch(0.92 0 0);
  274. --muted: oklch(0.22 0 0);
  275. --muted-foreground: oklch(0.66 0 0);
  276. --accent: oklch(0.28 0.02 165);
  277. --accent-foreground: oklch(0.94 0 0);
  278. --destructive: oklch(0.58 0.22 27);
  279. --destructive-foreground: oklch(0.98 0 0);
  280. --border: oklch(0.27 0 0);
  281. --input: oklch(0.27 0 0);
  282. --ring: oklch(0.72 0.15 165);
  283. --chart-1: oklch(0.72 0.15 165);
  284. --chart-2: oklch(0.70 0.13 220);
  285. --chart-3: oklch(0.75 0.15 90);
  286. --chart-4: oklch(0.65 0.15 300);
  287. --chart-5: oklch(0.62 0.18 20);
  288. --sidebar: oklch(0.17 0 0);
  289. --sidebar-foreground: oklch(0.94 0 0);
  290. --sidebar-primary: oklch(0.72 0.15 165);
  291. --sidebar-primary-foreground: oklch(0.16 0.02 165);
  292. --sidebar-accent: oklch(0.28 0.02 165);
  293. --sidebar-accent-foreground: oklch(0.94 0 0);
  294. --sidebar-border: oklch(0.27 0 0);
  295. --sidebar-ring: oklch(0.72 0.15 165);
  296. }
  297. /* Card edges. --border sits about one shade off the card fill, which reads as
  298. no edge at all once a shadow is over it. This mixes the theme's own
  299. foreground into the edge instead, so it stays visible in every theme —
  300. dark ones included — without hardcoding a colour per theme. Declared on the
  301. same element the theme classes land on (documentElement), so `var(--foreground)`
  302. resolves to whichever theme is active. */
  303. :root {
  304. --card-edge: color-mix(in oklab, var(--foreground) 20%, transparent);
  305. }
  306. @layer base {
  307. * {
  308. @apply border-border outline-ring/50;
  309. }
  310. html {
  311. /* Reserve scrollbar space so its appearance never shifts layout width
  312. (the dashboard grid measures its container on mount) */
  313. scrollbar-gutter: stable;
  314. }
  315. body {
  316. @apply bg-background text-foreground;
  317. }
  318. }
  319. /* Font sets (src/lib/font-sets.ts), chosen on the Appearance page.
  320. The theme's font utilities are inlined, so `font-sans` compiles straight to
  321. var(--font-geist-sans) and there is no --font-sans left to override. The
  322. swap is therefore made one level down: fonts.css declares Geist under
  323. --font-geist-sans-base, and --font-geist-sans, the name everything uses, is
  324. pointed at it here or at another face by a set. Every `font-sans` and
  325. `font-mono` in the app follows, and the picker can still draw the default
  326. set from the *-base names while another one is active.
  327. --font-display-active is the heading face: the text face by default, Barlow
  328. Condensed in the workshop set, where a heading is also set in capitals and a
  329. size up, because a condensed face reads a size smaller than it is. */
  330. body {
  331. --font-geist-sans: var(--font-geist-sans-base);
  332. --font-geist-mono: var(--font-geist-mono-base);
  333. --font-display-active: var(--font-geist-sans-base);
  334. }
  335. html.font-set-workshop body {
  336. --font-geist-sans: var(--font-plex-sans);
  337. --font-geist-mono: var(--font-plex-mono);
  338. --font-display-active: var(--font-barlow-condensed);
  339. }
  340. html.font-set-precise body {
  341. --font-geist-sans: var(--font-inter);
  342. --font-geist-mono: var(--font-jetbrains-mono);
  343. --font-display-active: var(--font-inter-tight);
  344. }
  345. html.font-set-legible body {
  346. --font-geist-sans: var(--font-atkinson);
  347. --font-geist-mono: var(--font-atkinson-mono);
  348. --font-display-active: var(--font-atkinson);
  349. }
  350. /* Inter's clearer forms, where the font file carries them: a slashed zero and
  351. a lowercase l with a tail, so a part number cannot be read as a word. A
  352. feature a build of the font lacks is simply ignored. */
  353. html.font-set-precise body {
  354. font-feature-settings: 'zero', 'cv05';
  355. }
  356. html.font-set-workshop .font-display {
  357. text-transform: uppercase;
  358. letter-spacing: 0.03em;
  359. font-size: 1.22em;
  360. line-height: 1.1;
  361. }
  362. /* The work order number: figures in the default set, the heading face in the
  363. workshop set, as the mock has it. */
  364. .order-number {
  365. font-family: var(--font-geist-mono);
  366. }
  367. html.font-set-workshop .order-number {
  368. font-family: var(--font-barlow-condensed);
  369. font-size: 1.3em;
  370. letter-spacing: 0.01em;
  371. }
  372. /* Dialogs lock body scroll through react-remove-scroll, which then pushes
  373. the body in by one scrollbar width to stop the page jumping when the bar
  374. goes. The gutter above already keeps that space, so the push is the jump:
  375. the whole page slid left every time the search or plate lookup opened.
  376. Outranks the library's injected !important rule by specificity. */
  377. html body[data-scroll-locked] {
  378. margin-right: 0 !important;
  379. padding-right: 0 !important;
  380. }
  381. /* Keyboard-focusable rows (see src/lib/interactive-row.ts). Inset outline so
  382. the ring survives overflow-hidden card and table corners. */
  383. [data-row-interactive] {
  384. cursor: pointer;
  385. }
  386. [data-row-interactive]:focus-visible {
  387. outline: 2px solid var(--ring);
  388. outline-offset: -2px;
  389. }
  390. /* Hide scrollbar utility */
  391. .scrollbar-none {
  392. scrollbar-width: none;
  393. }
  394. .scrollbar-none::-webkit-scrollbar {
  395. display: none;
  396. }
  397. /* Scroll-edge shadows: a soft shadow at the top or bottom edge only while content is
  398. hidden in that direction. Pure CSS: the covering gradients scroll with the content
  399. (attachment: local) and slide off the fixed shadows as soon as there is more to see. */
  400. .scroll-shadows {
  401. --scroll-shadow: color-mix(in oklab, var(--foreground) 9%, transparent);
  402. background:
  403. linear-gradient(var(--background) 40%, transparent) center top / 100% 32px local,
  404. linear-gradient(transparent, var(--background) 60%) center bottom / 100% 32px local,
  405. radial-gradient(farthest-side at 50% 0, var(--scroll-shadow), transparent) center top / 100% 12px scroll,
  406. radial-gradient(farthest-side at 50% 100%, var(--scroll-shadow), transparent) center bottom / 100% 12px scroll;
  407. background-repeat: no-repeat;
  408. background-color: var(--background);
  409. }
  410. .dark .scroll-shadows {
  411. --scroll-shadow: color-mix(in oklab, var(--foreground) 14%, transparent);
  412. }
  413. /* Custom scrollbar */
  414. ::-webkit-scrollbar {
  415. width: 6px;
  416. height: 6px;
  417. }
  418. ::-webkit-scrollbar-track {
  419. background: transparent;
  420. }
  421. /* Derived from the theme tokens so every preset gets a matching thumb */
  422. ::-webkit-scrollbar-thumb {
  423. background: color-mix(in oklab, var(--muted-foreground) 45%, transparent);
  424. border-radius: 3px;
  425. }
  426. .dark ::-webkit-scrollbar-thumb {
  427. background: color-mix(in oklab, var(--muted-foreground) 40%, transparent);
  428. }
  429. /* Glass effect utility */
  430. .glass {
  431. background: color-mix(in oklab, var(--card) 70%, transparent);
  432. backdrop-filter: blur(16px);
  433. border: 1px solid color-mix(in oklab, var(--border) 60%, transparent);
  434. }
  435. /* Gradient text */
  436. .gradient-text {
  437. background: linear-gradient(
  438. 135deg,
  439. var(--primary),
  440. color-mix(in oklab, var(--primary) 55%, var(--chart-3))
  441. );
  442. -webkit-background-clip: text;
  443. -webkit-text-fill-color: transparent;
  444. background-clip: text;
  445. }
  446. /* Subtle grid bg */
  447. .grid-bg {
  448. background-image:
  449. linear-gradient(color-mix(in oklab, var(--border) 45%, transparent) 1px, transparent 1px),
  450. linear-gradient(90deg, color-mix(in oklab, var(--border) 45%, transparent) 1px, transparent 1px);
  451. background-size: 40px 40px;
  452. }
  453. /* Tiptap editor styles */
  454. .tiptap-content {
  455. overflow-wrap: anywhere;
  456. &:focus {
  457. outline: none;
  458. }
  459. > * + * {
  460. margin-top: 0.5em;
  461. }
  462. h2 {
  463. font-size: 1.25rem;
  464. font-weight: 600;
  465. line-height: 1.4;
  466. }
  467. h3 {
  468. font-size: 1.1rem;
  469. font-weight: 600;
  470. line-height: 1.4;
  471. }
  472. ul {
  473. list-style: disc;
  474. padding-left: 1.25rem;
  475. }
  476. ol {
  477. list-style: decimal;
  478. padding-left: 1.25rem;
  479. }
  480. li {
  481. margin-top: 0.15em;
  482. }
  483. blockquote {
  484. border-left: 3px solid var(--border);
  485. padding-left: 0.75rem;
  486. color: var(--muted-foreground);
  487. font-style: italic;
  488. }
  489. a {
  490. color: var(--primary);
  491. text-decoration: underline;
  492. cursor: pointer;
  493. }
  494. p.is-editor-empty:first-child::before {
  495. content: attr(data-placeholder);
  496. float: left;
  497. color: var(--muted-foreground);
  498. pointer-events: none;
  499. height: 0;
  500. }
  501. }
  502. /* Notes display (rendered HTML) */
  503. .notes-content {
  504. overflow-wrap: anywhere;
  505. h2 {
  506. font-size: 1.25rem;
  507. font-weight: 600;
  508. line-height: 1.4;
  509. }
  510. h3 {
  511. font-size: 1.1rem;
  512. font-weight: 600;
  513. line-height: 1.4;
  514. }
  515. ul {
  516. list-style: disc;
  517. padding-left: 1.25rem;
  518. }
  519. ol {
  520. list-style: decimal;
  521. padding-left: 1.25rem;
  522. }
  523. li {
  524. margin-top: 0.15em;
  525. }
  526. blockquote {
  527. border-left: 3px solid var(--border);
  528. padding-left: 0.75rem;
  529. color: var(--muted-foreground);
  530. font-style: italic;
  531. }
  532. a {
  533. color: var(--primary);
  534. text-decoration: underline;
  535. }
  536. > * + * {
  537. margin-top: 0.5em;
  538. }
  539. }
  540. /* AI chat markdown */
  541. .ai-markdown {
  542. > * + * {
  543. margin-top: 0.5em;
  544. }
  545. h1, h2, h3 {
  546. font-weight: 600;
  547. margin-top: 0.75em;
  548. }
  549. h1 { font-size: 1.25rem; }
  550. h2 { font-size: 1.1rem; }
  551. h3 { font-size: 1rem; }
  552. p { line-height: 1.5; }
  553. strong { font-weight: 600; }
  554. ul {
  555. list-style: disc;
  556. padding-left: 1.25rem;
  557. }
  558. ol {
  559. list-style: decimal;
  560. padding-left: 1.25rem;
  561. }
  562. li { margin-top: 0.15em; }
  563. code {
  564. font-size: 0.85em;
  565. background: var(--muted);
  566. padding: 0.15em 0.35em;
  567. border-radius: 4px;
  568. }
  569. pre {
  570. background: var(--muted);
  571. padding: 0.75rem 1rem;
  572. border-radius: 6px;
  573. overflow-x: auto;
  574. }
  575. pre code {
  576. background: none;
  577. padding: 0;
  578. }
  579. blockquote {
  580. border-left: 3px solid var(--border);
  581. padding-left: 0.75rem;
  582. color: var(--muted-foreground);
  583. font-style: italic;
  584. }
  585. .table-wrapper {
  586. overflow-x: auto;
  587. }
  588. table {
  589. width: 100%;
  590. border-collapse: collapse;
  591. margin: 0.5em 0;
  592. font-size: 0.875rem;
  593. white-space: nowrap;
  594. }
  595. th, td {
  596. padding: 0.5rem 0.75rem;
  597. text-align: left;
  598. border-bottom: 1px solid var(--border);
  599. }
  600. th {
  601. font-weight: 600;
  602. background: var(--muted);
  603. }
  604. tr:last-child td {
  605. border-bottom: none;
  606. }
  607. }
  608. /* ─── Dashboard grid (react-grid-layout) ─────────────────────────────── */
  609. .dashboard-grid .react-grid-item {
  610. transition: transform 200ms ease, width 200ms ease, height 200ms ease;
  611. }
  612. .dashboard-grid .react-grid-item.react-grid-placeholder {
  613. background: color-mix(in oklab, var(--primary) 12%, transparent);
  614. border: 2px dashed color-mix(in oklab, var(--primary) 50%, transparent);
  615. border-radius: var(--radius, 0.75rem);
  616. opacity: 1;
  617. }
  618. .dashboard-grid .react-grid-item.react-draggable-dragging,
  619. .dashboard-grid .react-grid-item.resizing {
  620. z-index: 30;
  621. transition: none;
  622. box-shadow: 0 12px 32px -8px rgb(0 0 0 / 0.25);
  623. }
  624. /* Replace the faint default resize arrow with a clear corner grip in the
  625. theme color; only rendered in edit mode (when resizing is enabled) */
  626. .dashboard-grid .react-grid-item > .react-resizable-handle {
  627. z-index: 20;
  628. width: 28px;
  629. height: 28px;
  630. bottom: 0;
  631. right: 0;
  632. padding: 0;
  633. background-image: none;
  634. cursor: se-resize;
  635. }
  636. .dashboard-grid .react-grid-item > .react-resizable-handle::after {
  637. content: "";
  638. position: absolute;
  639. right: 7px;
  640. bottom: 7px;
  641. width: 13px;
  642. height: 13px;
  643. border-right: 3px solid var(--primary);
  644. border-bottom: 3px solid var(--primary);
  645. border-bottom-right-radius: 5px;
  646. opacity: 0.9;
  647. transition: transform 120ms ease, opacity 120ms ease;
  648. }
  649. .dashboard-grid .react-grid-item > .react-resizable-handle:hover::after,
  650. .dashboard-grid .react-grid-item.resizing > .react-resizable-handle::after {
  651. transform: scale(1.25);
  652. opacity: 1;
  653. }
  654. .dashboard-grid-editing .react-grid-item {
  655. user-select: none;
  656. }
  657. /* Cards fill their grid tile; the last section (CardContent) scrolls */
  658. .dashboard-card-wrap {
  659. height: 100%;
  660. }
  661. .dashboard-card-wrap > *:first-child {
  662. height: 100%;
  663. display: flex;
  664. flex-direction: column;
  665. overflow: hidden;
  666. }
  667. /* Legacy Card markup: whatever comes last is the content, so it scrolls.
  668. Guarded so an AppCard footer (which is also a last child) never becomes
  669. the scroll area. */
  670. .dashboard-card-wrap > *:first-child > *:last-child:not([data-slot="app-card-footer"]) {
  671. flex: 1;
  672. min-height: 0;
  673. overflow-y: auto;
  674. }
  675. /* AppCard names its content region, so target it directly. */
  676. .dashboard-card-wrap > *:first-child > [data-slot="app-card-content"] {
  677. flex: 1;
  678. min-height: 0;
  679. overflow-y: auto;
  680. }
  681. /* Masks a value without a password input. A password field anywhere in a card
  682. makes Chrome read the whole card as a sign-in form and offer the user's
  683. saved email into the text field above it, which is how provider credential
  684. forms end up autofilled with an address. Masking in CSS keeps the secret
  685. hidden and leaves the browser nothing to recognise. */
  686. .masked-value {
  687. -webkit-text-security: disc;
  688. text-security: disc;
  689. }
  690. /* Toasts in the app's own look, for every toast sonner draws: the glass
  691. surface and icon tile of the upgrade dialog, and the primary hairline the
  692. layout runs along the top of the window. One step more specific than
  693. sonner's own `[data-sonner-toast][data-styled='true']` rules, so these win
  694. without !important. `--toast-accent` is the one colour that changes with
  695. the kind of toast. Sonner uses both pseudo-elements itself (swipe and the
  696. hover gap), so the hairline is a background layer instead. */
  697. [data-sonner-toaster] [data-sonner-toast][data-styled='true'] {
  698. --toast-accent: var(--primary);
  699. background:
  700. linear-gradient(
  701. to right,
  702. var(--toast-accent),
  703. color-mix(in oklab, var(--toast-accent) 35%, transparent) 45%,
  704. transparent
  705. )
  706. top / 100% 1px no-repeat,
  707. radial-gradient(
  708. 9rem 5rem at 1.9rem 50%,
  709. color-mix(in oklab, var(--toast-accent) 12%, transparent),
  710. transparent 70%
  711. ),
  712. color-mix(in oklab, var(--popover) 90%, transparent);
  713. -webkit-backdrop-filter: blur(16px);
  714. backdrop-filter: blur(16px);
  715. border-color: color-mix(in oklab, var(--border) 70%, transparent);
  716. border-radius: calc(var(--radius) + 4px);
  717. box-shadow:
  718. inset 0 1px 0 rgb(255 255 255 / 0.04),
  719. 0 18px 40px -16px rgb(0 0 0 / 0.45);
  720. padding: 12px 14px;
  721. gap: 12px;
  722. }
  723. [data-sonner-toaster] [data-sonner-toast][data-type='error'] {
  724. --toast-accent: var(--destructive);
  725. }
  726. [data-sonner-toaster] [data-sonner-toast][data-type='warning'] {
  727. --toast-accent: oklch(0.75 0.15 70);
  728. }
  729. /* The icon sits in a tile, like the upgrade dialog's */
  730. [data-sonner-toaster] [data-sonner-toast][data-styled='true'] [data-icon] {
  731. width: 2rem;
  732. height: 2rem;
  733. margin: 0;
  734. justify-content: center;
  735. border-radius: 0.5rem;
  736. color: var(--toast-accent);
  737. background: linear-gradient(
  738. to bottom,
  739. color-mix(in oklab, var(--toast-accent) 16%, transparent),
  740. color-mix(in oklab, var(--toast-accent) 5%, transparent)
  741. );
  742. box-shadow:
  743. inset 0 0 0 1px color-mix(in oklab, var(--toast-accent) 28%, transparent),
  744. inset 0 1px 0 rgb(255 255 255 / 0.12);
  745. }
  746. [data-sonner-toaster] [data-sonner-toast][data-styled='true'] [data-icon] svg {
  747. margin: 0;
  748. }
  749. [data-sonner-toaster] [data-sonner-toast][data-styled='true'] [data-title] {
  750. font-weight: 600;
  751. letter-spacing: -0.01em;
  752. }
  753. [data-sonner-toaster] [data-sonner-toast][data-styled='true'] [data-description] {
  754. color: var(--muted-foreground);
  755. }
  756. [data-sonner-toaster] [data-sonner-toast][data-styled='true'] [data-button] {
  757. background: var(--primary);
  758. color: var(--primary-foreground);
  759. border-radius: calc(var(--radius) - 2px);
  760. font-weight: 500;
  761. }
  762. [data-sonner-toaster] [data-sonner-toast][data-styled='true'] [data-cancel] {
  763. background: transparent;
  764. color: var(--muted-foreground);
  765. box-shadow: inset 0 0 0 1px var(--border);
  766. }
  767. /* Search palette and settings deep links.
  768. The spotlight is what a deep link from the palette lands on: the card or
  769. the field grows a ring in the theme colour that fades over a couple of
  770. seconds, so the eye is taken to the thing searched for rather than left at
  771. the top of the page. The progress line sits under the palette's input
  772. while the workshop's records are being fetched; local matches arrive on
  773. the keystroke itself and never wait for it. */
  774. @keyframes settings-spotlight {
  775. 0% {
  776. box-shadow: 0 0 0 0 color-mix(in oklab, var(--primary) 0%, transparent);
  777. }
  778. 20% {
  779. box-shadow:
  780. 0 0 0 3px color-mix(in oklab, var(--primary) 55%, transparent),
  781. 0 0 28px 6px color-mix(in oklab, var(--primary) 22%, transparent);
  782. }
  783. 100% {
  784. box-shadow:
  785. 0 0 0 3px color-mix(in oklab, var(--primary) 0%, transparent),
  786. 0 0 0 0 color-mix(in oklab, var(--primary) 0%, transparent);
  787. }
  788. }
  789. .settings-spotlight {
  790. animation: settings-spotlight 2.4s cubic-bezier(0.2, 0.7, 0.2, 1) 1;
  791. border-radius: max(var(--radius), 6px);
  792. }
  793. @keyframes search-progress {
  794. 0% {
  795. transform: translateX(-100%);
  796. }
  797. 100% {
  798. transform: translateX(300%);
  799. }
  800. }
  801. .search-progress {
  802. position: absolute;
  803. inset-inline: 0;
  804. bottom: -1px;
  805. height: 2px;
  806. overflow: hidden;
  807. pointer-events: none;
  808. }
  809. .search-progress::after {
  810. content: '';
  811. position: absolute;
  812. inset-block: 0;
  813. left: 0;
  814. width: 33%;
  815. border-radius: 999px;
  816. background: linear-gradient(90deg, transparent, var(--primary), transparent);
  817. animation: search-progress 1.1s ease-in-out infinite;
  818. }
  819. /* Loading skeletons: a soft wave across the placeholder instead of a blink.
  820. Built from currentColor, so it follows the text colour in light, dark and
  821. every colour theme without a token of its own. In the components layer, so
  822. a page can still restyle one skeleton with utilities. With reduced motion
  823. the wave stands still. */
  824. :root {
  825. --skeleton-base: color-mix(in srgb, currentColor 9%, transparent);
  826. --skeleton-highlight: color-mix(in srgb, currentColor 20%, transparent);
  827. --skeleton-bg: linear-gradient(
  828. 90deg,
  829. var(--skeleton-base) 0%,
  830. var(--skeleton-highlight) 50%,
  831. var(--skeleton-base) 100%
  832. );
  833. }
  834. @keyframes skeleton-wave {
  835. 0% {
  836. background-position: 200% 0;
  837. }
  838. 100% {
  839. background-position: -200% 0;
  840. }
  841. }
  842. @media (prefers-reduced-motion: reduce) {
  843. @keyframes skeleton-wave {
  844. 0%,
  845. 100% {
  846. background-position: 50% 0;
  847. }
  848. }
  849. }
  850. @layer components {
  851. .skeleton {
  852. background: var(--skeleton-bg);
  853. background-size: 200% 100%;
  854. animation: skeleton-wave 1.6s linear infinite;
  855. }
  856. }