index.css 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607
  1. @import "tailwindcss";
  2. /* Inter, self-hosted (#1460). Bambuddy is a local-first, offline-capable PWA;
  3. pulling the font from fonts.googleapis.com at runtime broke the install on
  4. Android (CSP-blocked cross-origin fetch) and meant the UI couldn't render
  5. offline. These are the variable woff2 files Google Fonts serves — one file
  6. covers every weight via the variable axis, so font-weight is a 100-900 range.
  7. Files live in frontend/public/fonts/ and are served same-origin. */
  8. @font-face {
  9. font-family: 'Inter';
  10. font-style: normal;
  11. font-weight: 100 900;
  12. font-display: swap;
  13. src: url('/fonts/inter-latin.woff2') format('woff2');
  14. unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
  15. }
  16. @font-face {
  17. font-family: 'Inter';
  18. font-style: normal;
  19. font-weight: 100 900;
  20. font-display: swap;
  21. src: url('/fonts/inter-latin-ext.woff2') format('woff2');
  22. unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
  23. }
  24. /* Enable class-based dark mode for Tailwind v4 */
  25. @custom-variant dark (&:where(.dark, .dark *));
  26. /* Hover-capable pointer only (#2865). Tailwind v4 compiles `hover:` AND
  27. `group-hover:` inside `@media (hover: hover)`, so on a touch-only device the
  28. reveal rule is never applied at all — a control written as
  29. `opacity-0 group-hover:opacity-100` stays invisible forever, which is how the
  30. project card's action menu became unreachable on a phone. Gating the HIDING
  31. half on this variant is what fixes it: with no hover-capable pointer the
  32. element simply keeps its natural opacity. Same query the history thumbnail
  33. preview already uses further down. */
  34. @custom-variant can-hover (@media (hover: hover) and (pointer: fine));
  35. /* Restore the pointer cursor on interactive controls (#2791). Tailwind v3's
  36. Preflight set `button { cursor: pointer }`; v4 dropped it to match the
  37. browser default of `cursor: default`, so every button in the app looked
  38. unclickable unless someone remembered to add `cursor-pointer` by hand.
  39. Only a handful of the ~930 buttons did, which is why the UI felt
  40. inconsistent rather than uniformly wrong.
  41. This lives in `base`, the lowest of Tailwind's cascade layers, so the
  42. `cursor-not-allowed` / `disabled:cursor-*` utilities dotted around the
  43. codebase still win. The `:not(:disabled)` guard covers the elements that
  44. are disabled without also carrying such a utility. */
  45. @layer base {
  46. button:not(:disabled),
  47. select:not(:disabled),
  48. summary,
  49. input[type="checkbox"]:not(:disabled),
  50. input[type="radio"]:not(:disabled),
  51. [role="button"]:not([aria-disabled="true"]) {
  52. cursor: pointer;
  53. }
  54. }
  55. @theme {
  56. /* Accent colors - use CSS variables for theming */
  57. --color-bambu-green: var(--accent);
  58. --color-bambu-green-light: var(--accent-light);
  59. --color-bambu-green-dark: var(--accent-dark);
  60. /* Semantic status colors - fixed, don't change with accent */
  61. --color-status-ok: var(--status-ok);
  62. --color-status-error: var(--status-error);
  63. --color-status-warning: var(--status-warning);
  64. /* Theme-aware colors via CSS variables */
  65. --color-bambu-dark: var(--bg-primary);
  66. --color-bambu-dark-secondary: var(--bg-secondary);
  67. --color-bambu-dark-tertiary: var(--bg-tertiary);
  68. --color-bambu-gray: var(--text-muted);
  69. --color-bambu-gray-light: var(--text-secondary);
  70. --color-bambu-gray-dark: var(--text-tertiary);
  71. }
  72. /* ============================================
  73. BASE DEFAULTS
  74. ============================================ */
  75. html {
  76. font-size: 14.4px;
  77. }
  78. :root {
  79. /* Default accent color (green) */
  80. --accent: #00ae42;
  81. --accent-light: #00c64d;
  82. --accent-dark: #009438;
  83. /* Semantic status colors - these never change with accent theme */
  84. --status-ok: #22c55e; /* green-500 - always green for success/online/ok */
  85. --status-error: #ef4444; /* red-500 - always red for error/offline/failed */
  86. --status-warning: #f59e0b; /* amber-500 - always amber for warnings */
  87. /* Default light mode background (neutral) */
  88. --bg-primary: #f5f5f5;
  89. --bg-secondary: #ffffff;
  90. --bg-tertiary: #e5e5e5;
  91. --text-primary: #1a1a1a;
  92. --text-secondary: #4a4a4a;
  93. --text-muted: #6b6b6b;
  94. --text-tertiary: #808080;
  95. --border-color: #d4d4d4;
  96. /* Default style (classic) */
  97. --card-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
  98. --glow-color: transparent;
  99. /* Tell the browser which appearance the page is in, so the parts of a form
  100. control it draws itself follow the theme. Bambuddy themes by
  101. swapping CSS variables under a `.dark` class, which the browser cannot
  102. see: without this declaration it assumes light and paints every native
  103. widget internal that way -- number-input steppers, the date picker's
  104. calendar button and its popup, `<select>` dropdowns, scrollbars, the
  105. autofill highlight. That is why the quantity steppers in the print
  106. dialog were pale grey chips inside a dark field. Only the widget
  107. internals are affected; anything we set ourselves still wins. */
  108. color-scheme: light;
  109. font-family: 'Inter', system-ui, sans-serif;
  110. line-height: 1.5;
  111. font-weight: 400;
  112. font-synthesis: none;
  113. text-rendering: optimizeLegibility;
  114. -webkit-font-smoothing: antialiased;
  115. -moz-osx-font-smoothing: grayscale;
  116. }
  117. /* Dark mode base */
  118. .dark {
  119. /* Pairs with `color-scheme: light` on :root -- see the note there. This
  120. sits on `.dark` rather than on each `.dark.bg-*` palette because the
  121. kiosk adds `dark` to <html> on its own (SpoolBuddyLayout), and every
  122. palette is a dark one regardless of which background it picks. */
  123. color-scheme: dark;
  124. --bg-primary: #1a1a1a;
  125. --bg-secondary: #2d2d2d;
  126. --bg-tertiary: #3d3d3d;
  127. --text-primary: #ffffff;
  128. --text-secondary: #a0a0a0;
  129. --text-muted: #808080;
  130. --text-tertiary: #4a4a4a;
  131. --border-color: #3d3d3d;
  132. --card-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
  133. }
  134. /* ============================================
  135. LAYER 1: BACKGROUND PALETTES
  136. ============================================ */
  137. /* Light mode backgrounds */
  138. .bg-neutral {
  139. /* Default - already set in :root */
  140. }
  141. .bg-warm {
  142. --bg-primary: #faf8f5;
  143. --bg-secondary: #fffefa;
  144. --bg-tertiary: #e8e4dd;
  145. --text-primary: #2d2a26;
  146. --text-secondary: #5c5750;
  147. --text-muted: #7a756c;
  148. --text-tertiary: #9a9590;
  149. --border-color: #d8d4cc;
  150. }
  151. .bg-cool {
  152. --bg-primary: #f0f4f8;
  153. --bg-secondary: #ffffff;
  154. --bg-tertiary: #dce4ec;
  155. --text-primary: #1a2530;
  156. --text-secondary: #4a5568;
  157. --text-muted: #6b7a8a;
  158. --text-tertiary: #8a9aaa;
  159. --border-color: #c8d4e0;
  160. }
  161. /* Dark mode backgrounds */
  162. .dark.bg-neutral {
  163. --bg-primary: #1a1a1a;
  164. --bg-secondary: #2d2d2d;
  165. --bg-tertiary: #3d3d3d;
  166. --text-primary: #ffffff;
  167. --text-secondary: #a0a0a0;
  168. --text-muted: #808080;
  169. --text-tertiary: #4a4a4a;
  170. --border-color: #3d3d3d;
  171. }
  172. .dark.bg-warm {
  173. --bg-primary: #1c1a18;
  174. --bg-secondary: #2e2a26;
  175. --bg-tertiary: #3e3a36;
  176. --text-primary: #f5f0ea;
  177. --text-secondary: #b0a898;
  178. --text-muted: #8a8278;
  179. --text-tertiary: #5a5248;
  180. --border-color: #3e3a36;
  181. }
  182. .dark.bg-cool {
  183. --bg-primary: #181c20;
  184. --bg-secondary: #262c32;
  185. --bg-tertiary: #363e46;
  186. --text-primary: #f0f4f8;
  187. --text-secondary: #98a8b8;
  188. --text-muted: #788898;
  189. --text-tertiary: #4a5a6a;
  190. --border-color: #363e46;
  191. }
  192. .dark.bg-oled {
  193. --bg-primary: #000000;
  194. --bg-secondary: #141414;
  195. --bg-tertiary: #1f1f1f;
  196. --text-primary: #ffffff;
  197. --text-secondary: #a0a0a0;
  198. --text-muted: #707070;
  199. --text-tertiary: #404040;
  200. --border-color: #2a2a2a;
  201. }
  202. .dark.bg-slate {
  203. --bg-primary: #0f172a;
  204. --bg-secondary: #1e293b;
  205. --bg-tertiary: #334155;
  206. --text-primary: #f1f5f9;
  207. --text-secondary: #94a3b8;
  208. --text-muted: #64748b;
  209. --text-tertiary: #475569;
  210. --border-color: #334155;
  211. }
  212. .dark.bg-forest {
  213. --bg-primary: #121a16;
  214. --bg-secondary: #1c2a22;
  215. --bg-tertiary: #2a3d30;
  216. --text-primary: #e8f5ec;
  217. --text-secondary: #8aa894;
  218. --text-muted: #6a8874;
  219. --text-tertiary: #4a6854;
  220. --border-color: #2a3d30;
  221. }
  222. /* Printer card control buttons: stack only when they clip */
  223. .printer-control-buttons-container {
  224. container-type: inline-size;
  225. }
  226. @container (max-width: 220px) {
  227. .printer-control-buttons {
  228. flex-direction: column;
  229. align-items: stretch;
  230. }
  231. .printer-control-buttons > button {
  232. width: 100%;
  233. }
  234. }
  235. /* ============================================
  236. LAYER 2: STYLE EFFECTS
  237. ============================================ */
  238. /* Classic - default, clean minimal shadows */
  239. .style-classic {
  240. /* Uses default shadows from :root and .dark */
  241. }
  242. /* Glow - accent-colored glow effects on cards */
  243. .style-glow {
  244. --card-shadow: 0 2px 8px rgba(0, 0, 0, 0.08), 0 0 25px color-mix(in srgb, var(--accent) 12%, transparent);
  245. }
  246. .dark.style-glow {
  247. --card-shadow: 0 4px 20px rgba(0, 0, 0, 0.5), 0 0 40px color-mix(in srgb, var(--accent) 15%, transparent);
  248. }
  249. /* Vibrant - dramatic deep shadows, more contrast */
  250. .style-vibrant {
  251. --card-shadow: 0 8px 30px rgba(0, 0, 0, 0.15), 0 2px 8px rgba(0, 0, 0, 0.1);
  252. }
  253. .dark.style-vibrant {
  254. --card-shadow: 0 10px 40px rgba(0, 0, 0, 0.6), 0 4px 12px rgba(0, 0, 0, 0.4);
  255. }
  256. /* ============================================
  257. LAYER 3: ACCENT COLORS
  258. ============================================ */
  259. .accent-green {
  260. --accent: #00ae42;
  261. --accent-light: #00c64d;
  262. --accent-dark: #009438;
  263. }
  264. .accent-teal {
  265. --accent: #14b8a6;
  266. --accent-light: #2dd4bf;
  267. --accent-dark: #0d9488;
  268. }
  269. .accent-blue {
  270. --accent: #3b82f6;
  271. --accent-light: #60a5fa;
  272. --accent-dark: #2563eb;
  273. }
  274. .accent-orange {
  275. --accent: #f97316;
  276. --accent-light: #fb923c;
  277. --accent-dark: #ea580c;
  278. }
  279. .accent-purple {
  280. --accent: #8b5cf6;
  281. --accent-light: #a78bfa;
  282. --accent-dark: #7c3aed;
  283. }
  284. .accent-red {
  285. --accent: #ef4444;
  286. --accent-light: #f87171;
  287. --accent-dark: #dc2626;
  288. }
  289. body {
  290. background-color: var(--bg-primary);
  291. color: var(--text-primary);
  292. margin: 0;
  293. min-height: 100vh;
  294. transition: background-color 0.2s ease, color 0.2s ease;
  295. }
  296. #root {
  297. min-height: 100vh;
  298. }
  299. /* Override text-white to be theme-aware */
  300. .text-white {
  301. color: var(--text-primary);
  302. }
  303. /* Smooth transitions for theme changes */
  304. .bg-bambu-dark,
  305. .bg-bambu-dark-secondary,
  306. .bg-bambu-dark-tertiary,
  307. .border-bambu-dark-tertiary {
  308. transition: background-color 0.2s ease, border-color 0.2s ease;
  309. }
  310. /* Toast slide-in animation */
  311. @keyframes slide-in {
  312. from {
  313. transform: translateX(100%);
  314. opacity: 0;
  315. }
  316. to {
  317. transform: translateX(0);
  318. opacity: 1;
  319. }
  320. }
  321. .animate-slide-in {
  322. animation: slide-in 0.2s ease-out;
  323. }
  324. /* Theme-aware icon inversion - only invert in dark mode */
  325. .icon-theme {
  326. opacity: 0.5;
  327. }
  328. .dark .icon-theme {
  329. filter: invert(1);
  330. opacity: 0.4;
  331. }
  332. /* Green-colored icon for active status indicators */
  333. .icon-green {
  334. filter: invert(48%) sepia(89%) saturate(459%) hue-rotate(93deg) brightness(95%) contrast(92%);
  335. opacity: 1;
  336. }
  337. /* Orange/red-colored icon for heating indicators */
  338. .icon-heating {
  339. filter: invert(50%) sepia(100%) saturate(1000%) hue-rotate(360deg) brightness(100%) contrast(100%);
  340. opacity: 1;
  341. }
  342. /* Jogpad theme styling - darken background in dark mode */
  343. .jogpad-theme {
  344. /* Light mode - normal */
  345. }
  346. .dark .jogpad-theme {
  347. filter: brightness(0.35) contrast(1.2);
  348. }
  349. /* Empty AMS slot with diagonal stripes */
  350. .ams-empty-slot {
  351. background: repeating-linear-gradient(
  352. 45deg,
  353. #444,
  354. #444 2px,
  355. #222 2px,
  356. #222 4px
  357. );
  358. }
  359. .dark .ams-empty-slot {
  360. background: repeating-linear-gradient(
  361. 45deg,
  362. #555,
  363. #555 2px,
  364. #333 2px,
  365. #333 4px
  366. );
  367. }
  368. /* Touch manipulation to prevent zoom on double-tap */
  369. .touch-manipulation {
  370. touch-action: manipulation;
  371. }
  372. /* Safe area insets for notched devices */
  373. .safe-area-bottom {
  374. padding-bottom: env(safe-area-inset-bottom, 0);
  375. }
  376. .safe-area-top {
  377. padding-top: env(safe-area-inset-top, 0);
  378. }
  379. /* Hide scrollbar but keep functionality */
  380. .scrollbar-hide {
  381. -ms-overflow-style: none;
  382. scrollbar-width: none;
  383. }
  384. .scrollbar-hide::-webkit-scrollbar {
  385. display: none;
  386. }
  387. /* Mobile drawer animation */
  388. @keyframes slide-in-left {
  389. from {
  390. transform: translateX(-100%);
  391. }
  392. to {
  393. transform: translateX(0);
  394. }
  395. }
  396. .animate-slide-in-left {
  397. animation: slide-in-left 0.3s ease-out;
  398. }
  399. /* SpoolBuddy idle scan animation: transform/opacity only for low-power devices */
  400. @keyframes spoolbuddy-optimized-ping {
  401. 0% {
  402. transform: scale(0.8);
  403. opacity: 0.8;
  404. }
  405. 80%,
  406. 100% {
  407. transform: scale(2.2);
  408. opacity: 0;
  409. }
  410. }
  411. .spoolbuddy-optimized-ping {
  412. will-change: auto;
  413. contain: layout;
  414. animation: spoolbuddy-optimized-ping 3.5s cubic-bezier(0, 0, 0.2, 1) infinite;
  415. }
  416. .spoolbuddy-spool-glow {
  417. transform: scale(2.0);
  418. will-change: auto;
  419. }
  420. @media (prefers-reduced-motion: reduce) {
  421. .spoolbuddy-optimized-ping {
  422. animation: none;
  423. opacity: 0.25;
  424. transform: scale(1.2);
  425. }
  426. .spoolbuddy-spool-glow {
  427. transition: none;
  428. }
  429. }
  430. /* SpoolBuddy quick menu slide-down */
  431. @keyframes slide-down {
  432. from {
  433. transform: translateY(-100%);
  434. }
  435. to {
  436. transform: translateY(0);
  437. }
  438. }
  439. .animate-slide-down {
  440. animation: slide-down 0.25s ease-out;
  441. }
  442. /* Card shadows - uses theme-specific shadow */
  443. .card-shadow {
  444. box-shadow: var(--card-shadow);
  445. }
  446. /* ============================================
  447. SPOOLBUDDY KIOSK MODAL CONSTRAINTS
  448. Cap large modals (max-w-2xl) to viewport on the
  449. small SpoolBuddy touchscreen. Excludes smaller
  450. modals like the slot action picker (max-w-sm).
  451. ============================================ */
  452. [data-spoolbuddy-kiosk] .fixed .relative.max-w-2xl {
  453. height: 90vh;
  454. max-height: 90vh;
  455. display: flex;
  456. flex-direction: column;
  457. }
  458. [data-spoolbuddy-kiosk] .fixed .relative.max-w-2xl > div:first-child {
  459. flex-shrink: 0;
  460. }
  461. [data-spoolbuddy-kiosk] .fixed .relative.max-w-2xl > div:last-child {
  462. flex-shrink: 0;
  463. }
  464. [data-spoolbuddy-kiosk] .fixed .relative.max-w-2xl > div:nth-child(2) {
  465. flex: 1 1 auto;
  466. min-height: 0;
  467. overflow-y: auto;
  468. }
  469. /* Remove inner spool grid max-height to avoid nested scrollbars */
  470. [data-spoolbuddy-kiosk] .fixed .relative.max-w-2xl .max-h-96 {
  471. max-height: none;
  472. }
  473. /* Calendar selected-day list scrollbar theming */
  474. .calendar-scroll {
  475. scrollbar-width: thin;
  476. scrollbar-color: color-mix(in srgb, var(--text-muted) 60%, transparent) transparent;
  477. }
  478. .calendar-scroll::-webkit-scrollbar {
  479. width: 8px;
  480. }
  481. .calendar-scroll::-webkit-scrollbar-track {
  482. background: transparent;
  483. }
  484. .calendar-scroll::-webkit-scrollbar-thumb {
  485. background-color: color-mix(in srgb, var(--text-muted) 60%, transparent);
  486. border-radius: 999px;
  487. border: 2px solid color-mix(in srgb, var(--bg-secondary) 70%, transparent);
  488. }
  489. .calendar-scroll::-webkit-scrollbar-thumb:hover {
  490. background-color: color-mix(in srgb, var(--text-muted) 80%, transparent);
  491. }
  492. /* History row thumbnail hover-to-enlarge — desktop only (no hover on touch). */
  493. @media (hover: hover) and (pointer: fine) {
  494. .history-thumb-hover:hover .history-thumb-preview {
  495. opacity: 1;
  496. }
  497. }
  498. /* Per-slot filament colour swatch in the slice modal (#2977). A native
  499. <input type="color"> paints its value inside a fixed inner padding and its
  500. own border, so at 14px the colour reads as a dot inside a pale ring rather
  501. than as the round swatch it replaced. These reset both so the element is
  502. nothing but the colour. */
  503. .slice-colour-swatch {
  504. -webkit-appearance: none;
  505. -moz-appearance: none;
  506. appearance: none;
  507. overflow: hidden;
  508. }
  509. .slice-colour-swatch::-webkit-color-swatch-wrapper {
  510. padding: 0;
  511. }
  512. .slice-colour-swatch::-webkit-color-swatch {
  513. border: none;
  514. border-radius: 999px;
  515. }
  516. .slice-colour-swatch::-moz-color-swatch {
  517. border: none;
  518. border-radius: 999px;
  519. }