collective

Browse / github.com/storytold/artcraft-services / patch

webapp: colors as theme variables (part 1 of 2, no visible change)

webapp: colors as theme variables (part 1 of 2, no visible change) Part 1 of 2 for #165. I split this work in two to make it easier to review. This part only changes how the web app's colors are written, not how anything looks. Part 2 (the Soul Mark theme) builds on it, and I'll open it once this one is in, or right away if you'd rather see both together. ## What it does The web app's colors were written directly into the code. They now read CSS variables whose defaults are exactly the old values, so **every page looks the same, pixel for pixel**. Nothing in this part sets the variables; it's the groundwork a theme needs. - `styles.css` declares the varia

Status
open
Score
0.00
Adopters
0 other operators
Author
Luminous-Enigma (imported from pull request #166, open)
Base
e57ee3d63ac478a24149384bf1473ef97aa067f8
Size
+301 −140 in 19 files
Review
pending
Shared
2026-10-28
Id
62a08236d11acd7b37e2a933

How it went

No other operator has reported using it yet.

Diff

raw · frontend/apps/artcraft-webapp/src/components/sidebar/app-sidebar.tsx frontend/apps/artcraft-webapp/src/components/topbar/TopBarActions.tsx frontend/apps/artcraft-webapp/src/components/topbar/task-queue.tsx frontend/apps/artcraft-webapp/src/components/topbar/topbar.tsx frontend/apps/artcraft-webapp/src/components/ui/sheet.tsx frontend/apps/artcraft-webapp/src/components/ui/tooltip.tsx frontend/apps/artcraft-webapp/src/pages/checkout/cancel.tsx frontend/apps/artcraft-webapp/src/pages/checkout/success.tsx frontend/apps/artcraft-webapp/src/pages/onboarding/onboarding.tsx frontend/apps/artcraft-webapp/src/pages/referrals/referrals.tsx frontend/apps/artcraft-webapp/src/pages/support/support.tsx frontend/apps/artcraft-webapp/src/pages/welcome/welcome.tsx frontend/apps/artcraft-webapp/src/styles.css frontend/apps/artcraft-webapp/tailwind.config.js frontend/libs/components/pagedraw/src/lib/PageDraw.tsx frontend/libs/components/pricing-modal/src/lib/pricing-modal.tsx frontend/libs/components/pricing-table/src/lib/pricing-table.tsx frontend/libs/components/pricing-table/src/lib/website-pricing-table.tsx frontend/libs/login/src/lib/desktop-login-approval.tsx

diff --git a/frontend/apps/artcraft-webapp/src/components/sidebar/app-sidebar.tsx b/frontend/apps/artcraft-webapp/src/components/sidebar/app-sidebar.tsx--- a/frontend/apps/artcraft-webapp/src/components/sidebar/app-sidebar.tsx+++ b/frontend/apps/artcraft-webapp/src/components/sidebar/app-sidebar.tsx@@ -267,7 +267,7 @@ export function AppSidebar() {               <img                 src="/images/artcraft-logo-2.png"                 alt="ArtCraft"-                className="h-5 w-auto shrink-0"+                className="st-wordmark h-5 w-auto shrink-0"               />             </Link>           )}diff --git a/frontend/apps/artcraft-webapp/src/components/topbar/TopBarActions.tsx b/frontend/apps/artcraft-webapp/src/components/topbar/TopBarActions.tsx--- a/frontend/apps/artcraft-webapp/src/components/topbar/TopBarActions.tsx+++ b/frontend/apps/artcraft-webapp/src/components/topbar/TopBarActions.tsx@@ -161,7 +161,7 @@ export function TopBarActions({ className }: { className?: string }) {             >               <MenuItems                 modal={false}-                className="absolute right-0 z-50 mt-2 w-48 origin-top-right rounded-[3px] bg-[#101014] border border-white/15 focus:outline-none overflow-hidden"+                className="absolute right-0 z-50 mt-2 w-48 origin-top-right rounded-[3px] bg-ui-panel border border-white/15 focus:outline-none overflow-hidden"               >                 <div className="px-4 py-3 border-b border-white/15">                   <p className="text-sm font-medium text-white truncate">@@ -303,7 +303,7 @@ function CreditsChip({         </span>       }       buttonClassName="h-8 px-3 ps-2.5 bg-transparent hover:bg-white/10 border border-white/15 hover:border-white/30 shadow-none text-white/80 rounded-[3px] gap-1.5"-      panelClassName="mt-2 bg-[#101014] border border-white/15 text-white rounded-[3px]"+      panelClassName="mt-2 bg-ui-panel border border-white/15 text-white rounded-[3px]"     >       {(close) => (         <div className="w-72 max-w-[calc(100vw-24px)] p-3 text-white">diff --git a/frontend/apps/artcraft-webapp/src/components/topbar/task-queue.tsx b/frontend/apps/artcraft-webapp/src/components/topbar/task-queue.tsx--- a/frontend/apps/artcraft-webapp/src/components/topbar/task-queue.tsx+++ b/frontend/apps/artcraft-webapp/src/components/topbar/task-queue.tsx@@ -1137,7 +1137,7 @@ export const TaskQueue = () => {           <PopoverMenu             mode="default"             buttonClassName="h-8 w-8 !p-0 relative bg-transparent hover:bg-white/10 border border-white/15 hover:border-white/30 text-white/80 rounded-[3px] shadow-none"-            panelClassName="w-[calc(100vw-5rem)] sm:w-[400px] p-2 bg-[#101014] border border-white/15 mt-2 rounded-[3px]"+            panelClassName="w-[calc(100vw-5rem)] sm:w-[400px] p-2 bg-ui-panel border border-white/15 mt-2 rounded-[3px]"             position="bottom"             align="end"             triggerIcon={diff --git a/frontend/apps/artcraft-webapp/src/components/topbar/topbar.tsx b/frontend/apps/artcraft-webapp/src/components/topbar/topbar.tsx--- a/frontend/apps/artcraft-webapp/src/components/topbar/topbar.tsx+++ b/frontend/apps/artcraft-webapp/src/components/topbar/topbar.tsx@@ -39,7 +39,7 @@ export function TopBar() {                     : "/images/artcraft-logo-2.png"                 }                 alt="ArtCraft"-                className={isMobile ? "h-5 w-auto" : "h-5 w-auto"}+                className={isMobile ? "h-5 w-auto" : "st-wordmark h-5 w-auto"}               />             </Link>           )}diff --git a/frontend/apps/artcraft-webapp/src/components/ui/sheet.tsx b/frontend/apps/artcraft-webapp/src/components/ui/sheet.tsx--- a/frontend/apps/artcraft-webapp/src/components/ui/sheet.tsx+++ b/frontend/apps/artcraft-webapp/src/components/ui/sheet.tsx@@ -53,7 +53,7 @@ const SheetContent = React.forwardRef<       <DialogPrimitive.Content         ref={ref}         className={cn(-          "fixed z-50 flex flex-col bg-[#101014] transition ease-in-out",+          "fixed z-50 flex flex-col bg-ui-panel transition ease-in-out",           "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-200 data-[state=open]:duration-300",           side === "left" &&             "inset-y-0 left-0 h-full border-r border-white/15 data-[state=open]:slide-in-from-left data-[state=closed]:slide-out-to-left",diff --git a/frontend/apps/artcraft-webapp/src/components/ui/tooltip.tsx b/frontend/apps/artcraft-webapp/src/components/ui/tooltip.tsx--- a/frontend/apps/artcraft-webapp/src/components/ui/tooltip.tsx+++ b/frontend/apps/artcraft-webapp/src/components/ui/tooltip.tsx@@ -15,7 +15,7 @@ const TooltipContent = React.forwardRef<       ref={ref}       sideOffset={sideOffset}       className={cn(-        "z-[60] overflow-hidden rounded-[3px] bg-[#101014] border border-white/15 px-2.5 py-1.5 text-xs text-white",+        "z-[60] overflow-hidden rounded-[3px] bg-ui-panel border border-white/15 px-2.5 py-1.5 text-xs text-white",         "animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",         className,       )}diff --git a/frontend/apps/artcraft-webapp/src/pages/checkout/cancel.tsx b/frontend/apps/artcraft-webapp/src/pages/checkout/cancel.tsx--- a/frontend/apps/artcraft-webapp/src/pages/checkout/cancel.tsx+++ b/frontend/apps/artcraft-webapp/src/pages/checkout/cancel.tsx@@ -16,7 +16,7 @@ const CheckoutCancel = () => {       <main className="relative z-10 pt-20 pb-20 px-4 sm:px-6 lg:px-8 flex flex-col items-center justify-center min-h-[calc(100vh-200px)]">         {/* Cancel Card */}         <div className="max-w-lg w-full">-          <div className="bg-[#101014] border border-white/15 p-8 md:p-12 text-center">+          <div className="bg-ui-panel border border-white/15 p-8 md:p-12 text-center">             {/* Cancel Icon */}             <div className="mb-6">               <div className="w-20 h-20 mx-auto border border-white/15 bg-white/10 flex items-center justify-center">diff --git a/frontend/apps/artcraft-webapp/src/pages/checkout/success.tsx b/frontend/apps/artcraft-webapp/src/pages/checkout/success.tsx--- a/frontend/apps/artcraft-webapp/src/pages/checkout/success.tsx+++ b/frontend/apps/artcraft-webapp/src/pages/checkout/success.tsx@@ -85,7 +85,7 @@ const CheckoutSuccess = () => {       <main className="relative z-10 pt-12 pb-20 px-4 sm:px-6 lg:px-8 flex flex-col items-center justify-center min-h-[calc(100vh-200px)]">         {/* Success Card */}         <div className="max-w-4xl w-full">-          <div className="bg-[#101014] border border-white/15 p-8 md:p-12 text-center">+          <div className="bg-ui-panel border border-white/15 p-8 md:p-12 text-center">             {/* Success Icon */}             <div className="mb-6">               <div className="w-20 h-20 mx-auto border border-white/15 bg-green-500/10 flex items-center justify-center">diff --git a/frontend/apps/artcraft-webapp/src/pages/onboarding/onboarding.tsx b/frontend/apps/artcraft-webapp/src/pages/onboarding/onboarding.tsx--- a/frontend/apps/artcraft-webapp/src/pages/onboarding/onboarding.tsx+++ b/frontend/apps/artcraft-webapp/src/pages/onboarding/onboarding.tsx@@ -338,7 +338,7 @@ const Onboarding = () => {             </div>           )} -          <div className="bg-[#101014] border border-white/15 p-6 py-8">+          <div className="bg-ui-panel border border-white/15 p-6 py-8">             <div className="text-center mb-8">               <h1 className="text-2xl font-semibold mb-2">                 {currentStep === "password" &&diff --git a/frontend/apps/artcraft-webapp/src/pages/referrals/referrals.tsx b/frontend/apps/artcraft-webapp/src/pages/referrals/referrals.tsx--- a/frontend/apps/artcraft-webapp/src/pages/referrals/referrals.tsx+++ b/frontend/apps/artcraft-webapp/src/pages/referrals/referrals.tsx@@ -163,7 +163,7 @@ export default function Referrals() {             <div className="h-px w-10 bg-white/20" />           </div> -          <section className="bg-[#101014] border border-white/15 p-6 sm:p-8">+          <section className="bg-ui-panel border border-white/15 p-6 sm:p-8">             <div className="flex items-start justify-between gap-4 mb-5">               <div>                 <div className="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white/60 mb-2">@@ -266,7 +266,7 @@ export default function Referrals() {  function ProfileLinkPanel({ profileLink }: { profileLink: string }) {   return (-    <section className="bg-[#101014] border border-white/15 p-6 sm:p-8">+    <section className="bg-ui-panel border border-white/15 p-6 sm:p-8">       <div className="mb-4">         <div className="font-mono text-[11px] font-semibold uppercase tracking-[0.12em] text-white/60 mb-2">           Option 1diff --git a/frontend/apps/artcraft-webapp/src/pages/support/support.tsx b/frontend/apps/artcraft-webapp/src/pages/support/support.tsx--- a/frontend/apps/artcraft-webapp/src/pages/support/support.tsx+++ b/frontend/apps/artcraft-webapp/src/pages/support/support.tsx@@ -58,7 +58,7 @@ const Support = () => {           </p>         </header> -        <ul className="divide-y divide-white/15 border border-white/15 overflow-hidden bg-[#101014]">+        <ul className="divide-y divide-white/15 border border-white/15 overflow-hidden bg-ui-panel">           {ROWS.map((row) => (             <li key={row.title}>               <SupportLink row={row} />diff --git a/frontend/apps/artcraft-webapp/src/pages/welcome/welcome.tsx b/frontend/apps/artcraft-webapp/src/pages/welcome/welcome.tsx--- a/frontend/apps/artcraft-webapp/src/pages/welcome/welcome.tsx+++ b/frontend/apps/artcraft-webapp/src/pages/welcome/welcome.tsx@@ -57,7 +57,7 @@ const Welcome = () => {          {/* Create-in-browser CTA — desktop download is optional */}         <div className="max-w-3xl mx-auto mb-8">-          <div className="bg-[#101014] border border-white/15 p-8 md:p-10">+          <div className="bg-ui-panel border border-white/15 p-8 md:p-10">             <div className="flex items-center gap-3 mb-2">               <div className="w-10 h-10 bg-white/10 flex items-center justify-center">                 <WandSparklesIcon className="text-white/70 text-lg" />@@ -101,7 +101,7 @@ const Welcome = () => {          {/* Desktop app alternative */}         <div className="max-w-3xl mx-auto">-          <div className="bg-[#101014] border border-white/15 p-8 md:p-10">+          <div className="bg-ui-panel border border-white/15 p-8 md:p-10">             <div className="flex items-center gap-3 mb-2">               <div className="w-10 h-10 bg-white/10 flex items-center justify-center">                 <MonitorIcon className="text-white/70 text-lg" />diff --git a/frontend/apps/artcraft-webapp/src/styles.css b/frontend/apps/artcraft-webapp/src/styles.css--- a/frontend/apps/artcraft-webapp/src/styles.css+++ b/frontend/apps/artcraft-webapp/src/styles.css@@ -4,16 +4,44 @@  @layer base {   :root {+    /* Theme tokens, as "R G B" so Tailwind can add alpha. These defaults are+       the app's own colours; a theme overrides them on <html>.+       "white" is the ink that hairlines and most text are drawn in, "black"+       the deepest surface; a theme swaps them as a pair.+       The desktop app's own names (--st-bg, --st-fg, --st-fg-rgb,+       --st-divider, --st-controls-rgb) are read by shared libs with+       fallbacks of their own, so they are left unset here: setting them+       would change those libs' look. A theme sets them as well. */+    --st-white-rgb: 255 255 255;+    --st-black-rgb: 0 0 0;+    --st-text-rgb: 242 241 238;+    --st-bg-rgb: 18 19 22;+    --st-panel-rgb: 16 16 20;+    --st-control-rgb: 30 31 34;+    --st-sunken-rgb: 6 6 7;+    --st-scroll-track-rgb: 11 11 12;+    --st-scroll-thumb-rgb: 44 44 47;+    --st-accent-rgb: 45 129 255;+    --st-accent-300-rgb: 142 199 255;+    --st-accent-400-rgb: 89 167 255;+    --st-accent-600-rgb: 27 99 245;+    --st-accent-700-rgb: 20 78 225;+    --st-accent-ink-rgb: 116 170 255;+    --st-accent-text-rgb: 45 129 255;+    --st-accent-line-rgb: 45 129 255;+    --st-text-fade: 1;+    --st-icon-filter: invert(1);+     /* Sidebar tokens (consumed by tailwind colors in tailwind.config.js).        Brutalist system: near-black surfaces, white/15 hairlines, white        accent for selection/focus. */-    color-scheme: dark;-    --sidebar-bg: #1e1f22;-    --sidebar-fg: rgba(255, 255, 255, 0.72);-    --sidebar-border: rgba(255, 255, 255, 0.15);-    --sidebar-accent: rgba(255, 255, 255, 0.08);-    --sidebar-accent-fg: #ffffff;-    --sidebar-ring: #74aaff;+    color-scheme: var(--st-color-scheme, dark);+    --sidebar-bg: rgb(var(--st-control-rgb));+    --sidebar-fg: rgb(var(--st-white-rgb) / 0.72);+    --sidebar-border: rgb(var(--st-white-rgb) / 0.15);+    --sidebar-accent: rgb(var(--st-white-rgb) / 0.08);+    --sidebar-accent-fg: rgb(var(--st-white-rgb));+    --sidebar-ring: rgb(var(--st-accent-ink-rgb));      /* shadcn design tokens consumed by @storyteller/ui-video-editor.        Dark-theme palette (the editor only runs in dark mode today).@@ -91,7 +119,7 @@  * {   scrollbar-width: thin;-  scrollbar-color: #2c2c2f #0b0b0c;+  scrollbar-color: rgb(var(--st-scroll-thumb-rgb)) rgb(var(--st-scroll-track-rgb)); }  *::-webkit-scrollbar {@@ -99,12 +127,12 @@ }  *::-webkit-scrollbar-track {-  background: #0b0b0c;+  background: rgb(var(--st-scroll-track-rgb)); }  *::-webkit-scrollbar-thumb {-  background-color: #2c2c2f;-  border: 2px solid #0b0b0c;+  background-color: rgb(var(--st-scroll-thumb-rgb));+  border: 2px solid rgb(var(--st-scroll-track-rgb)); }  .scrollbar-hidden {@@ -158,19 +186,19 @@       "Helvetica Neue",       Arial,       sans-serif;-    color: #f2f1ee;-    background-color: #121316;+    color: rgb(var(--st-text-rgb));+    background-color: rgb(var(--st-bg-rgb));     -webkit-font-smoothing: antialiased;     -moz-osx-font-smoothing: grayscale;   }    ::selection {-    background: #2d81ff;-    color: #ffffff;+    background: rgb(var(--st-accent-rgb));+    color: rgb(var(--st-white-rgb));   }    :focus-visible {-    outline: 2px solid #74aaff;+    outline: 2px solid rgb(var(--st-accent-ink-rgb));     outline-offset: 3px;   } @@ -203,18 +231,23 @@  /* Shared component utilities (matching desktop app theme) */ .bg-ui-badge {-  background-color: rgba(0, 0, 0, 0.4);+  background-color: rgb(var(--st-black-rgb) / 0.4);+}++/* The ArtCraft wordmark image, which is white; a light theme flips it. */+.st-wordmark {+  filter: var(--st-wordmark-filter, none); }  .icon-auto-contrast {-  filter: invert(1);+  filter: var(--st-icon-filter); }  /* Panel surface used by pagescene + lib components. Brutalist system:    flat near-black, no gradient (class name kept for lib compatibility). */ .bg-ui-panel-gradient {-  background: #101014;-  background-color: #101014;+  background: rgb(var(--st-panel-rgb));+  background-color: rgb(var(--st-panel-rgb)); }  .prevent-select {@@ -231,57 +264,57 @@   width: 10px;   height: 10px;   border-radius: 5px;-  background-color: #ffffff30;-  color: #ffffff30;+  background-color: rgb(var(--st-white-rgb) / 0.188);+  color: rgb(var(--st-white-rgb) / 0.188);   box-shadow:-    9984px 0 0 0 #ffffff30,-    9999px 0 0 0 #ffffff30,-    10014px 0 0 0 #ffffff30;+    9984px 0 0 0 rgb(var(--st-white-rgb) / 0.188),+    9999px 0 0 0 rgb(var(--st-white-rgb) / 0.188),+    10014px 0 0 0 rgb(var(--st-white-rgb) / 0.188);   animation: dot-typing 1.5s infinite linear; }  @keyframes dot-typing {   0% {     box-shadow:-      9984px 0 0 0 #ffffff30,-      9999px 0 0 0 #ffffff30,-      10014px 0 0 0 #ffffff30;+      9984px 0 0 0 rgb(var(--st-white-rgb) / 0.188),+      9999px 0 0 0 rgb(var(--st-white-rgb) / 0.188),+      10014px 0 0 0 rgb(var(--st-white-rgb) / 0.188);   }   16.667% {     box-shadow:-      9984px -10px 0 0 #ffffff30,-      9999px 0 0 0 #ffffff30,-      10014px 0 0 0 #ffffff30;+      9984px -10px 0 0 rgb(var(--st-white-rgb) / 0.188),+      9999px 0 0 0 rgb(var(--st-white-rgb) / 0.188),+      10014px 0 0 0 rgb(var(--st-white-rgb) / 0.188);   }   33.333% {     box-shadow:-      9984px 0 0 0 #ffffff30,-      9999px 0 0 0 #ffffff30,-      10014px 0 0 0 #ffffff30;+      9984px 0 0 0 rgb(var(--st-white-rgb) / 0.188),+      9999px 0 0 0 rgb(var(--st-white-rgb) / 0.188),+      10014px 0 0 0 rgb(var(--st-white-rgb) / 0.188);   }   50% {     box-shadow:-      9984px 0 0 0 #ffffff30,-      9999px -10px 0 0 #ffffff30,-      10014px 0 0 0 #ffffff30;+      9984px 0 0 0 rgb(var(--st-white-rgb) / 0.188),+      9999px -10px 0 0 rgb(var(--st-white-rgb) / 0.188),+      10014px 0 0 0 rgb(var(--st-white-rgb) / 0.188);   }   66.667% {     box-shadow:-      9984px 0 0 0 #ffffff30,-      9999px 0 0 0 #ffffff30,-      10014px 0 0 0 #ffffff30;+      9984px 0 0 0 rgb(var(--st-white-rgb) / 0.188),+      9999px 0 0 0 rgb(var(--st-white-rgb) / 0.188),+      10014px 0 0 0 rgb(var(--st-white-rgb) / 0.188);   }   83.333% {     box-shadow:-      9984px 0 0 0 #ffffff30,-      9999px 0 0 0 #ffffff30,-      10014px -10px 0 0 #ffffff30;+      9984px 0 0 0 rgb(var(--st-white-rgb) / 0.188),+      9999px 0 0 0 rgb(var(--st-white-rgb) / 0.188),+      10014px -10px 0 0 rgb(var(--st-white-rgb) / 0.188);   }   100% {     box-shadow:-      9984px 0 0 0 #ffffff30,-      9999px 0 0 0 #ffffff30,-      10014px 0 0 0 #ffffff30;+      9984px 0 0 0 rgb(var(--st-white-rgb) / 0.188),+      9999px 0 0 0 rgb(var(--st-white-rgb) / 0.188),+      10014px 0 0 0 rgb(var(--st-white-rgb) / 0.188);   } } @@ -290,19 +323,19 @@ @layer components {   .glass {     @apply relative border border-white/15 transition-colors duration-300 rounded-[3px] backdrop-blur-sm;-    @apply bg-[#1E1F22]/95 !important;+    @apply bg-ui-controls/95 !important;   } }  .liquid-glass {-  background: rgba(11, 11, 12, 0.92);-  border: 1px solid rgba(255, 255, 255, 0.15);+  background: rgb(var(--st-scroll-track-rgb) / 0.92);+  border: 1px solid rgb(var(--st-white-rgb) / 0.15); }  /* Minimal thin scrollbar for prompt textareas */ .promptbox-scrollbar {   scrollbar-width: thin;-  scrollbar-color: rgba(255, 255, 255, 0.2) transparent;+  scrollbar-color: rgb(var(--st-white-rgb) / 0.2) transparent; }  .promptbox-scrollbar::-webkit-scrollbar {@@ -314,11 +347,11 @@ }  .promptbox-scrollbar::-webkit-scrollbar-thumb {-  background-color: rgba(255, 255, 255, 0.2);+  background-color: rgb(var(--st-white-rgb) / 0.2); }  .promptbox-scrollbar::-webkit-scrollbar-thumb:hover {-  background-color: rgba(255, 255, 255, 0.35);+  background-color: rgb(var(--st-white-rgb) / 0.35); }  /* Hide the native resize grip (Chromium draws different grips for textareas@@ -344,16 +377,16 @@   background: linear-gradient(     135deg,     transparent 40%,-    rgba(255, 255, 255, 0.5) 40%,-    rgba(255, 255, 255, 0.5) 45%,+    rgb(var(--st-white-rgb) / 0.5) 40%,+    rgb(var(--st-white-rgb) / 0.5) 45%,     transparent 45%,     transparent 55%,-    rgba(255, 255, 255, 0.5) 55%,-    rgba(255, 255, 255, 0.5) 60%,+    rgb(var(--st-white-rgb) / 0.5) 55%,+    rgb(var(--st-white-rgb) / 0.5) 60%,     transparent 60%,     transparent 70%,-    rgba(255, 255, 255, 0.5) 70%,-    rgba(255, 255, 255, 0.5) 75%,+    rgb(var(--st-white-rgb) / 0.5) 70%,+    rgb(var(--st-white-rgb) / 0.5) 75%,     transparent 75%   ); }@@ -448,7 +481,7 @@   background: linear-gradient(     90deg,     transparent 0%,-    rgba(255, 255, 255, 0.08) 50%,+    rgb(var(--st-white-rgb) / 0.08) 50%,     transparent 100%   );   background-size: 200% 100%;@@ -456,8 +489,8 @@ }  .bg-dots {-  background-image: radial-gradient(#ffffff20 1px, transparent 0);-  background-color: #101014;+  background-image: radial-gradient(rgb(var(--st-white-rgb) / 0.125) 1px, transparent 0);+  background-color: rgb(var(--st-panel-rgb));   background-size: 33px 33px;   @apply bg-center; }@@ -535,15 +568,15 @@   margin: -5px;   pointer-events: none;   background:-    linear-gradient(rgba(242, 241, 238, 0.4), rgba(242, 241, 238, 0.4)) center / 1px 11px no-repeat,-    linear-gradient(rgba(242, 241, 238, 0.4), rgba(242, 241, 238, 0.4)) center / 11px 1px no-repeat;+    linear-gradient(rgb(var(--st-text-rgb) / 0.4), rgb(var(--st-text-rgb) / 0.4)) center / 1px 11px no-repeat,+    linear-gradient(rgb(var(--st-text-rgb) / 0.4), rgb(var(--st-text-rgb) / 0.4)) center / 11px 1px no-repeat; }  /* Contained section borders match the landing without decorative extensions. */ .create-empty-section {   margin-inline: 12px;-  border: 1px solid rgba(242, 241, 238, 0.2);-  background: rgba(242, 241, 238, 0.015);+  border: 1px solid rgb(var(--st-text-rgb) / 0.2);+  background: rgb(var(--st-text-rgb) / 0.015); }  @media (min-width: 640px) {diff --git a/frontend/apps/artcraft-webapp/tailwind.config.js b/frontend/apps/artcraft-webapp/tailwind.config.js--- a/frontend/apps/artcraft-webapp/tailwind.config.js+++ b/frontend/apps/artcraft-webapp/tailwind.config.js@@ -2,6 +2,129 @@ const { createGlobPatternsForDependencies } = require("@nx/react/tailwind"); const { join } = require("path"); import colors from "tailwindcss/colors"; +// The brand blue. Themeable: each shade a theme may change reads its own+// --st-accent-* variable (defaults in src/styles.css are the values below).+// Fills use DEFAULT; text, rings and lines get their own variables further+// down, so a theme can give each the shade that reads in its place.+const primaryScale = {+  DEFAULT: "rgb(var(--st-accent-rgb) / <alpha-value>)",+  50: "#eef6ff",+  100: "#d9ebff",+  200: "#bcdcff",+  300: "rgb(var(--st-accent-300-rgb) / <alpha-value>)",+  400: "rgb(var(--st-accent-400-rgb) / <alpha-value>)",+  500: "rgb(var(--st-accent-rgb) / <alpha-value>)",+  600: "rgb(var(--st-accent-600-rgb) / <alpha-value>)",+  700: "rgb(var(--st-accent-700-rgb) / <alpha-value>)",+  800: "#173fb6",+  900: "#19398f",+  950: "#142457",+};++// Faded text (text-white/40 ...) draws at 1 - (1 - alpha) * --st-text-fade.+// The fade is 1 by default, which is exactly the declared alpha; a light+// theme lowers it so its captions stay readable.+const fadedText = (rgbVar) =>+  `rgb(var(${rgbVar}) / calc(1 - (1 - <alpha-value>) * var(--st-text-fade, 1)))`;+// Faded elements (opacity-40 ... opacity-90, mostly captions) follow the same+// fade. 0, 100 and the decorative 5-30 steps stay exact: they show, hide and+// animate things.+const fadedOpacity = Object.fromEntries(+  [40, 50, 60, 70, 75, 80, 90].map((n) => [+    n,+    `calc(1 - (1 - ${n / 100}) * var(--st-text-fade, 1))`,+  ]),+);+// Text in the palette's status colours (text-red-400 errors, amber BETA+// badges ...) reads --st-<family>-<shade>-rgb, which only a theme sets; until+// then the fallback is Tailwind's own colour.+const MEANING_FAMILIES = [+  "rose",+  "indigo",+  "red",+  "blue",+  "orange",+  "amber",+  "green",+  "emerald",+  "teal",+  "cyan",+  "yellow",+  "pink",+  "purple",+];+const MEANING_SHADES = [200, 300, 400, 500, 600];+const COLOR_DEFAULTS = { red: "#D33242", blue: "#2E70FF", green: "#308752" };+const triplet = (hex) =>+  [1, 3, 5].map((i) => Number.parseInt(hex.slice(i, i + 2), 16)).join(" ");+const meaningText = Object.fromEntries(+  MEANING_FAMILIES.map((family) => [+    family,+    {+      ...colors[family],+      ...(COLOR_DEFAULTS[family] ? { DEFAULT: COLOR_DEFAULTS[family] } : {}),+      ...Object.fromEntries(+        MEANING_SHADES.map((shade) => [+          shade,+          `rgb(var(--st-${family}-${shade}-rgb, ${triplet(colors[family][shade])}) / <alpha-value>)`,+        ]),+      ),+    },+  ]),+);+// .st-keep-dark: a branded dark surface (the pricing cards) that keeps+// ArtCraft's own colours whatever the theme. Inside it every theme variable+// is reset to its default; when a theme is on it also gets the default page+// colour beneath it, since its see-through gradients were drawn for a dark page.+const KEEP_DARK = {+  "--st-white-rgb": "255 255 255",+  "--st-black-rgb": "0 0 0",+  "--st-text-rgb": "242 241 238",+  "--st-bg-rgb": "18 19 22",+  "--st-panel-rgb": "16 16 20",+  "--st-control-rgb": "30 31 34",+  "--st-sunken-rgb": "6 6 7",+  "--st-accent-rgb": "45 129 255",+  "--st-accent-300-rgb": "142 199 255",+  "--st-accent-400-rgb": "89 167 255",+  "--st-accent-600-rgb": "27 99 245",+  "--st-accent-700-rgb": "20 78 225",+  "--st-accent-ink-rgb": "116 170 255",+  "--st-accent-text-rgb": "45 129 255",+  "--st-accent-line-rgb": "45 129 255",+  "--st-text-fade": "1",+  // The desktop app's names, which shared libs read with their own fallbacks.+  "--st-bg": "initial",+  "--st-fg": "initial",+  "--st-fg-rgb": "initial",+  "--st-divider": "initial",+  "--st-controls-rgb": "initial",+  "--st-icon-filter": "invert(1)",+  "--st-wordmark-filter": "none",+  ...Object.fromEntries(+    MEANING_FAMILIES.flatMap((family) =>+      MEANING_SHADES.map((shade) => [`--st-${family}-${shade}-rgb`, "initial"]),+    ),+  ),+};+const keepDark = ({ addComponents }) =>+  addComponents({+    ".st-keep-dark": KEEP_DARK,+    // Text that sets no colour of its own inherits the page's, which was+    // worked out before the reset above, so the island sets it again.+    // :where() keeps this at zero specificity, so a card's own background+    // and text colour (bg-[#1b1b20], .website-plan-card) still win.+    ':where(:root[data-theme]:not([data-theme="default"]) .st-keep-dark)': {+      backgroundColor: "rgb(18 19 22)",+      color: "rgb(242 241 238)",+      colorScheme: "dark",+    },+  });+const accentFor = (rgbVar) => ({+  ...primaryScale,+  DEFAULT: `rgb(var(${rgbVar}) / <alpha-value>)`,+});+ /** @type {import('tailwindcss').Config} */ module.exports = {   content: [@@ -18,6 +141,34 @@ module.exports = {   ],   theme: {     extend: {+      opacity: fadedOpacity,+      textColor: {+        ...meaningText,+        white: fadedText("--st-white-rgb"),+        "base-fg": fadedText("--st-text-rgb"),+        primary: accentFor("--st-accent-text-rgb"),+        "brand-primary": accentFor("--st-accent-text-rgb"),+      },+      ringColor: {+        primary: accentFor("--st-accent-line-rgb"),+        "brand-primary": accentFor("--st-accent-line-rgb"),+      },+      borderColor: {+        primary: accentFor("--st-accent-line-rgb"),+        "brand-primary": accentFor("--st-accent-line-rgb"),+      },+      outlineColor: {+        primary: accentFor("--st-accent-line-rgb"),+        "brand-primary": accentFor("--st-accent-line-rgb"),+      },+      fill: {+        primary: accentFor("--st-accent-line-rgb"),+        "brand-primary": accentFor("--st-accent-line-rgb"),+      },+      stroke: {+        primary: accentFor("--st-accent-line-rgb"),+        "brand-primary": accentFor("--st-accent-line-rgb"),+      },       fontFamily: {         mono: [           "Geist Mono",@@ -53,9 +204,12 @@ module.exports = {     colors: {       transparent: "transparent",       current: "currentColor",-      white: colors.white,+      // Themeable: "white" is the ink every surface is drawn+      // against and "black" the deepest surface, so a theme swaps them as a+      // pair. The default values are exactly Tailwind's white and black.+      white: "rgb(var(--st-white-rgb) / <alpha-value>)",       gray: colors.gray,-      black: colors.black,+      black: "rgb(var(--st-black-rgb) / <alpha-value>)",       rose: colors.rose,       indigo: colors.indigo,       red: { ...colors.red, DEFAULT: "#D33242" },@@ -69,20 +223,7 @@ module.exports = {       yellow: colors.yellow,       pink: colors.pink,       purple: colors.purple,-      primary: {-        DEFAULT: "#2d81ff",-        50: "#eef6ff",-        100: "#d9ebff",-        200: "#bcdcff",-        300: "#8ec7ff",-        400: "#59a7ff",-        500: "#2d81ff",-        600: "#1b63f5",-        700: "#144ee1",-        800: "#173fb6",-        900: "#19398f",-        950: "#142457",-      },+      primary: primaryScale,       danger: {         // copper rust — aligned with artcraft app's tailwind.config.ts         DEFAULT: "#8f4951",@@ -98,20 +239,7 @@ module.exports = {         900: "#64373f",         950: "#361b1e",       },-      "brand-primary": {-        DEFAULT: "#2d81ff",-        50: "#eef6ff",-        100: "#d9ebff",-        200: "#bcdcff",-        300: "#8ec7ff",-        400: "#59a7ff",-        500: "#2d81ff",-        600: "#1b63f5",-        700: "#144ee1",-        800: "#173fb6",-        900: "#19398f",-        950: "#142457",-      },+      "brand-primary": primaryScale,       "brand-secondary": {         DEFAULT: "#3E3E41",         50: "#f6f6f6",@@ -168,7 +296,7 @@ module.exports = {         900: "#3d3d3d",         950: "#3E3E41",       },-      "base-fg": "rgb(242 241 238 / <alpha-value>)",+      "base-fg": "rgb(var(--st-text-rgb) / <alpha-value>)",       sidebar: {         DEFAULT: "var(--sidebar-bg)",         foreground: "var(--sidebar-fg)",@@ -180,19 +308,19 @@ module.exports = {       ui: {         // Brutalist system: near-black surfaces with white/15 hairline         // borders, aligned with artcraft-website + artcraft-website-next.-        background: "#121316",-        panel: "#101014",-        "panel-border": "rgb(255 255 255 / 0.15)",-        border: "rgb(255 255 255 / 0.15)",+        background: "rgb(var(--st-bg-rgb) / <alpha-value>)",+        panel: "rgb(var(--st-panel-rgb) / <alpha-value>)",+        "panel-border": "rgb(var(--st-white-rgb) / 0.15)",+        border: "rgb(var(--st-white-rgb) / 0.15)",         // Raised controls stay distinct from the website-matched panel surface.-        controls: "#1e1f22",-        "controls-button": "#1e1f22",-        "controls-border": "rgb(255 255 255 / 0.15)",-        divider: "rgb(255 255 255 / 0.15)",-        modal: "#101014",-        ink: "#f2f1ee",-        "accent-ink": "#74aaff",-        sunken: "#060607",+        controls: "rgb(var(--st-control-rgb) / <alpha-value>)",+        "controls-button": "rgb(var(--st-control-rgb) / <alpha-value>)",+        "controls-border": "rgb(var(--st-white-rgb) / 0.15)",+        divider: "rgb(var(--st-white-rgb) / 0.15)",+        modal: "rgb(var(--st-panel-rgb) / <alpha-value>)",+        ink: "rgb(var(--st-text-rgb) / <alpha-value>)",+        "accent-ink": "rgb(var(--st-accent-ink-rgb) / <alpha-value>)",+        sunken: "rgb(var(--st-sunken-rgb) / <alpha-value>)",       },       "axis-x": "#D33242",       "axis-y": "#308752",@@ -227,5 +355,5 @@ module.exports = {       ring: "hsl(var(--ring))",     },   },-  plugins: [],+  plugins: [keepDark], };diff --git a/frontend/libs/components/pagedraw/src/lib/PageDraw.tsx b/frontend/libs/components/pagedraw/src/lib/PageDraw.tsx--- a/frontend/libs/components/pagedraw/src/lib/PageDraw.tsx+++ b/frontend/libs/components/pagedraw/src/lib/PageDraw.tsx@@ -268,7 +268,7 @@ interface PageDrawProps {    * Tailwind class for the full-viewport canvas backdrop (and the base-image    * selector surface). Defaults to the app's `bg-ui-background`; hosts that    * embed PageDraw under their own page chrome can pass a matching color (e.g.-   * the webapp's `bg-[#101014]`).+   * the webapp's `bg-ui-panel`).    */   backgroundClassName?: string;   /**diff --git a/frontend/libs/components/pricing-modal/src/lib/pricing-modal.tsx b/frontend/libs/components/pricing-modal/src/lib/pricing-modal.tsx--- a/frontend/libs/components/pricing-modal/src/lib/pricing-modal.tsx+++ b/frontend/libs/components/pricing-modal/src/lib/pricing-modal.tsx@@ -194,7 +194,7 @@ export function PricingContent({ title, subtitle }: PricingContentProps) {               className={twMerge(                 getColorSchemeClasses(plan.colorScheme),                 frame-                  ? `relative z-10 h-full w-full border-2 bg-[#101014] shadow-2xl ${frame.border}`+                  ? `relative z-10 h-full w-full border-2 bg-ui-panel shadow-2xl ${frame.border}`                   : "",                 isCurrent ? "ring-2 ring-white/50" : "",               )}@@ -323,7 +323,7 @@ const getColorSchemeClasses = (   colorScheme: SubscriptionPlanDetails["colorScheme"], ) => {   const base =-    "relative flex flex-col rounded-3xl border p-6 transition-all duration-300 backdrop-blur-md md:p-8";+    "st-keep-dark relative flex flex-col rounded-3xl border p-6 transition-all duration-300 backdrop-blur-md md:p-8";    switch (colorScheme) {     case "green":@@ -430,7 +430,7 @@ export function PricingModal({}: PricingModalProps = {}) {     <Modal       isOpen={isOpen}       onClose={closeModal}-      className="flex max-h-[90vh] max-w-screen-2xl flex-col overflow-y-auto rounded-2xl border border-white/5 bg-[#101014]"+      className="flex max-h-[90vh] max-w-screen-2xl flex-col overflow-y-auto rounded-2xl border border-white/5 bg-ui-panel"       allowBackgroundInteraction={false}       showClose={true}       closeOnOutsideClick={true}diff --git a/frontend/libs/components/pricing-table/src/lib/pricing-table.tsx b/frontend/libs/components/pricing-table/src/lib/pricing-table.tsx--- a/frontend/libs/components/pricing-table/src/lib/pricing-table.tsx+++ b/frontend/libs/components/pricing-table/src/lib/pricing-table.tsx@@ -141,8 +141,8 @@ const PricingTable = ({     colorScheme: SubscriptionPlanDetails["colorScheme"],   ) => {     const baseClasses = unifiedTheme-      ? "relative p-5 md:p-6 border flex flex-col transition-all duration-300"-      : "relative p-6 md:p-8 border flex flex-col transition-all duration-300 backdrop-blur-md";+      ? "st-keep-dark relative p-5 md:p-6 border flex flex-col transition-all duration-300"+      : "st-keep-dark relative p-6 md:p-8 border flex flex-col transition-all duration-300 backdrop-blur-md";      switch (colorScheme) {       case "dark":@@ -397,7 +397,7 @@ const PricingTable = ({               className={twMerge(                 getColorSchemeClasses(plan.colorScheme),                 frame-                  ? `relative z-10 border-2 h-full w-full shadow-2xl bg-[#101014] ${frame.border}`+                  ? `relative z-10 border-2 h-full w-full shadow-2xl bg-ui-panel ${frame.border}`                   : "",                 isCurrent ? "ring-2 ring-white/50" : "",               )}@@ -528,8 +528,8 @@ const PricingTable = ({           <div             className={               unifiedTheme-                ? "relative p-5 md:p-6 border flex flex-col transition-all duration-300 bg-gradient-to-b from-[#0d1f4a]/90 via-[#183878]/60 to-[#2456b8]/15 border-[#3568c9]/40 hover:border-[#3568c9] hover:shadow-[0_0_30px_rgba(53,104,201,0.25)]"-                : "relative p-6 md:p-8 border flex flex-col transition-all duration-300 backdrop-blur-md bg-gradient-to-b from-[#0d1f4a]/90 via-[#183878]/60 to-[#2456b8]/15 border-[#3568c9]/40 hover:border-[#3568c9] hover:shadow-[0_0_30px_rgba(53,104,201,0.25)]"+                ? "st-keep-dark relative p-5 md:p-6 border flex flex-col transition-all duration-300 bg-gradient-to-b from-[#0d1f4a]/90 via-[#183878]/60 to-[#2456b8]/15 border-[#3568c9]/40 hover:border-[#3568c9] hover:shadow-[0_0_30px_rgba(53,104,201,0.25)]"+                : "st-keep-dark relative p-6 md:p-8 border flex flex-col transition-all duration-300 backdrop-blur-md bg-gradient-to-b from-[#0d1f4a]/90 via-[#183878]/60 to-[#2456b8]/15 border-[#3568c9]/40 hover:border-[#3568c9] hover:shadow-[0_0_30px_rgba(53,104,201,0.25)]"             }           >             <h3 className="text-xl md:text-2xl font-semibold mb-2 text-white">@@ -564,8 +564,8 @@ const PricingTable = ({         <div           className={             unifiedTheme-              ? "mt-4 relative p-6 border flex flex-col md:flex-row md:items-center gap-4 md:gap-8 transition-all duration-300 bg-gradient-to-b from-[#0d1f4a]/90 via-[#183878]/60 to-[#2456b8]/15 border-[#3568c9]/40 hover:border-[#3568c9] hover:shadow-[0_0_30px_rgba(53,104,201,0.25)]"-              : "mt-4 relative p-6 border flex flex-col md:flex-row md:items-center gap-4 md:gap-8 transition-all duration-300 backdrop-blur-md bg-gradient-to-b from-[#0d1f4a]/90 via-[#183878]/60 to-[#2456b8]/15 border-[#3568c9]/40 hover:border-[#3568c9] hover:shadow-[0_0_30px_rgba(53,104,201,0.25)]"+              ? "st-keep-dark mt-4 relative p-6 border flex flex-col md:flex-row md:items-center gap-4 md:gap-8 transition-all duration-300 bg-gradient-to-b from-[#0d1f4a]/90 via-[#183878]/60 to-[#2456b8]/15 border-[#3568c9]/40 hover:border-[#3568c9] hover:shadow-[0_0_30px_rgba(53,104,201,0.25)]"+              : "st-keep-dark mt-4 relative p-6 border flex flex-col md:flex-row md:items-center gap-4 md:gap-8 transition-all duration-300 backdrop-blur-md bg-gradient-to-b from-[#0d1f4a]/90 via-[#183878]/60 to-[#2456b8]/15 border-[#3568c9]/40 hover:border-[#3568c9] hover:shadow-[0_0_30px_rgba(53,104,201,0.25)]"           }         >           <div className="flex-shrink-0">@@ -718,7 +718,7 @@ const ContactUsButton = ({       {open && (         <div           className={twMerge(-            "absolute top-full z-20 mt-2 overflow-hidden border border-white/15 bg-[#1b1b20] shadow-[0_20px_60px_-15px_rgba(0,0,0,0.7)]",+            "st-keep-dark absolute top-full z-20 mt-2 overflow-hidden border border-white/15 bg-[#1b1b20] shadow-[0_20px_60px_-15px_rgba(0,0,0,0.7)]",             menuClassName,           )}         >diff --git a/frontend/libs/components/pricing-table/src/lib/website-pricing-table.tsx b/frontend/libs/components/pricing-table/src/lib/website-pricing-table.tsx--- a/frontend/libs/components/pricing-table/src/lib/website-pricing-table.tsx+++ b/frontend/libs/components/pricing-table/src/lib/website-pricing-table.tsx@@ -193,7 +193,7 @@ function PlanCell({       data-highlight={         PLAN_STYLE[plan.slug]?.highlight || isCurrent ? "" : undefined       }-      className={`website-plan-card plan-${plan.colorScheme} flex flex-col`}+      className={`st-keep-dark website-plan-card plan-${plan.colorScheme} flex flex-col`}     >       <CellIndexRow index={index}>         {isCurrent ? (@@ -270,7 +270,7 @@ function EnterpriseCell({ index }: { index: string }) {   return (     <article       id="enterprise"-      className="website-plan-card plan-enterprise flex flex-col"+      className="st-keep-dark website-plan-card plan-enterprise flex flex-col"     >       <CellIndexRow index={index}>For teams</CellIndexRow> diff --git a/frontend/libs/login/src/lib/desktop-login-approval.tsx b/frontend/libs/login/src/lib/desktop-login-approval.tsx--- a/frontend/libs/login/src/lib/desktop-login-approval.tsx+++ b/frontend/libs/login/src/lib/desktop-login-approval.tsx@@ -85,7 +85,7 @@ export function DesktopLoginApproval({ loggedIn, authChecked, signIn }: DesktopL   if (!token) return <Status>Open a new login link or scan the QR code from ArtCraft.</Status>;   if (!loggedIn) return <>{signIn}</>; -  return <main className="flex min-h-svh items-center justify-center bg-[#101014] p-6 text-white">+  return <main className="flex min-h-svh items-center justify-center bg-ui-panel p-6 text-white">     <section className="w-full max-w-md border border-white/15 bg-white/5 p-6 sm:p-8">       <Link to="/" className="text-sm text-white/60">ArtCraft</Link>       <h1 className="mb-4 mt-6 text-3xl font-semibold">Log in to ArtCraft Desktop?</h1>@@ -108,7 +108,7 @@ export function DesktopLoginApproval({ loggedIn, authChecked, signIn }: DesktopL }  function Status({ children }: { children: ReactNode }) {-  return <main className="flex min-h-svh items-center justify-center bg-[#101014] p-8 text-white" role="status">{children}</main>;+  return <main className="flex min-h-svh items-center justify-center bg-ui-panel p-8 text-white" role="status">{children}</main>; }  function completionMessage(result: LoginChallengeResult): string | null {