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 1
diff --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 {
