= {};
+ constructor() {
+ this.migrateLegacyTheme();
+ }
+
+ // One-time migration from the old theme model (theme: light|dark|midnight|
+ // custom|system + customThemeBase + customThemePrimary) to the new one
+ // (theme: light|dark|system + lightPrimary + darkPrimary). Runs once and
+ // rewrites storage; a no-op for already-migrated or empty preferences.
+ private migrateLegacyTheme(): void {
+ try {
+ const stored = localStorage.getItem(STORAGE_KEY);
+ if (!stored) return;
+ const p = JSON.parse(stored) as Record;
+ const hadLegacy =
+ "customThemeBase" in p ||
+ "customThemePrimary" in p ||
+ p.theme === "custom" ||
+ p.theme === "midnight";
+ if (!hadLegacy) return;
+
+ const legacyBase = p.customThemeBase === "light" ? "light" : "dark";
+ const legacyPrimary =
+ typeof p.customThemePrimary === "string"
+ ? p.customThemePrimary
+ : DEFAULT_ACCENT;
+
+ // midnight → dark; custom → whichever base it was sitting on.
+ if (p.theme === "midnight") p.theme = "dark";
+ else if (p.theme === "custom") p.theme = legacyBase;
+
+ // Seed the customised side from the old primary; the other side gets the default (neutral) theme.
+ if (p.lightPrimary === undefined) {
+ p.lightPrimary = legacyBase === "light" ? legacyPrimary : DEFAULT_ACCENT;
+ }
+ if (p.darkPrimary === undefined) {
+ p.darkPrimary = legacyBase === "dark" ? legacyPrimary : DEFAULT_ACCENT;
+ }
+ delete p.customThemeBase;
+ delete p.customThemePrimary;
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(p));
+ } catch (error) {
+ console.error("Error migrating theme preferences:", error);
+ }
+ }
+
setServerDefaults(defaults: Partial): void {
this.serverDefaults = defaults;
}
diff --git a/frontend/editor/src/core/styles/theme.css b/frontend/editor/src/core/styles/theme.css
index c02965cf0b..a57c3f4e2f 100644
--- a/frontend/editor/src/core/styles/theme.css
+++ b/frontend/editor/src/core/styles/theme.css
@@ -1,32 +1,26 @@
:root {
/* Compare highlight colors (same in light/dark) */
- --spdf-compare-removed-bg: rgba(255, 107, 107, 0.45); /* #ff6b6b @ 0.45 */
- --spdf-compare-added-bg: rgba(81, 207, 102, 0.35); /* #51cf66 @ 0.35 */
+ --spdf-compare-removed-bg: color-mix(in srgb, var(--p-red-400) 45%, transparent); /* #ff6b6b @ 0.45 */
+ --spdf-compare-added-bg: color-mix(in srgb, var(--p-green-500) 35%, transparent); /* #51cf66 @ 0.35 */
/* Badge colors for dropdowns */
- --spdf-compare-removed-badge-bg: rgba(255, 59, 48, 0.15);
- --spdf-compare-removed-badge-fg: #b91c1c;
- --spdf-compare-added-badge-bg: rgba(52, 199, 89, 0.18);
- --spdf-compare-added-badge-fg: #1b5e20;
+ --spdf-compare-removed-badge-bg: color-mix(in srgb, var(--p-red-500) 15%, transparent);
+ --spdf-compare-removed-badge-fg: var(--p-red-600);
+ --spdf-compare-added-badge-bg: color-mix(in srgb, var(--p-green-500) 18%, transparent);
+ --spdf-compare-added-badge-fg: var(--p-green-700);
/* Inline highlights in summary */
- --spdf-compare-inline-removed-bg: rgba(255, 59, 48, 0.25);
- --spdf-compare-inline-added-bg: rgba(52, 199, 89, 0.25);
+ --spdf-compare-inline-removed-bg: color-mix(in srgb, var(--p-red-500) 25%, transparent);
+ --spdf-compare-inline-added-bg: color-mix(in srgb, var(--p-green-500) 25%, transparent);
/* File Manager active file colors */
- --file-active-bg: rgba(
- 59,
- 130,
- 246,
- 0.1
- ); /* Transparent blue for active file background */
--file-active-badge-bg: rgba(
34,
197,
94,
0.15
); /* Transparent green for active badge */
- --file-active-badge-fg: #15803d; /* Green text for active badge */
+ --file-active-badge-fg: var(--p-green-700); /* Green text for active badge */
--file-active-badge-border: rgba(
34,
197,
@@ -42,13 +36,6 @@
--fullscreen-anim-duration-in: 0.28s;
--fullscreen-anim-duration-out: 0.22s;
- /* Z-index constants (added in this PR) */
- --z-over-fullscreen-surface: 1400;
- --z-fullscreen-surface: 1200;
- --z-fullscreen-favorite-star: 2;
- --z-fullscreen-icon-svg: 1;
- --z-toolpicker-star: 1;
-
/* Standard gray scale */
--gray-50: 249 250 251;
--gray-100: 243 244 246;
@@ -67,108 +54,91 @@
--border: 229 231 235;
/* Colors for Mantine integration */
- --color-primary-50: #eff6ff;
- --color-primary-100: #dbeafe;
- --color-primary-200: #bfdbfe;
- --color-primary-300: #93c5fd;
- --color-primary-400: #60a5fa;
- --color-primary-500: #3b82f6;
- --color-primary-600: #2563eb;
- --color-primary-700: #1d4ed8;
- --color-primary-800: #1e40af;
- --color-primary-900: #1e3a8a;
+ --color-primary-50: var(--p-blue-400);
+ --color-primary-100: var(--p-blue-400);
+ --color-primary-200: var(--p-blue-400);
+ --color-primary-300: var(--p-blue-400);
+ --color-primary-400: var(--p-blue-400);
+ --color-primary-500: var(--p-blue-500);
+ --color-primary-600: var(--p-blue-600);
+ --color-primary-700: var(--p-blue-700);
+ --color-primary-800: var(--p-blue-700);
+ --color-primary-900: var(--p-blue-700);
/* Success (green) */
- --color-green-50: #f0fdf4;
- --color-green-100: #dcfce7;
- --color-green-200: #bbf7d0;
- --color-green-300: #86efac;
- --color-green-400: #4ade80;
- --color-green-500: #22c55e;
- --color-green-600: #16a34a;
- --color-green-700: #15803d;
- --color-green-800: #166534;
- --color-green-900: #14532d;
+ --color-green-50: var(--p-green-500);
+ --color-green-100: var(--p-green-500);
+ --color-green-200: var(--p-green-500);
+ --color-green-300: var(--p-green-500);
+ --color-green-400: var(--p-green-500);
+ --color-green-500: var(--p-green-500);
+ --color-green-600: var(--p-green-600);
+ --color-green-700: var(--p-green-700);
+ --color-green-800: var(--p-green-700);
+ --color-green-900: var(--p-green-700);
/* Warning (yellow) */
- --color-yellow-50: #fefce8;
- --color-yellow-100: #fef9c3;
- --color-yellow-200: #fef08a;
- --color-yellow-300: #fde047;
- --color-yellow-400: #facc15;
+ --color-yellow-50: var(--p-amber-400);
+ --color-yellow-100: var(--p-amber-400);
+ --color-yellow-200: var(--p-amber-400);
+ --color-yellow-300: var(--p-amber-400);
+ --color-yellow-400: var(--p-amber-400);
/* Category colors - consistent across light and dark modes */
- --category-color-removal: #ef4444; /* Red for removal tools */
- --category-color-security: #f59e0b; /* Orange for security tools */
- --category-color-formatting: #8b5cf6; /* Purple for formatting tools */
- --category-color-extraction: #06b6d4; /* Cyan for extraction tools */
- --category-color-signing: #10b981; /* Green for signing tools */
- --category-color-general: #3b82f6; /* Blue for general tools */
- --category-color-verification: #f97316; /* Orange for verification tools */
- --category-color-automation: #ec4899; /* Pink for automation tools */
- --category-color-developer: #6b7280; /* Gray for developer tools */
- --category-color-default: #6b7280; /* Default gray */
+ --category-color-removal: var(--p-red-500); /* Red for removal tools */
+ --category-color-security: var(--p-amber-500); /* Orange for security tools */
+ --category-color-formatting: var(--p-blue-400); /* Purple for formatting tools */
+ --category-color-extraction: var(--p-blue-500); /* Cyan for extraction tools */
+ --category-color-signing: var(--p-green-500); /* Green for signing tools */
+ --category-color-general: var(--p-blue-500); /* Blue for general tools */
+ --category-color-verification: var(--p-amber-600); /* Orange for verification tools */
+ --category-color-automation: var(--p-red-400); /* Pink for automation tools */
+ --category-color-developer: var(--p-gray-500); /* Gray for developer tools */
+ --category-color-default: var(--p-gray-500); /* Default gray */
/* Special section colors - consistent across light and dark modes */
- --special-color-favorites: #ffc107; /* Yellow/gold for favorites */
- --special-color-recommended: #1bb1d4; /* Cyan for recommended */
- --color-yellow-500: #eab308;
- --color-yellow-600: #ca8a04;
- --color-yellow-700: #a16207;
- --color-yellow-800: #854d0e;
- --color-yellow-900: #713f12;
+ --special-color-favorites: var(--p-amber-400); /* Yellow/gold for favorites */
+ --special-color-recommended: var(--p-blue-500); /* Cyan for recommended */
+ --color-yellow-500: var(--p-amber-500);
+ --color-yellow-600: var(--p-amber-600);
+ --color-yellow-700: var(--p-amber-600);
+ --color-yellow-800: var(--p-amber-600);
+ --color-yellow-900: var(--p-amber-600);
- --color-red-50: #fef2f2;
- --color-red-100: #fee2e2;
- --color-red-200: #fecaca;
- --color-red-300: #fca5a5;
- --color-red-400: #f87171;
- --color-red-500: #ef4444;
- --color-red-600: #dc2626;
- --color-red-700: #b91c1c;
- --color-red-800: #991b1b;
- --color-red-900: #7f1d1d;
+ --color-red-50: var(--p-red-400);
+ --color-red-100: var(--p-red-400);
+ --color-red-200: var(--p-red-400);
+ --color-red-300: var(--p-red-400);
+ --color-red-400: var(--p-red-400);
+ --color-red-500: var(--p-red-500);
+ --color-red-600: var(--p-red-600);
+ --color-red-700: var(--p-red-600);
+ --color-red-800: var(--p-red-600);
+ --color-red-900: var(--p-red-600);
- --color-gray-50: #f9fafb;
- --color-gray-100: #f3f4f6;
- --color-gray-200: #e5e7eb;
- --color-gray-300: #d1d5db;
- --color-gray-400: #9ca3af;
- --color-gray-500: #6b7280;
- --color-gray-600: #4b5563;
- --color-gray-700: #374151;
- --color-gray-800: #1f2937;
- --color-gray-900: #111827;
+ --color-gray-50: var(--p-gray-50);
+ --color-gray-100: var(--p-gray-100);
+ --color-gray-200: var(--p-gray-200);
+ --color-gray-300: var(--p-gray-300);
+ --color-gray-400: var(--p-gray-400);
+ --color-gray-500: var(--p-gray-500);
+ --color-gray-600: var(--p-gray-600);
+ --color-gray-700: var(--p-gray-700);
+ --color-gray-800: var(--p-gray-800);
+ --color-gray-900: var(--p-gray-900);
/* Categorical accents (sidebar groups, file-type icons, badges). One hue per
token; the dark block overrides each with a LIGHTER shade for contrast on
dark backgrounds. JS consumers go through utils/accentColors.ts. */
- --accent-green: #16a34a;
- --accent-blue: #2563eb;
- --accent-red: #dc2626;
- --accent-orange: #ea580c;
- --accent-purple: #9333ea;
- --accent-teal: #0d9488;
- --accent-pink: #db2777;
- --accent-amber: #d97706;
- --accent-indigo: #4f46e5;
- --accent-cyan: #0891b2;
- --accent-violet: #7c3aed;
- --accent-gray: #6b7280;
-
- /* Spacing system */
- --space-xs: 4px;
- --space-sm: 8px;
- --space-md: 16px;
- --space-lg: 24px;
- --space-xl: 32px;
-
- /* Radius system */
- --radius-xs: 2px;
- --radius-sm: 4px;
- --radius-md: 8px;
- --radius-lg: 12px;
- --radius-xl: 16px;
+ --accent-orange: var(--p-amber-600);
+ --accent-purple: var(--p-blue-500);
+ --accent-teal: var(--p-blue-700);
+ --accent-pink: var(--p-red-500);
+ --accent-amber: var(--p-amber-600);
+ --accent-indigo: var(--p-blue-600);
+ --accent-cyan: var(--p-blue-600);
+ --accent-violet: var(--p-blue-600);
+ --accent-gray: var(--p-gray-500);
/* Shadow system */
--shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.05);
@@ -177,201 +147,73 @@
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
--shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.1);
- /* Font weights */
- --font-weight-normal: 400;
- --font-weight-medium: 500;
- --font-weight-semibold: 600;
- --font-weight-bold: 700;
-
- /* Light theme semantic colors */
- --bg-surface: #ffffff;
- --bg-raised: #f9fafb;
- --bg-muted: #f3f4f6;
- --bg-background: #f9fafb;
- --bg-toolbar: #ffffff;
- --bg-file-manager: #f5f6f8;
- --bg-file-list: #ffffff;
- --btn-open-file: #0a8bff;
- --text-primary: #111827;
- --text-secondary: #4b5563;
- --text-muted: #6b7280;
/* Always-dark text (for use on light backgrounds like alerts) - does not change in dark mode */
- --text-always-dark: #1f2937;
- --text-always-dark-muted: #6b7280;
- --border-subtle: #e5e7eb;
- --border-default: #e2e8f0;
- --border-strong: #9ca3af;
- --border-hover: #9ca3af;
- --hover-bg: #f9fafb;
- --active-bg: #f3f4f6;
- --automation-entry-hover-bg: var(--color-gray-100);
+ --text-always-dark: var(--p-gray-800);
+ --text-always-dark-muted: var(--p-gray-500);
/* Icon colors for light mode */
- --icon-user-bg: #9ca3af;
- --icon-user-color: #ffffff;
- --icon-notifications-bg: #9ca3af;
- --icon-notifications-color: #ffffff;
- --icon-tools-bg: #1e88e5;
+ --icon-tools-bg: var(--p-blue-500);
--icon-tools-color: #ffffff;
- --icon-read-bg: #4caf50;
- --icon-read-color: #ffffff;
- --icon-sign-bg: #3ba99c;
- --icon-sign-color: #ffffff;
- --icon-automate-bg: #a576e3;
- --icon-automate-color: #ffffff;
- --icon-files-bg: #d3e7f7;
- --icon-files-color: #0a8bff;
- --icon-activity-bg: #d3e7f7;
- --icon-activity-color: #0a8bff;
- --icon-config-bg: #9ca3af;
- --icon-config-color: #ffffff;
+ --icon-files-color: var(--p-blue-500);
/* Colors for tooltips */
- --tooltip-title-bg: #dbefff;
- --tooltip-title-color: #31528e;
- --tooltip-header-bg: #31528e;
+ --tooltip-header-bg: var(--p-blue-700);
--tooltip-header-color: white;
--tooltip-border: var(--border-default);
/* Inactive icon colors for light mode */
- --icon-inactive-bg: #9ca3af;
+ --icon-inactive-bg: var(--p-gray-400);
--icon-inactive-color: #ffffff;
- /* New theme colors for text and icons */
- --tools-text-and-icon-color: var(--text-primary);
-
- /* Tool picker sticky header variables (light mode) */
- --tool-header-bg: #dbefff;
- --tool-header-border: #bee2ff;
- --tool-header-text: #1e88e5;
- --tool-header-badge-bg: #c0ddff;
- --tool-header-badge-text: #004e99;
-
- /* Subcategory title styling (light mode) */
- --tool-subcategory-text-color: #9ca3af; /* lighter text */
- --tool-subcategory-rule-color: #e5e7eb; /* doubly lighter rule line */
- --accent-interactive: #4a90e2;
- --text-instruction: #4a90e2;
- --text-brand: var(--color-gray-700);
- --text-brand-accent: #dc2626;
+/* doubly lighter rule line */
+ --text-brand-accent: var(--p-red-600);
/* PDF text selection colors */
- --pdf-selection-bg: rgba(59, 130, 246, 0.2);
- --pdf-selection-ring: rgba(59, 130, 246, 0.18);
-
- /* Placeholder text colors */
- --search-text-and-icon-color: #6b7382;
- --input-bg: #ffffff;
-
- /* Tool panel search bar background colors */
- --tool-panel-search-bg: #eff1f4;
- --tool-panel-search-border-bottom: #eff1f4;
+ --pdf-selection-bg: color-mix(in srgb, var(--p-blue-500) 20%, transparent);
+ --pdf-selection-ring: color-mix(in srgb, var(--p-blue-500) 18%, transparent);
/* container */
- --landing-paper-bg: var(--bg-surface);
- --landing-inner-paper-bg: #eef8ff;
- --landing-inner-paper-border: #cdeaff;
--landing-button-bg: var(--bg-surface);
- --landing-button-color: var(--icon-tools-bg);
- --landing-button-border: #e0f2f7;
- --landing-button-hover-bg: rgb(251, 251, 251);
+ --landing-button-border: var(--p-blue-400);
+ --landing-button-hover-bg: var(--p-gray-50);
/* drop state */
- --landing-drop-paper-bg: #e3f2fd;
- --landing-drop-inner-paper-bg: #bbdefb;
- --landing-drop-inner-paper-border: #90caf9;
+ --landing-drop-inner-paper-bg: var(--p-blue-400);
+ --landing-drop-inner-paper-border: var(--p-blue-400);
/* landing hero & stack */
- --landing-hero-gradient: linear-gradient(135deg, #4c8bf5 0%, #3a7be8 100%);
+ --landing-hero-gradient: linear-gradient(135deg, var(--p-blue-500) 0%, var(--p-blue-500) 100%);
--landing-stack-w: 224px;
--landing-stack-h: 176px;
- --landing-stack-glow-bg: radial-gradient(
- circle,
- rgba(74, 144, 226, 0.18) 0%,
- transparent 70%
- );
/* landing doc stack shadows */
--landing-doc-shadow-back-idle:
0 4px 20px rgba(0, 0, 0, 0.08), 0 1px 3px rgba(0, 0, 0, 0.04);
- --landing-doc-shadow-back-hover:
- 0 12px 40px rgba(0, 0, 0, 0.15), 0 4px 12px rgba(0, 0, 0, 0.08);
--landing-doc-shadow-front-idle:
0 8px 30px rgba(0, 0, 0, 0.12), 0 4px 12px rgba(0, 0, 0, 0.06),
0 0 0 1px rgba(0, 0, 0, 0.02);
- --landing-doc-shadow-front-hover:
- 0 18px 48px rgba(0, 0, 0, 0.18), 0 8px 20px rgba(0, 0, 0, 0.1),
- 0 0 0 1px rgba(0, 0, 0, 0.04);
-
- /* landing action button shadows */
- --landing-action-transition:
- transform 0.28s cubic-bezier(0.4, 0, 0.2, 1),
- box-shadow 0.32s cubic-bezier(0.4, 0, 0.2, 1);
- --landing-action-shadow-idle:
- 0 6px 18px rgba(0, 0, 0, 0), 0 2px 6px rgba(0, 0, 0, 0);
- --landing-action-shadow-hover:
- 0 6px 18px rgba(0, 0, 0, 0.14), 0 2px 6px rgba(0, 0, 0, 0.08);
- --landing-action-primary-shadow-idle:
- 0 10px 26px rgba(58, 123, 232, 0), 0 4px 12px rgba(0, 0, 0, 0);
- --landing-action-primary-shadow-hover:
- 0 10px 26px rgba(58, 123, 232, 0.42), 0 4px 12px rgba(0, 0, 0, 0.1);
/* selected file header colors */
- --header-selected-bg: #1e88e5; /* light mode selected header matches dark */
+ --header-selected-bg: var(--p-blue-500); /* light mode selected header matches dark */
--header-selected-fg: #ffffff;
- --file-card-bg: #ffffff; /* file card background (light/dark paired) */
- --accordion-item-bg: #e8eaed; /* accordion item background - more distinguishable */
- /* shadows */
- --drop-shadow-color: rgba(0, 0, 0, 0.08);
- --drop-shadow-color-strong: rgba(0, 0, 0, 0.04);
- --drop-shadow-filter: drop-shadow(0 0.2rem 0.4rem rgba(0, 0, 0, 0.08))
- drop-shadow(0 0.6rem 0.6rem rgba(0, 0, 0, 0.06))
- drop-shadow(0 1.2rem 1rem rgba(0, 0, 0, 0.04));
+/* slightly more blue than dark mode header */
- /* Light mode card hover and selection */
- --header-hover-bg: #3b4b6e; /* same family as selected, a touch muted for hover */
- --card-selected-border: #3fafff; /* slightly more blue than dark mode header */
- --checkbox-border: #2f83bf;
- --checkbox-checked-bg: #3fafff;
- --checkbox-tick: #ffffff;
-
- --information-text-bg: #eaeaea;
- --information-text-color: #5e5e5e;
- /* Bulk selection panel specific colors (light mode) */
- --bulk-panel-bg: #ffffff; /* white background for parent container */
- --bulk-card-bg: #ffffff; /* white background for cards */
- --bulk-card-border: #e5e7eb; /* light gray border for cards and buttons */
- --bulk-card-hover-border: #d1d5db; /* slightly darker on hover */
- --unsupported-bar-bg: #5a616e;
- --unsupported-bar-border: #6b7280;
+ --information-text-color: var(--p-zinc-400);
+/* white background for cards */
+ --bulk-card-border: var(--p-gray-200); /* light gray border for cards and buttons */
+ --bulk-card-hover-border: var(--p-gray-300); /* slightly darker on hover */
/* Config Modal colors (light mode) */
- --modal-nav-bg: #f5f6f8;
- --modal-nav-section-title: #6b7280;
- --modal-nav-item: #374151;
- --modal-nav-item-active: #0a8bff;
- --modal-nav-item-active-bg: rgba(10, 139, 255, 0.08);
- --modal-content-bg: #ffffff;
- --modal-header-border: rgba(0, 0, 0, 0.06);
+ --modal-nav-item: var(--p-gray-700);
/* API Keys section colors (light mode) */
- --api-keys-card-bg: #ffffff;
- --api-keys-card-border: #e0e0e0;
--api-keys-card-shadow: rgba(0, 0, 0, 0.06);
- --api-keys-input-bg: #f8f8f8;
- --api-keys-input-border: #e0e0e0;
/* PDF Report Colors (always light) */
- --pdf-light-header-bg: 239 246 255;
- --pdf-light-accent: 59 130 246;
- --pdf-light-text-primary: 30 41 59;
--pdf-light-text-muted: 100 116 139;
--pdf-light-box-bg: 248 250 252;
--pdf-light-box-border: 226 232 240;
- --pdf-light-warning: 234 179 8;
- --pdf-light-danger: 248 113 113;
- --pdf-light-success: 34 197 94;
--pdf-light-neutral: 148 163 184;
--pdf-light-status-valid-bg: 209 250 229;
--pdf-light-status-valid-text: 6 95 70;
@@ -381,32 +223,20 @@
--pdf-light-status-invalid-text: 153 27 27;
--pdf-light-status-neutral-bg: 229 231 235;
--pdf-light-status-neutral-text: 55 65 81;
- --pdf-light-report-container-bg: 249 250 251;
- --pdf-light-simulated-page-bg: 255 255 255;
--pdf-light-simulated-page-text: 15 23 42;
/* Code token colors (light mode) */
- --code-kw-color: #1d4ed8; /* blue-700 */
- --code-str-color: #16a34a; /* green-600 */
- --code-num-color: #4338ca; /* indigo-700 */
- --code-com-color: #6b7280; /* gray-500 */
- /* Compare tool specific colors - only for colors that don't have existing theme pairs */
- --compare-upload-dropzone-bg: rgba(241, 245, 249, 0.45);
- --compare-upload-dropzone-border: rgba(148, 163, 184, 0.6);
- --compare-upload-icon-bg: rgba(148, 163, 184, 0.2);
- --compare-upload-icon-color: rgba(17, 24, 39, 0.75);
- --compare-upload-divider: rgba(148, 163, 184, 0.5);
+ --code-kw-color: var(--p-blue-700); /* blue-700 */
+ --code-str-color: var(--p-green-600); /* green-600 */
+ --code-num-color: var(--p-blue-700); /* indigo-700 */
+ --code-com-color: var(--p-gray-500); /* gray-500 */
/* Compare page label chip (light mode): slightly lighter than surrounding rows */
- --compare-page-label-bg: var(--bg-muted);
--compare-page-label-fg: var(--text-secondary);
/* Status indicator dot ring shadow */
--status-dot-ring: 0 0 0 2px rgba(0, 0, 0, 0.08);
- /* Input element styling */
- --input-bg: #ffffff;
-
/* Select/dropdown placeholder state */
--select-placeholder-bg: var(--mantine-color-gray-1);
--select-placeholder-text: var(--mantine-color-gray-6);
@@ -414,7 +244,6 @@
/* Grouped format dropdown */
--dropdown-trigger-bg: var(--mantine-color-white);
--dropdown-trigger-text: var(--mantine-color-dark-9);
- --dropdown-trigger-text-disabled: var(--mantine-color-dark-7);
--dropdown-trigger-icon: var(--mantine-color-gray-6);
--dropdown-panel-bg: var(--mantine-color-white);
--dropdown-panel-border: var(--mantine-color-gray-4);
@@ -423,211 +252,140 @@
/* Onboarding (light mode) */
:root {
- --onboarding-title: #0a0a0a;
- --onboarding-body: #4a5565;
- --onboarding-step-active: #1e2939;
- --onboarding-step-inactive: #d1d5dc;
+ --onboarding-step-active: var(--p-gray-800);
+ --onboarding-step-inactive: var(--p-gray-300);
}
[data-mantine-color-scheme="dark"] {
- /* Dark theme gray scale (inverted) — navy-indigo tinted */
- --gray-50: 9 11 24;
- --gray-100: 13 16 32;
- --gray-200: 19 23 41;
- --gray-300: 28 35 64;
- --gray-400: 45 55 96;
- --gray-500: 90 98 128;
- --gray-600: 144 153 185;
- --gray-700: 195 200 230;
- --gray-800: 218 222 245;
- --gray-900: 238 241 252;
+ /* Inverted dark gray scale — RGB channels for Tailwind's rgb(var(--gray-N)). */
+ --gray-50: 10 10 11; /* zinc-950 */
+ --gray-100: 16 16 18; /* zinc-900 */
+ --gray-200: 24 24 27; /* zinc-800 */
+ --gray-300: 42 42 46; /* zinc-650 */
+ --gray-400: 63 63 70; /* zinc-500 */
+ --gray-500: 82 82 91; /* zinc-400 */
+ --gray-600: 113 113 122; /* zinc-300 */
+ --gray-700: 161 161 170; /* zinc-200 */
+ --gray-800: 212 212 216;
+ --gray-900: 244 244 245; /* zinc-100 */
- /* Dark semantic colors for Tailwind — aligned with portal tokens.css */
- --surface: 21 28 46; /* #151c2e — portal --color-surface */
- --background: 9 12 20; /* #090c14 — portal --color-bg */
- --border: 40 50 72; /* #283248 — portal --color-border */
+ /* Dark semantic channels for Tailwind. */
+ --surface: 24 24 27; /* zinc-800 */
+ --background: 10 10 11; /* zinc-950 */
+ --border: 42 42 46; /* zinc-650 */
/* Dark theme Mantine colors */
- --color-red-50: #2d1b1b;
- --color-red-100: #3a2323;
- --color-red-200: #4a2d2d;
- --color-red-300: #5c3535;
- --color-red-400: #7c4a4a;
- --color-red-500: #ef4444;
- --color-red-600: #dc2626;
- --color-red-700: #b91c1c;
- --color-red-800: #991b1b;
- --color-red-900: #7f1d1d;
+ --color-red-50: var(--p-zinc-775);
+ --color-red-100: var(--p-zinc-700);
+ --color-red-200: var(--p-zinc-600);
+ --color-red-300: var(--p-zinc-500);
+ --color-red-400: var(--p-zinc-400);
+ --color-red-500: var(--p-red-500);
+ --color-red-600: var(--p-red-600);
+ --color-red-700: var(--p-red-600);
+ --color-red-800: var(--p-red-600);
+ --color-red-900: var(--p-red-600);
- --color-gray-50: #111827;
- --color-gray-100: #1f2329;
- --color-gray-200: #2a2f36;
- --color-gray-300: #374151;
- --color-gray-400: #4b5563;
- --color-gray-500: #6b7280;
- --color-gray-600: #9ca3af;
- --color-gray-700: #d1d5db;
- --color-gray-800: #e5e7eb;
- --color-gray-900: #f3f4f6;
+ --color-gray-50: var(--p-gray-900);
+ --color-gray-100: var(--p-zinc-775);
+ --color-gray-200: var(--p-zinc-650);
+ --color-gray-300: var(--p-gray-700);
+ --color-gray-400: var(--p-gray-600);
+ --color-gray-500: var(--p-gray-500);
+ --color-gray-600: var(--p-gray-400);
+ --color-gray-700: var(--p-gray-300);
+ --color-gray-800: var(--p-gray-200);
+ --color-gray-900: var(--p-gray-100);
/* Categorical accents: lighter shades of the same hues, for contrast on dark. */
- --accent-green: #4ade80;
- --accent-blue: #60a5fa;
- --accent-red: #f87171;
- --accent-orange: #fb923c;
- --accent-purple: #c084fc;
- --accent-teal: #2dd4bf;
- --accent-pink: #f472b6;
- --accent-amber: #fbbf24;
- --accent-indigo: #818cf8;
- --accent-cyan: #22d3ee;
- --accent-violet: #a78bfa;
- --accent-gray: #9ca3af;
+ --accent-orange: var(--p-amber-500);
+ --accent-purple: var(--p-blue-400);
+ --accent-teal: var(--p-blue-400);
+ --accent-pink: var(--p-red-400);
+ --accent-amber: var(--p-amber-400);
+ --accent-indigo: var(--p-blue-400);
+ --accent-cyan: var(--p-blue-400);
+ --accent-violet: var(--p-blue-400);
+ --accent-gray: var(--p-gray-400);
/* Category colors - same as light mode for consistency */
- --category-color-removal: #ef4444; /* Red for removal tools */
- --category-color-security: #f59e0b; /* Orange for security tools */
- --category-color-formatting: #8b5cf6; /* Purple for formatting tools */
- --category-color-extraction: #06b6d4; /* Cyan for extraction tools */
- --category-color-signing: #10b981; /* Green for signing tools */
- --category-color-general: #3b82f6; /* Blue for general tools */
- --category-color-verification: #f97316; /* Orange for verification tools */
- --category-color-automation: #ec4899; /* Pink for automation tools */
- --category-color-developer: #6b7280; /* Gray for developer tools */
- --category-color-default: #6b7280; /* Default gray */
+ --category-color-removal: var(--p-red-500); /* Red for removal tools */
+ --category-color-security: var(--p-amber-500); /* Orange for security tools */
+ --category-color-formatting: var(--p-blue-400); /* Purple for formatting tools */
+ --category-color-extraction: var(--p-blue-500); /* Cyan for extraction tools */
+ --category-color-signing: var(--p-green-500); /* Green for signing tools */
+ --category-color-general: var(--p-blue-500); /* Blue for general tools */
+ --category-color-verification: var(--p-amber-600); /* Orange for verification tools */
+ --category-color-automation: var(--p-red-400); /* Pink for automation tools */
+ --category-color-developer: var(--p-gray-500); /* Gray for developer tools */
+ --category-color-default: var(--p-gray-500); /* Default gray */
/* Special section colors - same as light mode for consistency */
- --special-color-favorites: #ffc107; /* Yellow/gold for favorites */
- --special-color-recommended: #1bb1d4; /* Cyan for recommended */
+ --special-color-favorites: var(--p-amber-400); /* Yellow/gold for favorites */
+ --special-color-recommended: var(--p-blue-500); /* Cyan for recommended */
/* Success (green) - dark */
- --color-green-50: #052e16;
- --color-green-100: #064e3b;
- --color-green-200: #065f46;
- --color-green-300: #047857;
- --color-green-400: #059669;
- --color-green-500: #22c55e;
- --color-green-600: #16a34a;
- --color-green-700: #4ade80;
- --color-green-800: #86efac;
- --color-green-900: #bbf7d0;
+ --color-green-50: var(--p-green-700);
+ --color-green-100: var(--p-green-700);
+ --color-green-200: var(--p-green-700);
+ --color-green-300: var(--p-green-700);
+ --color-green-400: var(--p-green-600);
+ --color-green-500: var(--p-green-500);
+ --color-green-600: var(--p-green-600);
+ --color-green-700: var(--p-green-500);
+ --color-green-800: var(--p-green-500);
+ --color-green-900: var(--p-green-500);
/* Warning (yellow) - dark */
- --color-yellow-50: #451a03;
- --color-yellow-100: #713f12;
- --color-yellow-200: #854d0e;
- --color-yellow-300: #a16207;
- --color-yellow-400: #ca8a04;
- --color-yellow-500: #eab308;
- --color-yellow-600: #facc15;
- --color-yellow-700: #fde047;
- --color-yellow-800: #fef08a;
- --color-yellow-900: #fef9c3;
+ --color-yellow-50: var(--p-amber-600);
+ --color-yellow-100: var(--p-amber-600);
+ --color-yellow-200: var(--p-amber-600);
+ --color-yellow-300: var(--p-amber-600);
+ --color-yellow-400: var(--p-amber-600);
+ --color-yellow-500: var(--p-amber-500);
+ --color-yellow-600: var(--p-amber-400);
+ --color-yellow-700: var(--p-amber-400);
+ --color-yellow-800: var(--p-amber-400);
+ --color-yellow-900: var(--p-amber-400);
- /* Dark theme semantic colors — deep navy/indigo palette, aligned with portal
- tokens.css so the editor and portal (Processor) dark modes don't drift.
- Chrome surfaces (--bg-toolbar, --bg-file-manager) sit at the portal's
- sidebar level (#0d1120) so the left/right sidebars lift off the darker
- canvas (--bg-background) instead of blending into it. */
- --bg-surface: #151c2e; /* portal --color-surface */
- --bg-raised: #0d1120; /* portal --color-bg-alt */
- --bg-muted: #0d1120;
- --bg-background: #090c14; /* portal --color-bg */
- --bg-toolbar: #0d1120; /* portal --color-sidebar-bg — lifted off the canvas */
- --bg-file-manager: #0d1120; /* portal --color-sidebar-bg */
- --bg-file-list: #151c2e; /* portal --color-surface */
- --btn-open-file: #4f8ef5;
- --text-primary: #e8eaf6;
- --text-secondary: #9299b0;
- --text-muted: #5b6280;
+ /* Dark theme semantic colours — the neutral zinc ramp. These legacy
+ `--bg-*`/`--text-*` names are shadowed by compat.css (→ --c-*), so this is
+ kept only for any consumer not yet on the semantic tokens. */
/* Always-dark text (for use on light backgrounds like alerts) - does not change in dark mode */
- --text-always-dark: #1f2937;
- --text-always-dark-muted: #6b7280;
- --border-subtle: rgba(255, 255, 255, 0.05);
- --border-default: #1e2840; /* portal --color-border-light */
- --border-strong: #283248; /* portal --color-border */
- --border-hover: #3d4f6a; /* portal --color-border-hover */
- --hover-bg: #1c2640; /* portal --color-bg-hover */
- --active-bg: #243044; /* portal --color-bg-muted */
- --automation-entry-hover-bg: var(--color-gray-200);
+ --text-always-dark: var(--p-gray-800);
+ --text-always-dark-muted: var(--p-gray-500);
/* Icon colors for dark mode */
- --icon-user-bg: #131729;
- --icon-user-color: #6e7898;
- --icon-notifications-bg: #131729;
- --icon-notifications-color: #6e7898;
- --icon-tools-bg: #1c2340;
- --icon-tools-color: #e0e3f8;
- --icon-read-bg: #1c2340;
- --icon-read-color: #e0e3f8;
- --icon-sign-bg: #1c2340;
- --icon-sign-color: #e0e3f8;
- --icon-automate-bg: #1c2340;
- --icon-automate-color: #e0e3f8;
- --icon-files-bg: #1c2340;
- --icon-files-color: #e0e3f8;
- --icon-activity-bg: #1c2340;
- --icon-activity-color: #e0e3f8;
- --icon-config-bg: #1c2340;
- --icon-config-color: #e0e3f8;
+ --icon-tools-bg: var(--p-zinc-650);
+ --icon-tools-color: var(--p-blue-400);
+ --icon-files-color: var(--p-blue-400);
/* Inactive icon colors for dark mode */
- --icon-inactive-bg: #131729;
- --icon-inactive-color: #6e7898;
+ --icon-inactive-bg: var(--p-zinc-800);
+ --icon-inactive-color: var(--p-gray-500);
/* Dark mode tooltip colors */
- --tooltip-title-bg: #1c2340;
- --tooltip-title-color: #e8eaf6;
--tooltip-header-bg: var(--bg-raised);
--tooltip-header-color: var(--text-primary);
--tooltip-border: var(--border-default);
- --accent-interactive: #e8eaf6;
- --text-instruction: #e8eaf6;
- --text-brand: var(--color-gray-800);
- --text-brand-accent: #ef4444;
+ --text-brand-accent: var(--p-red-500);
/* Compare badge text colors (dark mode): lighter for readability */
--spdf-compare-removed-badge-fg: var(--color-red-500);
--spdf-compare-added-badge-fg: var(--color-green-500);
- /* container */
- --landing-paper-bg: #090b18;
- --landing-inner-paper-bg: var(--bg-raised);
- --landing-inner-paper-border: #1c2340;
-
/* landing dark overrides */
- --landing-stack-glow-bg: radial-gradient(
- circle,
- rgba(50, 90, 210, 0.22) 0%,
- transparent 70%
- );
--landing-doc-shadow-back-idle:
0 4px 20px rgba(0, 0, 0, 0.45), 0 1px 3px rgba(0, 0, 0, 0.35);
- --landing-doc-shadow-back-hover:
- 0 14px 44px rgba(0, 0, 0, 0.65), 0 6px 16px rgba(0, 0, 0, 0.45);
--landing-doc-shadow-front-idle:
0 8px 30px rgba(0, 0, 0, 0.55), 0 4px 12px rgba(0, 0, 0, 0.4),
0 0 0 1px rgba(255, 255, 255, 0.04);
- --landing-doc-shadow-front-hover:
- 0 20px 52px rgba(0, 0, 0, 0.7), 0 10px 24px rgba(0, 0, 0, 0.5),
- 0 0 0 1px rgba(255, 255, 255, 0.06);
- --landing-button-color: #e8eaf6;
--landing-button-hover-bg: var(--bg-raised);
/* selected file header colors for dark */
- --header-selected-bg: #2040a0;
+ --header-selected-bg: var(--p-blue-700);
--header-selected-fg: #ffffff;
- /* file card background (dark) */
- --file-card-bg: #0d1120;
- --accordion-item-bg: #1c2340; /* accordion item background - more distinguishable */
-
- /* shadows */
- --drop-shadow-color: rgba(255, 255, 255, 0.05);
- --drop-shadow-color-strong: rgba(255, 255, 255, 0.03);
- --drop-shadow-filter: drop-shadow(0 0.2rem 0.4rem rgba(0, 0, 0, 0.3))
- drop-shadow(0 0.6rem 0.6rem rgba(0, 0, 0, 0.2))
- drop-shadow(0 1.2rem 1rem rgba(0, 0, 0, 0.15));
/* Adjust shadows for dark mode */
--shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.4);
@@ -636,87 +394,41 @@
--shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.5);
--shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.5);
- --tools-text-and-icon-color: var(--text-primary);
-
- /* Tool picker sticky header variables (dark mode) */
- --tool-header-bg: #131729;
- --tool-header-border: #1c2340;
- --tool-header-text: #c2c8e0;
- --tool-header-badge-bg: #1c2340;
- --tool-header-badge-text: #e8eaf6;
-
- /* Subcategory title styling (dark mode) */
- --tool-subcategory-text-color: #5b6280;
- --tool-subcategory-rule-color: #1c2340;
-
/* PDF text selection colors (dark mode) */
- --pdf-selection-bg: rgba(96, 130, 250, 0.25);
- --pdf-selection-ring: rgba(96, 130, 250, 0.28);
-
- /* Placeholder text colors (dark mode) */
- --search-text-and-icon-color: #e8eaf6 !important;
- --input-bg: #1c2340;
+ --pdf-selection-bg: color-mix(in srgb, var(--p-blue-400) 25%, transparent);
+ --pdf-selection-ring: color-mix(in srgb, var(--p-blue-400) 28%, transparent);
/* Tool panel search bar background colors (dark mode) */
- --tool-panel-search-bg: #0d1020;
- --tool-panel-search-border-bottom: #1c2340;
- --information-text-bg: #131729;
- --information-text-color: #e0e3f8;
+ --information-text-color: var(--p-blue-400);
- /* Bulk selection panel specific colors (dark mode) */
- --bulk-panel-bg: var(--bg-raised); /* dark background for parent container */
- --bulk-card-bg: var(--bg-raised); /* dark background for cards */
+/* dark background for cards */
--bulk-card-border: var(
--border-default
); /* default border for cards and buttons */
--bulk-card-hover-border: var(--border-strong); /* stronger border on hover */
- --unsupported-bar-bg: #0d1020;
- --unsupported-bar-border: #1c2340;
/* Config Modal colors (dark mode) */
- --modal-nav-bg: #0d1020;
- --modal-nav-section-title: #5b6280;
- --modal-nav-item: #c2c8e0;
- --modal-nav-item-active: #4f8ef5;
- --modal-nav-item-active-bg: rgba(79, 142, 245, 0.15);
- --modal-content-bg: #131729;
- --modal-header-border: rgba(255, 255, 255, 0.05);
+ --modal-nav-item: var(--p-gray-300);
/* Onboarding (dark mode) */
- --onboarding-title: #e8eaf6;
- --onboarding-body: #9299b0;
- --onboarding-step-active: #d8dcf0;
- --onboarding-step-inactive: #2d3560;
+ --onboarding-step-active: var(--p-gray-250);
+ --onboarding-step-inactive: var(--p-zinc-500);
/* API Keys section colors (dark mode) */
- --api-keys-card-bg: #131729;
- --api-keys-card-border: #1c2340;
--api-keys-card-shadow: none;
- --api-keys-input-bg: #0d1020;
- --api-keys-input-border: #1c2340;
/* Code token colors (dark mode - Cursor-like) */
- --code-kw-color: #c792ea; /* purple */
- --code-str-color: #c3e88d; /* green */
- --code-num-color: #f78c6c; /* orange */
- --code-com-color: #697098; /* muted gray-blue */
- /* Compare tool specific colors (dark mode) - only for colors that don't have existing theme pairs */
- --compare-upload-dropzone-bg: rgba(9, 11, 24, 0.45);
- --compare-upload-dropzone-border: rgba(28, 35, 64, 0.6);
- --compare-upload-icon-bg: rgba(28, 35, 64, 0.2);
- --compare-upload-icon-color: rgba(232, 234, 246, 0.75);
- --compare-upload-divider: rgba(28, 35, 64, 0.5);
+ --code-kw-color: var(--p-blue-400); /* purple */
+ --code-str-color: var(--p-green-500); /* green */
+ --code-num-color: var(--p-red-400); /* orange */
+ --code-com-color: var(--p-gray-500); /* muted gray-blue */
/* Compare page label chip (dark mode): slightly darker than surrounding rows */
- --compare-page-label-bg: #0d1020;
--compare-page-label-fg: var(--text-secondary);
/* Status indicator dot ring shadow (dark) */
--status-dot-ring: 0 0 0 2px rgba(255, 255, 255, 0.15);
- /* Input element styling (dark) */
- --input-bg: #1c2340;
-
/* Select/dropdown placeholder state (dark) */
--select-placeholder-bg: var(--mantine-color-dark-5);
--select-placeholder-text: var(--mantine-color-dark-2);
@@ -724,7 +436,6 @@
/* Grouped format dropdown (dark) */
--dropdown-trigger-bg: var(--mantine-color-dark-6);
--dropdown-trigger-text: var(--mantine-color-dark-0);
- --dropdown-trigger-text-disabled: var(--mantine-color-dark-1);
--dropdown-trigger-icon: var(--mantine-color-dark-2);
--dropdown-panel-bg: var(--mantine-color-dark-7);
--dropdown-panel-border: var(--mantine-color-dark-4);
@@ -739,7 +450,6 @@
/* Plan section card borders - only override in dark mode */
[data-mantine-color-scheme="dark"] .plan-card {
- --paper-border-color: rgb(44, 52, 120) !important;
}
[data-mantine-color-scheme="dark"] .plan-card [data-size="sm"] {
@@ -809,22 +519,22 @@
border-radius: 8px;
}
-/* Smooth transitions for theme switching */
+/* Smooth transitions for theme switching. Text `color` is deliberately EXCLUDED:
+ it must swap instantly, never fade white↔black through grey. */
* {
transition:
background-color 0.2s ease,
- border-color 0.2s ease,
- color 0.2s ease;
+ border-color 0.2s ease;
}
/* ── PDF Link Overlay (viewer) ── */
:root {
- --link-hover-bg: rgba(10, 139, 255, 0.1);
- --link-hover-border: rgba(10, 139, 255, 0.32);
+ --link-hover-bg: color-mix(in srgb, var(--p-blue-500) 10%, transparent);
+ --link-hover-border: color-mix(in srgb, var(--p-blue-500) 32%, transparent);
--link-hover-shadow:
inset 0 0 0 1px rgba(255, 255, 255, 0.35),
0 1px 4px rgba(10, 139, 255, 0.12);
- --link-focus-ring: rgba(10, 139, 255, 0.25);
- --link-toolbar-bg: rgba(15, 23, 42, 0.88);
+ --link-focus-ring: color-mix(in srgb, var(--p-blue-500) 25%, transparent);
+ --link-toolbar-bg: color-mix(in srgb, var(--p-blue-700) 88%, transparent);
--link-toolbar-border: rgba(255, 255, 255, 0.1);
--link-toolbar-shadow:
0 12px 40px rgba(0, 0, 0, 0.2), 0 4px 12px rgba(0, 0, 0, 0.16),
@@ -832,12 +542,12 @@
}
[data-mantine-color-scheme="dark"] {
- --link-hover-bg: rgba(79, 130, 245, 0.14);
- --link-hover-border: rgba(79, 130, 245, 0.38);
+ --link-hover-bg: color-mix(in srgb, var(--p-blue-500) 14%, transparent);
+ --link-hover-border: color-mix(in srgb, var(--p-blue-500) 38%, transparent);
--link-hover-shadow:
inset 0 0 0 1px rgba(255, 255, 255, 0.1), 0 1px 4px rgba(79, 142, 245, 0.18);
- --link-focus-ring: rgba(79, 130, 245, 0.3);
- --link-toolbar-bg: rgba(9, 11, 24, 0.94);
+ --link-focus-ring: color-mix(in srgb, var(--p-blue-500) 30%, transparent);
+ --link-toolbar-bg: color-mix(in srgb, var(--p-blue-700) 94%, transparent);
--link-toolbar-border: rgba(255, 255, 255, 0.07);
--link-toolbar-shadow:
0 12px 40px rgba(0, 0, 0, 0.5), 0 4px 12px rgba(0, 0, 0, 0.35),
@@ -1021,7 +731,7 @@
}
:root {
- --shadow-color: rgba(15, 23, 42, 0.55);
+ --shadow-color: color-mix(in srgb, var(--p-blue-700) 55%, transparent);
}
[data-theme="dark"] {
@@ -1039,9 +749,8 @@
display: block;
}
-/* Theme-aware image display utilities */
-/* Use .theme-img-light-only for images to show in light mode only */
-/* Use .theme-img-dark-only for images to show in dark mode only */
+/* Theme-aware image display utilities: .theme-img-light-only shows in light
+ mode only, .theme-img-dark-only shows in dark mode only. */
.theme-img-light-only {
display: inline;
}
@@ -1105,8 +814,8 @@
fill: var(--mantine-color-body);
}
-/* Wordmark muted variant (used in empty/placeholder states) */
-/* Stirling text: 30% opacity in light mode, full opacity in dark */
+/* Wordmark muted variant (empty/placeholder states).
+ Stirling text: 30% opacity in light mode, full opacity in dark. */
.wordmark__text--muted {
fill-opacity: 0.3;
}
diff --git a/frontend/editor/src/core/theme/README.md b/frontend/editor/src/core/theme/README.md
new file mode 100644
index 0000000000..8abc110723
--- /dev/null
+++ b/frontend/editor/src/core/theme/README.md
@@ -0,0 +1,75 @@
+# Theme system (`core/theme/`)
+
+Read this before touching colours or theming anywhere in the frontend.
+
+## TL;DR rules
+
+1. **Never write a raw colour** (`#hex`, `rgb()`, `hsl()`, or a named colour like `red`) in a component, inline style, or stylesheet. Use a token:
+ - `var(--c-…)` — a **semantic** token (preferred: `--c-text`, `--c-surface`, `--c-primary`, …).
+ - `var(--p-…)` — a **palette** primitive, only when no semantic token fits.
+2. **The only file allowed to contain literal colours is `primitives.css`.** If you need a new hue, add it there first, then reference it.
+3. Structural `black` / `white` / `transparent` (shadows, scrims, overlays) are allowed anywhere.
+4. Colours must adapt to light/dark automatically. If you're reaching for a hardcoded colour "just for dark mode", you're doing it wrong — pick the right `--c-*` token.
+5. `task frontend:lint:colors` enforces rules 1–3 **inside `core/theme/`** (see [Linter](#linter)).
+
+## Files
+
+| File | Role |
+|---|---|
+| `primitives.css` | **The palette.** 41 literal colours (`--p-*`) — one neutral ramp (`--p-gray-*` light, `--p-zinc-*` dark) + status hues (blue/green/amber/red). The ONLY place literals live. |
+| `colors.css` | **Semantic tokens** (`--c-*`), mapped from primitives per theme. This is what you should reference. |
+| `compat.css` | **Legacy-name aliases** → `--c-*`. Lets old files keep their `--bg-surface` / `--color-text-1` / etc. names working. Uses `:root:root` so it out-ranks legacy files. |
+| `dimensions.css` | Non-colour tokens: spacing, radius, z-index, type, motion. |
+| `index.css` | Barrel that `@import`s the above. Imported by `ThemeProvider` (and Storybook). |
+| `mantineTheme.ts` | Mantine theme object wiring. |
+
+Two **legacy** colour files still exist outside this folder and are being phased out — prefer `--c-*` over their tokens, and don't add to them:
+`core/styles/theme.css` (editor `--bg-*`/`--text-*` vocab) and `core/tokens/tokens.css` (SUI `--color-*` vocab, gradients, code palette). `compat.css` already overrides their surface/text/border tokens, so most of what they define is dead weight.
+
+## Semantic tokens (`--c-*`)
+
+Reference these, not primitives, wherever possible:
+
+- **Surfaces (elevation):** `--c-bg` (canvas) < `--c-bg-raised` (sidebars/toolbars) < `--c-surface` (cards/modals) < `--c-surface-raised` < `--c-surface-sunken`; plus `--c-input-bg`, `--c-hover`, `--c-active`, `--c-overlay`.
+- **Text:** `--c-text`, `--c-text-muted`, `--c-text-subtle`, `--c-text-on-primary` (foreground on a filled primary).
+- **Borders:** `--c-border`, `--c-border-subtle`, `--c-border-strong`.
+- **Accent:** `--c-primary`, `--c-primary-hover`, `--c-primary-subtle`, and `--c-accent-fg` (see below).
+- **Status:** `--c-success`, `--c-danger`.
+
+## Theme model
+
+The **mode** and the **accent colour** are independent.
+
+- `preferences.theme` is the mode: `light` | `dark` | `system` (System follows the OS). There is no separate "custom" or "midnight" mode any more.
+- Light and dark each have their **own accent**: `preferences.lightPrimary` / `preferences.darkPrimary` (both default `#3b82f6` blue).
+- `ThemeProvider` resolves the mode to a concrete `light`/`dark` base, picks that side's accent, and **always** sets `data-app-theme="custom"` on ``. So the custom-tint blocks in `colors.css` are the only themed blocks that apply — the chosen accent drives every accent **and** a subtle app-wide surface tint. With the default blue the tint is near-neutral.
+- Selection attributes on ``: `data-theme` = `light|dark` (SUI + the tint blocks), `data-mantine-color-scheme` = `light|dark` (Mantine).
+
+### Custom-theme contrast guardrails (`core/utils/customPrimary.ts`)
+
+Because the accent is user-chosen, `deriveAccessiblePrimary(pick, base)` clamps it and injects three vars on ``:
+
+- `--user-primary` → `--c-primary`. Lightness-clamped so it can't collapse into the base (dark floor `L ≥ 0.42`, light ceil `L ≤ 0.6`). Used for **fills**.
+- `--user-primary-on` → `--c-text-on-primary`. White by default; flips to **black only for genuinely light picks** (relative-luminance cutoff `0.62`, so saturated amber/green/cyan keep white text).
+- `--user-accent-fg` → `--c-accent-fg`. The accent tuned as a **foreground** (text/icon on the app surface): forced light on dark bases (`L ≥ 0.62`), dark on light (`L ≤ 0.45`), so accent text never goes dark-on-dark.
+
+**Rule of thumb:** a filled control's background uses `--c-primary` with its label on `--c-text-on-primary`; an accent used **as text/icon on a surface** (nav selection, links, tool-header text) uses `--c-accent-fg`. Because `compat.css` (`:root:root`) wins on specificity, accent-as-text legacy tokens are routed there as `var(--c-accent-fg, var(--c-primary))` — the fallback keeps non-custom builds on the raw primary.
+
+The FAB / logo mark is a deliberate exception: it's pinned to white (`--p-white`), not the on-primary flip — a brand mark, not body text.
+
+## Linter
+
+One file: `editor/scripts/lint/theme-lint.mjs` (no baseline). Run via `task frontend:lint:colors` (part of `task frontend:lint`).
+
+- **Default (blocking):** enforces "literals only in `primitives.css`; everything else in `core/theme/` references tokens; no duplicate primitives." Scope is deliberately just `core/theme/` — the layer this owns, which is clean.
+- **`node theme-lint.mjs contrast`** (task `frontend:contrast`, non-blocking): WCAG contrast report for text-on-surface / on-primary pairs per theme.
+
+App-wide "no hardcoded colours in components" is **not** enforced yet (there are 260+ legacy sites); that's a separate migration. Don't add new hardcoded colours regardless.
+
+## Gotchas
+
+- **`--mantine-*` vars are consumed by Mantine at runtime**, not via our `var()`. A source scan can't see their use — never delete them as "unused", and set them (not raw colours) when overriding Mantine.
+- **`--accent-*` (categorical hues) are used dynamically** via `` `var(--accent-${hue})` `` in `utils/accentColors.ts`. A literal search won't find them — don't treat them as unused.
+- **Tailwind consumes some vars** (`--gray-*`, `--color-*`, `--background`, `--border`) via `editor/tailwind.config.js` using `rgb(var(--x))`. That file is outside the usual scan roots — check it before removing those.
+- **Specificity:** `compat.css` uses `:root:root` (0,2,0) to beat legacy files. If a token you set in `colors.css` (`html[data-app-theme="custom"]`, 0,1,1) isn't winning, a `:root:root` or a `[data-mantine-color-scheme]`-compound block is probably overriding it — set it in `compat.css` or the more specific block.
+- **Adding a colour:** put the literal in `primitives.css`, map it to a `--c-*` in `colors.css` if it's a new semantic role, and reference the `--c-*` (or an existing `compat` alias) from components. Don't skip straight to a `--p-*` in a component unless there's genuinely no semantic fit.
diff --git a/frontend/editor/src/core/theme/colors.css b/frontend/editor/src/core/theme/colors.css
new file mode 100644
index 0000000000..0746292be7
--- /dev/null
+++ b/frontend/editor/src/core/theme/colors.css
@@ -0,0 +1,206 @@
+/* COLORS — canonical semantic core (~21 --c-* tokens) mapped from primitives.css per theme; compat.css aliases legacy names onto these. Editor: always data-app-theme="custom" + data-mantine-color-scheme=light|dark. Portal/Storybook: data-theme=light|dark → LIGHT/MIDNIGHT. */
+/* Surface elevation: --c-bg (canvas) < --c-bg-raised (sidebars) < --c-surface (cards) < --c-surface-raised < --c-surface-sunken; plus --c-input-bg, --c-hover, --c-active, --c-overlay. */
+
+/* ── LIGHT ───────────────────────────────────────────────────────────────── */
+:root,
+[data-theme="light"],
+html[data-app-theme="light"] {
+ --c-bg: var(--p-gray-50);
+ --c-bg-raised: var(--p-white);
+ --c-surface: var(--p-white);
+ --c-surface-raised: var(--p-white);
+ --c-surface-sunken: var(--p-gray-100);
+ --c-input-bg: var(--p-white);
+ --c-hover: var(--p-gray-50);
+ --c-active: var(--p-gray-100);
+ --c-overlay: rgba(0, 0, 0, 0.5);
+
+ --c-text: var(--p-gray-900);
+ --c-text-muted: var(--p-gray-600);
+ --c-text-subtle: var(--p-gray-500);
+ --c-text-on-primary: var(--p-white);
+
+ --c-border: var(--p-gray-250);
+ --c-border-subtle: var(--p-gray-200);
+ --c-border-strong: var(--p-gray-400);
+
+ --c-primary: var(--p-blue-500);
+ --c-primary-hover: var(--p-blue-600);
+ --c-primary-subtle: color-mix(in srgb, var(--p-blue-500) 10%, transparent);
+
+ --c-success: var(--p-green-600);
+ --c-danger: var(--p-red-600);
+}
+
+/* ── MIDNIGHT (original navy) — also the default portal/Storybook dark ────── */
+[data-theme="dark"],
+html[data-app-theme="midnight"] {
+ --c-bg: var(--p-zinc-900);
+ --c-bg-raised: var(--p-zinc-850);
+ --c-surface: var(--p-zinc-800);
+ --c-surface-raised: var(--p-zinc-650);
+ --c-surface-sunken: var(--p-zinc-850);
+ --c-input-bg: var(--p-zinc-650);
+ --c-hover: var(--p-gray-800);
+ --c-active: var(--p-gray-800);
+ --c-overlay: rgba(0, 0, 0, 0.6);
+
+ --c-text: var(--p-zinc-100);
+ --c-text-muted: var(--p-zinc-200);
+ --c-text-subtle: var(--p-zinc-300);
+ --c-text-on-primary: var(--p-white);
+
+ --c-border: var(--p-zinc-650);
+ --c-border-subtle: rgba(255, 255, 255, 0.05);
+ --c-border-strong: var(--p-zinc-500);
+
+ --c-primary: var(--p-blue-500);
+ --c-primary-hover: var(--p-blue-700);
+ --c-primary-subtle: color-mix(in srgb, var(--p-blue-500) 15%, transparent);
+
+ --c-success: var(--p-green-500);
+ --c-danger: var(--p-red-500);
+}
+
+/* ── CUSTOM — --user-primary drives every accent + a surface tint (whole app takes the hue); the [data-accent="default"] blocks at the end opt surfaces back to neutral. LIGHT base here, DARK base next. ── */
+html[data-app-theme="custom"] {
+ --c-primary: var(--user-primary, var(--p-blue-500));
+ --c-primary-hover: color-mix(in srgb, var(--c-primary) 85%, var(--p-black));
+ --c-primary-subtle: color-mix(in srgb, var(--c-primary) 14%, transparent);
+ /* Foreground on the filled primary — contrast-picked in JS (flips white/black). */
+ --c-text-on-primary: var(--user-primary-on, var(--p-white));
+ /* Accent as text/icon on a surface — JS lightness-clamped for legibility; fills/borders keep raw --c-primary. */
+ --c-accent-fg: var(--user-accent-fg, var(--c-primary));
+
+ /* Primary-tinted surfaces (light base). Neutralised by the default override. */
+ --c-bg: color-mix(in srgb, var(--c-primary) 7%, var(--p-gray-50));
+ --c-bg-raised: color-mix(in srgb, var(--c-primary) 4%, var(--p-white));
+ --c-surface: color-mix(in srgb, var(--c-primary) 3%, var(--p-white));
+ --c-surface-raised: color-mix(in srgb, var(--c-primary) 4%, var(--p-white));
+ --c-surface-sunken: color-mix(in srgb, var(--c-primary) 8%, var(--p-gray-100));
+ --c-input-bg: color-mix(in srgb, var(--c-primary) 2%, var(--p-white));
+ --c-hover: color-mix(in srgb, var(--c-primary) 9%, var(--p-gray-50));
+ --c-active: color-mix(in srgb, var(--c-primary) 13%, var(--p-gray-100));
+ --c-border: color-mix(in srgb, var(--c-primary) 14%, var(--p-gray-250));
+ --c-border-subtle: color-mix(in srgb, var(--c-primary) 10%, var(--p-gray-200));
+
+ /* Mantine primary ramp (--color-primary-*) derived from --c-primary. */
+ --color-primary-50: color-mix(in srgb, var(--c-primary) 12%, var(--p-white));
+ --color-primary-100: color-mix(in srgb, var(--c-primary) 20%, var(--p-white));
+ --color-primary-200: color-mix(in srgb, var(--c-primary) 35%, var(--p-white));
+ --color-primary-300: color-mix(in srgb, var(--c-primary) 55%, var(--p-white));
+ --color-primary-400: color-mix(in srgb, var(--c-primary) 78%, var(--p-white));
+ --color-primary-500: var(--c-primary);
+ --color-primary-600: color-mix(in srgb, var(--c-primary) 88%, var(--p-black));
+ --color-primary-700: color-mix(in srgb, var(--c-primary) 74%, var(--p-black));
+ --color-primary-800: color-mix(in srgb, var(--c-primary) 60%, var(--p-black));
+ --color-primary-900: color-mix(in srgb, var(--c-primary) 46%, var(--p-black));
+ --mantine-primary-color-filled: var(--c-primary);
+ --mantine-primary-color-filled-hover: var(--c-primary-hover);
+ /* Mantine "light" variant surfaces (chips, subtle buttons, some panels). */
+ --mantine-primary-color-light: color-mix(in srgb, var(--c-primary) 12%, transparent);
+ --mantine-primary-color-light-hover: color-mix(in srgb, var(--c-primary) 18%, transparent);
+ --mantine-primary-color-light-color: var(--c-primary);
+
+ /* Brand-tint family — re-derived from --c-primary so legacy accents harmonise to the chosen hue. */
+
+ /* Fills / borders — text uses --c-text-on-primary. */
+ --btn-open-file: var(--c-primary);
+ --header-selected-bg: var(--c-primary);
+ --header-selected-fg: var(--c-text-on-primary);
+ --checkbox-checked-bg: var(--c-primary);
+ --card-selected-border: var(--c-primary);
+
+ /* Accent-as-text/icon tokens live in compat.css (:root:root wins); only --icon-files-color is owned here. */
+ --icon-files-color: var(--c-accent-fg);
+
+ /* Subtle surface tints (mix with the base surface so they adapt light/dark) */
+ --tool-header-border: color-mix(in srgb, var(--c-primary) 28%, var(--c-surface));
+ --tool-header-badge-bg: color-mix(in srgb, var(--c-primary) 20%, var(--c-surface));
+ --tooltip-title-bg: color-mix(in srgb, var(--c-primary) 12%, var(--c-surface));
+ --landing-inner-paper-bg: color-mix(in srgb, var(--c-primary) 8%, var(--c-surface));
+ --landing-inner-paper-border: color-mix(in srgb, var(--c-primary) 25%, var(--c-surface));
+ --landing-button-border: color-mix(in srgb, var(--c-primary) 22%, var(--c-surface));
+ --landing-hero-gradient: linear-gradient(
+ 135deg,
+ var(--c-primary) 0%,
+ var(--c-primary-hover) 100%
+ );
+
+ /* Translucent state tints */
+ --color-nav-active: color-mix(in srgb, var(--c-primary) 12%, transparent);
+ --modal-nav-item-active-bg: color-mix(in srgb, var(--c-primary) 10%, transparent);
+ --pdf-selection-bg: color-mix(in srgb, var(--c-primary) 20%, transparent);
+ --pdf-selection-ring: color-mix(in srgb, var(--c-primary) 28%, transparent);
+ --tool-panel-search-bg: color-mix(in srgb, var(--c-primary) 6%, var(--c-surface));
+}
+
+/* ── DARK — editor dark theme: neutral text/borders/icons + accent-tinted surfaces (default override opts out). After :root so it wins for dark. ── */
+html[data-app-theme="custom"][data-mantine-color-scheme="dark"] {
+ /* Neutral text / borders / overlay (not accent-tinted). */
+ --c-text: var(--p-zinc-100);
+ --c-text-muted: var(--p-zinc-200);
+ --c-text-subtle: var(--p-zinc-300);
+ --c-border-strong: var(--p-zinc-500);
+ --c-overlay: rgba(0, 0, 0, 0.6);
+
+ /* Accent-tinted surfaces (dark base). Neutralised by the default override. */
+ --c-bg: color-mix(in srgb, var(--c-primary) 8%, var(--p-zinc-950));
+ --c-bg-raised: color-mix(in srgb, var(--c-primary) 9%, var(--p-zinc-850));
+ --c-surface: color-mix(in srgb, var(--c-primary) 8%, var(--p-zinc-800));
+ --c-surface-raised: color-mix(in srgb, var(--c-primary) 9%, var(--p-zinc-775));
+ --c-surface-sunken: color-mix(in srgb, var(--c-primary) 8%, var(--p-zinc-900));
+ --c-input-bg: color-mix(in srgb, var(--c-primary) 7%, var(--p-zinc-900));
+ --c-hover: color-mix(in srgb, var(--c-primary) 12%, var(--p-zinc-750));
+ --c-active: color-mix(in srgb, var(--c-primary) 15%, var(--p-zinc-700));
+ --c-border: color-mix(in srgb, var(--c-primary) 14%, var(--p-zinc-650));
+ --c-border-subtle: color-mix(in srgb, var(--c-primary) 10%, var(--p-zinc-700));
+
+ /* Dark-tuned status shades (lighter than the light-theme :root values). */
+ --c-success: var(--p-green-500);
+ --c-danger: var(--p-red-400);
+
+ /* Category icon chips → neutral. */
+ --icon-tools-bg: var(--c-surface-raised);
+ --icon-inactive-bg: var(--c-surface-raised);
+ --icon-tools-color: var(--c-text-muted);
+ --icon-files-color: var(--c-text-muted);
+ --icon-inactive-color: var(--c-text-muted);
+ --onboarding-step-inactive: var(--c-border-strong);
+
+ /* Point Mantine's dark ramp at the --c-* surfaces so components follow the theme. */
+ --mantine-color-dark-4: var(--c-border);
+ --mantine-color-dark-5: var(--c-surface-raised);
+ --mantine-color-dark-6: var(--c-surface);
+ --mantine-color-dark-7: var(--c-bg);
+ --mantine-color-body: var(--c-bg);
+ --mantine-color-default: var(--c-surface);
+ --mantine-color-default-hover: var(--c-hover);
+}
+
+/* ── DEFAULT (no tint) — surfaces opt out of the accent tint (neutral white/grey light, zinc dark); --c-primary stays for buttons. Extra [data-accent="default"] beats the tinted blocks. ── */
+html[data-app-theme="custom"][data-accent="default"] {
+ --c-bg: var(--p-gray-50);
+ --c-bg-raised: var(--p-white);
+ --c-surface: var(--p-white);
+ --c-surface-raised: var(--p-white);
+ --c-surface-sunken: var(--p-gray-100);
+ --c-input-bg: var(--p-white);
+ --c-hover: var(--p-gray-50);
+ --c-active: var(--p-gray-100);
+ --c-border: var(--p-gray-250);
+ --c-border-subtle: var(--p-gray-200);
+}
+
+html[data-app-theme="custom"][data-accent="default"][data-mantine-color-scheme="dark"] {
+ --c-bg: var(--p-zinc-950);
+ --c-bg-raised: var(--p-zinc-850);
+ --c-surface: var(--p-zinc-800);
+ --c-surface-raised: var(--p-zinc-775);
+ --c-surface-sunken: var(--p-zinc-900);
+ --c-input-bg: var(--p-zinc-900);
+ --c-hover: var(--p-zinc-750);
+ --c-active: var(--p-zinc-700);
+ --c-border: var(--p-zinc-650);
+ --c-border-subtle: var(--p-zinc-700);
+}
diff --git a/frontend/editor/src/core/theme/compat.css b/frontend/editor/src/core/theme/compat.css
new file mode 100644
index 0000000000..77162317d2
--- /dev/null
+++ b/frontend/editor/src/core/theme/compat.css
@@ -0,0 +1,136 @@
+/* COMPAT — legacy token names (editor --bg-*/--text-* and SUI/portal --color-*) aliased onto the canonical --c-* set so existing files keep working. :root:root (0,2,0) out-ranks the legacy blocks; aliases are theme-agnostic. */
+
+:root:root {
+ /* ── Canvas (darkest / centre) ── */
+ --bg-background: var(--c-bg);
+ --bg-file-manager: var(--c-bg);
+ --color-bg: var(--c-bg);
+
+ /* ── Raised chrome — sidebars, toolbars, headers, side nav. ── */
+ --bg-toolbar: var(--c-bg-raised);
+ --color-sidebar-bg: var(--c-bg-raised);
+ --color-header-bg: var(--c-bg-raised);
+ --modal-nav-bg: var(--c-bg-raised);
+ --unsupported-bar-bg: var(--c-bg-raised);
+
+ /* ── Surfaces — cards, modals, dropdowns, popovers ── */
+ --bg-surface: var(--c-surface);
+ --bg-file-list: var(--c-surface);
+ --modal-content-bg: var(--c-surface);
+ --bulk-panel-bg: var(--c-surface);
+ --bulk-card-bg: var(--c-surface);
+ --api-keys-card-bg: var(--c-surface);
+ --color-surface: var(--c-surface);
+ --color-dropdown-bg: var(--c-surface);
+ --information-text-bg: var(--c-surface);
+
+ --bg-raised: var(--c-surface-raised);
+ --file-card-bg: var(--c-surface-raised);
+ --color-surface-alt: var(--c-surface-raised);
+ --landing-inner-paper-bg: var(--c-surface-raised);
+ --accordion-item-bg: var(--c-surface-raised);
+
+ --bg-muted: var(--c-surface-sunken);
+ --color-bg-muted: var(--c-surface-sunken);
+ --tool-panel-search-bg: var(--c-surface-sunken);
+ --compare-page-label-bg: var(--c-surface-sunken);
+
+ /* ── Inputs ── */
+ --input-bg: var(--c-input-bg);
+ --api-keys-input-bg: var(--c-input-bg);
+
+ /* ── Text ── */
+ --text-primary: var(--c-text);
+ --color-text-1: var(--c-text);
+ --color-header-text: var(--c-text);
+ --color-logo-text: var(--c-text);
+ --tools-text-and-icon-color: var(--c-text);
+
+ --text-secondary: var(--c-text-muted);
+ --color-text-2: var(--c-text-muted);
+ --color-nav-hover-text: var(--c-text-muted);
+
+ --text-muted: var(--c-text-subtle);
+ --color-text-3: var(--c-text-subtle);
+ --color-text-4: var(--c-text-subtle);
+ --color-text-5: var(--c-text-subtle);
+ --color-text-6: var(--c-text-subtle);
+ --color-text-muted: var(--c-text-subtle);
+ --color-nav-text: var(--c-text-subtle);
+ --search-text-and-icon-color: var(--c-text-subtle);
+
+ /* ── Borders ── */
+ --border-default: var(--c-border);
+ --color-border: var(--c-border);
+ --color-border-input: var(--c-border);
+
+ --border-subtle: var(--c-border-subtle);
+ --color-border-light: var(--c-border-subtle);
+ --color-divider: var(--c-border-subtle);
+
+ --border-strong: var(--c-border-strong);
+ --border-hover: var(--c-border-strong);
+ --color-border-hover: var(--c-border-strong);
+
+ /* More borders/dividers that were still navy in neutral dark. */
+ --color-header-border: var(--c-border);
+ --color-sidebar-border: var(--c-border);
+ --color-dropdown-border: var(--c-border);
+ --color-search-border: var(--c-border);
+ --tool-header-border: var(--c-border);
+ --tool-panel-search-border-bottom: var(--c-border);
+ --landing-inner-paper-border: var(--c-border);
+ --unsupported-bar-border: var(--c-border);
+ --api-keys-card-border: var(--c-border);
+ --api-keys-input-border: var(--c-border);
+ --color-usage-track: var(--c-border);
+ --color-sidebar-divider: var(--c-border-subtle);
+ --tool-subcategory-rule-color: var(--c-border-subtle);
+ --modal-header-border: var(--c-border-subtle);
+
+ /* ── More surfaces that were still navy in neutral dark ── */
+ --color-bg-alt: var(--c-bg-raised);
+ --color-search-bg: var(--c-surface-sunken);
+ --color-dropdown-hover: var(--c-hover);
+ --tool-header-badge-bg: var(--c-surface-raised);
+ --color-nav-active: var(--c-primary-subtle);
+ --modal-nav-item-active-bg: var(--c-primary-subtle);
+
+ /* ── More text tokens that were still navy in neutral dark ── */
+ --onboarding-title: var(--c-text);
+ --onboarding-body: var(--c-text-muted);
+ --modal-nav-section-title: var(--c-text-subtle);
+ --tool-subcategory-text-color: var(--c-text-subtle);
+ --dropdown-trigger-text-disabled: var(--c-text-subtle);
+ --tool-header-badge-text: var(--c-accent-fg, var(--c-primary));
+ --tooltip-title-bg: color-mix(in srgb, var(--c-primary) 12%, var(--c-surface));
+ --tooltip-title-color: var(--c-text);
+
+ /* ── Interaction states ── */
+ --hover-bg: var(--c-hover);
+ --color-bg-hover: var(--c-hover);
+ --color-nav-hover: var(--c-hover);
+ --automation-entry-hover-bg: var(--c-hover);
+ --active-bg: var(--c-active);
+
+ /* ── Primary / accent — fills/borders use raw --c-primary; text/icon-on-surface uses --c-accent-fg (falls back to --c-primary outside the custom theme). ── */
+ --color-blue: var(--c-primary);
+ --accent-blue: var(--c-primary);
+ --accent-interactive: var(--c-primary);
+ --btn-open-file: var(--c-primary);
+ --card-selected-border: var(--c-primary);
+ --checkbox-checked-bg: var(--c-primary);
+ --color-blue-dark: var(--c-primary-hover);
+ --modal-nav-item-active: var(--c-accent-fg, var(--c-primary));
+ --color-nav-active-text: var(--c-accent-fg, var(--c-primary));
+ --tool-header-text: var(--c-accent-fg, var(--c-primary));
+ --text-instruction: var(--c-accent-fg, var(--c-primary));
+ --landing-button-color: var(--c-accent-fg, var(--c-primary));
+ /* SUI "blue" accent border/light (secondary buttons, chips) — derived from the primary. */
+ --color-blue-border: color-mix(in srgb, var(--c-primary) 38%, var(--c-surface));
+ --color-blue-light: color-mix(in srgb, var(--c-primary) 12%, var(--c-surface));
+
+ /* ── Status ── */
+ --accent-green: var(--c-success);
+ --accent-red: var(--c-danger);
+}
diff --git a/frontend/editor/src/core/theme/dimensions.css b/frontend/editor/src/core/theme/dimensions.css
new file mode 100644
index 0000000000..8042fcade6
--- /dev/null
+++ b/frontend/editor/src/core/theme/dimensions.css
@@ -0,0 +1,65 @@
+/* DIMENSIONS — single source for every non-colour length (spacing, radius, sizing, borders, z-index, shadow, type, motion). Theme-agnostic. */
+
+:root {
+ /* ── Spacing — 4px grid. Canonical numeric scale + named aliases ── */
+ --space-0: 0;
+ --space-0_5: 0.125rem; /* 2px */
+ --space-1: 0.25rem; /* 4px */
+ --space-1_5: 0.375rem; /* 6px */
+ --space-2: 0.5rem; /* 8px */
+ --space-3: 0.75rem; /* 12px */
+ --space-4: 1rem; /* 16px */
+ --space-5: 1.25rem; /* 20px */
+ --space-6: 1.5rem; /* 24px */
+ --space-8: 2rem; /* 32px */
+
+ /* T-shirt aliases (legacy editor names) → the numeric scale above. */
+ --space-xs: var(--space-1); /* 4px */
+ --space-sm: var(--space-2); /* 8px */
+ --space-md: var(--space-4); /* 16px */
+ --space-lg: var(--space-6); /* 24px */
+ --space-xl: var(--space-8); /* 32px */
+
+ /* ── Radius — one coherent scale (resolves the 8px-vs-6px collision) ── */
+ --radius-xs: 2px;
+ --radius-sm: 4px;
+ --radius-md: 8px;
+ --radius-lg: 12px;
+ --radius-xl: 16px;
+ --radius-pill: 9999px;
+
+ /* ── Layout sizing ── */
+ --footer-height: 2rem;
+ --landing-stack-w: 224px;
+ --landing-stack-h: 176px;
+
+ /* --shadow-* is intentionally NOT defined here: editor (drop shadows) and SUI/portal (inset hairlines) reuse the name with different values. */
+
+ /* ── Typography ── */
+ --font-sans:
+ "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial,
+ sans-serif;
+ --font-mono:
+ "SF Mono", "Fira Code", Menlo, Consolas, "DejaVu Sans Mono", monospace;
+ --font-brand: "Alumni Sans", "Inter", sans-serif;
+
+ --font-weight-medium: 500;
+ --font-weight-semibold: 600;
+
+ /* ── Motion ── */
+ --motion-fast: 0.15s ease;
+ --motion-base: 0.2s cubic-bezier(0.4, 0, 0.2, 1);
+ --motion-slow: 0.3s ease;
+ --motion-enter: 0.22s cubic-bezier(0.4, 0, 0.2, 1);
+ --fullscreen-anim-duration-in: 0.28s;
+ --fullscreen-anim-duration-out: 0.22s;
+
+ /* ── Z-index ladder ── */
+ --z-dropdown: 25;
+ --z-drawer: 50;
+ --z-toast: 200;
+ /* Fullscreen tool-picker surfaces (editor) */
+ --z-fullscreen-icon-svg: 1;
+ --z-toolpicker-star: 1;
+ --z-fullscreen-surface: 1200;
+}
diff --git a/frontend/editor/src/core/theme/index.css b/frontend/editor/src/core/theme/index.css
new file mode 100644
index 0000000000..dc4523c653
--- /dev/null
+++ b/frontend/editor/src/core/theme/index.css
@@ -0,0 +1,5 @@
+/* Consolidated theme entry. Order matters: primitives → dimensions → colors → compat (legacy aliases, :root:root wins). */
+@import "./primitives.css";
+@import "./dimensions.css";
+@import "./colors.css";
+@import "./compat.css";
diff --git a/frontend/editor/src/core/theme/mantineTheme.ts b/frontend/editor/src/core/theme/mantineTheme.ts
index 743142e820..7e88b7b2ef 100644
--- a/frontend/editor/src/core/theme/mantineTheme.ts
+++ b/frontend/editor/src/core/theme/mantineTheme.ts
@@ -58,21 +58,18 @@ const gray: MantineColorsTuple = [
"var(--color-gray-900)",
];
-// Navy-indigo dark scale — replaces Mantine's neutral gray defaults so all
-// dark-mode components (SegmentedControl, inputs, dropdowns, etc.) use the
-// portal palette automatically via --mantine-color-dark-*.
-// dark-0..3 = text/icon shades, dark-4..7 = surface elevations, dark-8..9 = deepest bg.
+// Neutral dark scale (zinc, mirroring --p-zinc-*) replacing Mantine's default gray ramp; colors.css re-points dark-4..7 at the --c-* surfaces. 0..3 text, 4..7 surfaces, 8..9 deepest.
const dark: MantineColorsTuple = [
- "#c2c8e0", // dark-0 — primary text on dark bg
- "#9299b0", // dark-1 — secondary text
- "#6e7898", // dark-2 — muted text / icons
- "#4a5282", // dark-3 — subtle text / dividers
- "#1c2340", // dark-4 — elevated surface / selected bg (e.g. SegmentedControl indicator)
- "#131729", // dark-5 — card / panel surface
- "#0d1020", // dark-6 — toolbar / sidebar bg (e.g. SegmentedControl root)
- "#090b18", // dark-7 — page background (deepest reachable surface)
- "#07091a", // dark-8
- "#050714", // dark-9
+ "#f4f4f5", // dark-0 — primary text on dark bg (zinc-100)
+ "#a1a1aa", // dark-1 — secondary text (zinc-200)
+ "#71717a", // dark-2 — muted text / icons (zinc-300)
+ "#52525b", // dark-3 — subtle text / dividers (zinc-400)
+ "#2a2a2e", // dark-4 — elevated surface / selected bg (zinc-650)
+ "#202023", // dark-5 — card / panel surface (zinc-775)
+ "#18181b", // dark-6 — toolbar / sidebar bg (zinc-800)
+ "#0a0a0b", // dark-7 — page background (zinc-950)
+ "#070708", // dark-8 — deeper than the reachable surfaces
+ "#050506", // dark-9 — deepest
];
export const mantineTheme = createTheme({
diff --git a/frontend/editor/src/core/theme/primitives.css b/frontend/editor/src/core/theme/primitives.css
new file mode 100644
index 0000000000..4cf4be221b
--- /dev/null
+++ b/frontend/editor/src/core/theme/primitives.css
@@ -0,0 +1,45 @@
+/* PRIMITIVES — the raw palette (theme-agnostic). The ONE place literal colours live: neutral ramps (gray=light, zinc=dark) + status hues; colors.css and compat.css reference these. */
+
+:root {
+ --p-white: #ffffff;
+ --p-black: #000000;
+ --p-gray-50: #f9fafb;
+ --p-gray-100: #f3f4f6;
+ --p-gray-150: #eef0f2;
+ --p-gray-200: #e5e7eb;
+ --p-gray-250: #e2e8f0;
+ --p-gray-300: #d1d5db;
+ --p-gray-400: #9ca3af;
+ --p-gray-500: #6b7280;
+ --p-gray-600: #4b5563;
+ --p-gray-700: #374151;
+ --p-gray-800: #1f2937;
+ --p-gray-900: #111827;
+ --p-zinc-950: #0a0a0b;
+ --p-zinc-900: #101012;
+ --p-zinc-850: #131315;
+ --p-zinc-800: #18181b;
+ --p-zinc-775: #202023;
+ --p-zinc-750: #1f1f23;
+ --p-zinc-700: #27272a;
+ --p-zinc-650: #2a2a2e;
+ --p-zinc-600: #333338;
+ --p-zinc-500: #3f3f46;
+ --p-zinc-400: #52525b;
+ --p-zinc-300: #71717a;
+ --p-zinc-200: #a1a1aa;
+ --p-zinc-100: #f4f4f5;
+ --p-blue-400: #60a5fa;
+ --p-blue-500: #3b82f6;
+ --p-blue-600: #2563eb;
+ --p-blue-700: #1d4ed8;
+ --p-green-500: #22c55e;
+ --p-green-600: #16a34a;
+ --p-green-700: #15803d;
+ --p-amber-400: #fbbf24;
+ --p-amber-500: #f59e0b;
+ --p-amber-600: #d97706;
+ --p-red-400: #f87171;
+ --p-red-500: #ef4444;
+ --p-red-600: #dc2626;
+}
diff --git a/frontend/editor/src/core/tokens/Tokens.stories.tsx b/frontend/editor/src/core/tokens/Tokens.stories.tsx
index f83c93517c..7543d7d4d6 100644
--- a/frontend/editor/src/core/tokens/Tokens.stories.tsx
+++ b/frontend/editor/src/core/tokens/Tokens.stories.tsx
@@ -92,8 +92,10 @@ export const Colours: Story = {
void;
+ /** Fires once when picking finishes (pointer up / blur), not every drag frame — for committing expensive updates. */
+ onChangeEnd?: (value: string) => void;
defaultValue?: string;
// Behaviour
@@ -30,6 +32,8 @@ export interface ColorInputProps {
swatches?: string[];
swatchesPerRow?: number;
withPicker?: boolean;
+ /** Restrict the gamut live: every change is run through this (handle sticks at the boundary), receiving the previous in-gamut value so a clamp can preserve hue at achromatic extremes. */
+ clampValue?: (value: string, previous: string) => string;
// Popover escape hatch — only for zIndex / offset overrides in modals
popoverProps?: MantineColorInputProps["popoverProps"];
@@ -44,8 +48,8 @@ export interface ColorInputProps {
required?: boolean;
disabled?: boolean;
readOnly?: boolean;
- onFocus?: React.FocusEventHandler;
- onBlur?: React.FocusEventHandler;
+ onFocus?: FocusEventHandler;
+ onBlur?: FocusEventHandler;
// SUI — invalid applies error styling; FormField renders the message itself.
inputSize?: ColorInputSize;
@@ -57,6 +61,7 @@ type PassthroughProps = Omit<
MantineColorInputProps,
| "value"
| "onChange"
+ | "onChangeEnd"
| "defaultValue"
| "format"
| "swatches"
@@ -90,6 +95,8 @@ export function ColorInput({
format = "hex",
value,
onChange,
+ onChangeEnd,
+ clampValue,
defaultValue,
swatches,
swatchesPerRow,
@@ -108,9 +115,27 @@ export function ColorInput({
onBlur,
}: ColorInputProps) {
const inputRef = useInputAria({ describedBy: ariaDescribedBy });
+
+ // With a clamp, this component owns the displayed value so the picker reflects the constraint live (clamped and mirrored back each frame); without one, value/handlers pass straight through.
+ const [clamped, setClamped] = useState(value ?? defaultValue ?? "");
+ useEffect(() => {
+ if (value !== undefined) setClamped(value);
+ }, [value]);
+
+ const constrainedProps = clampValue
+ ? {
+ value: clamped,
+ onChange: (next: string) => {
+ const c = clampValue(next, clamped);
+ setClamped(c);
+ onChange?.(c);
+ },
+ onChangeEnd: (next: string) => onChangeEnd?.(clampValue(next, clamped)),
+ }
+ : { value, onChange, onChangeEnd };
+
const passthroughProps: PassthroughProps = {
- value,
- onChange,
+ ...constrainedProps,
defaultValue,
format,
swatches,
diff --git a/frontend/editor/src/core/ui/accents.css b/frontend/editor/src/core/ui/accents.css
index 75001f6777..1d47b4ec89 100644
--- a/frontend/editor/src/core/ui/accents.css
+++ b/frontend/editor/src/core/ui/accents.css
@@ -9,6 +9,10 @@
--_bd: var(--color-blue-border);
--_tint: color-mix(in srgb, var(--color-blue) 12%, transparent);
}
+
+html[data-app-theme="custom"] .sui-acc-default {
+ --_on: var(--c-text-on-primary);
+}
.sui-acc-danger {
--_solid: var(--color-red);
--_solid-hover: var(--color-red-dark);
diff --git a/frontend/editor/src/core/utils/customPrimary.ts b/frontend/editor/src/core/utils/customPrimary.ts
new file mode 100644
index 0000000000..d4d242c7d5
--- /dev/null
+++ b/frontend/editor/src/core/utils/customPrimary.ts
@@ -0,0 +1,141 @@
+// Derives an accessible custom-theme primary from a user-chosen colour: a lightness clamp keeps it visible on the base, and the on-primary colour is contrast-picked so filled content stays legible.
+
+import type { ColorScheme } from "@app/constants/theme";
+
+export interface DerivedPrimary {
+ /** Contrast-safe primary to drive --user-primary. Tuned for FILLS. */
+ primary: string;
+ /** Legible foreground for content on the filled primary. */
+ onPrimary: string;
+ /** Accent tuned as a foreground (text/icon) on the app surface — light on dark, dark on light. */
+ accentForeground: string;
+}
+
+interface RGB {
+ r: number;
+ g: number;
+ b: number;
+}
+
+const FALLBACK = "#3b82f6"; // blue-500 — fallback when a stored colour won't parse
+
+function parseHex(input: string): RGB | null {
+ const s = input.trim().replace(/^#/, "");
+ const hex =
+ s.length === 3
+ ? s
+ .split("")
+ .map((c) => c + c)
+ .join("")
+ : s;
+ if (!/^[0-9a-fA-F]{6}$/.test(hex)) return null;
+ return {
+ r: parseInt(hex.slice(0, 2), 16),
+ g: parseInt(hex.slice(2, 4), 16),
+ b: parseInt(hex.slice(4, 6), 16),
+ };
+}
+
+function rgbToHsl({ r, g, b }: RGB): { h: number; s: number; l: number } {
+ const rn = r / 255;
+ const gn = g / 255;
+ const bn = b / 255;
+ const max = Math.max(rn, gn, bn);
+ const min = Math.min(rn, gn, bn);
+ const l = (max + min) / 2;
+ const d = max - min;
+ if (d === 0) return { h: 0, s: 0, l };
+ const s = d / (1 - Math.abs(2 * l - 1));
+ let h: number;
+ if (max === rn) h = ((gn - bn) / d) % 6;
+ else if (max === gn) h = (bn - rn) / d + 2;
+ else h = (rn - gn) / d + 4;
+ h *= 60;
+ if (h < 0) h += 360;
+ return { h, s, l };
+}
+
+function hslToHex(h: number, s: number, l: number): string {
+ const c = (1 - Math.abs(2 * l - 1)) * s;
+ const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
+ const m = l - c / 2;
+ const [r, g, b] =
+ h < 60
+ ? [c, x, 0]
+ : h < 120
+ ? [x, c, 0]
+ : h < 180
+ ? [0, c, x]
+ : h < 240
+ ? [0, x, c]
+ : h < 300
+ ? [x, 0, c]
+ : [c, 0, x];
+ const to = (v: number) =>
+ Math.round((v + m) * 255)
+ .toString(16)
+ .padStart(2, "0");
+ return `#${to(r)}${to(g)}${to(b)}`;
+}
+
+function relLuminance({ r, g, b }: RGB): number {
+ const lin = (v: number) => {
+ const c = v / 255;
+ return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
+ };
+ return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);
+}
+
+// Relative-luminance cutoff above which on-primary flips white→black (tuned high so saturated brand hues keep white text).
+const ON_PRIMARY_DARK_CUTOFF = 0.62;
+
+// Lightness floor (dark base) / ceiling (light base) so an accent stays distinguishable from the base surfaces.
+const DARK_MIN_L = 0.42;
+const LIGHT_MAX_L = 0.6;
+
+// Stricter lightness bounds for an accent used as foreground text/icon (light on dark, dark on light).
+const DARK_FG_MIN_L = 0.62;
+const LIGHT_FG_MAX_L = 0.45;
+
+// Saturation/lightness bounds so a stored accent stays a real, visible hue (never white/grey/black).
+const ACCENT_MIN_S = 0.35;
+const ACCENT_MIN_L = 0.32;
+const ACCENT_MAX_L = 0.68;
+
+// Clamp a pick into gamut; at achromatic extremes (no hue in hex) borrow the hue from `previous` so black/white/grey give the darkest/lightest/least-saturated version of the working hue, not an unrelated colour.
+export function clampAccentChoice(input: string, previous?: string): string {
+ const rgb = parseHex(input);
+ if (!rgb) return FALLBACK;
+ const { h, s, l } = rgbToHsl(rgb);
+ const prevRgb = previous ? parseHex(previous) : null;
+ const hue = s < 1e-6 && prevRgb ? rgbToHsl(prevRgb).h : h;
+ return hslToHex(
+ hue,
+ Math.max(s, ACCENT_MIN_S),
+ Math.min(Math.max(l, ACCENT_MIN_L), ACCENT_MAX_L),
+ );
+}
+
+export function deriveAccessiblePrimary(
+ input: string,
+ base: ColorScheme,
+): DerivedPrimary {
+ const rgb = parseHex(input) ?? parseHex(FALLBACK)!;
+ const { h, s } = rgbToHsl(rgb);
+ let { l } = rgbToHsl(rgb);
+ if (base === "dark" && l < DARK_MIN_L) l = DARK_MIN_L;
+ if (base === "light" && l > LIGHT_MAX_L) l = LIGHT_MAX_L;
+
+ const primary = hslToHex(h, s, l);
+ const lum = relLuminance(parseHex(primary)!);
+ // White by default; flip to black only once the colour is genuinely light.
+ const onPrimary = lum > ON_PRIMARY_DARK_CUTOFF ? "#000000" : "#ffffff";
+
+ // Foreground accent: same hue, lightness pushed so it stays legible on the base surface.
+ let fgL = rgbToHsl(rgb).l;
+ if (base === "dark") fgL = Math.max(fgL, DARK_FG_MIN_L);
+ else fgL = Math.min(fgL, LIGHT_FG_MAX_L);
+ const accentForeground = hslToHex(h, s, fgL);
+
+ return { primary, onPrimary, accentForeground };
+}
diff --git a/frontend/editor/src/portal/components/EditorStatusCard.css b/frontend/editor/src/portal/components/EditorStatusCard.css
index 83426bae91..557e47ff61 100644
--- a/frontend/editor/src/portal/components/EditorStatusCard.css
+++ b/frontend/editor/src/portal/components/EditorStatusCard.css
@@ -15,7 +15,7 @@
align-items: center;
gap: 1.25rem;
padding: 1rem 1.25rem;
- background: #16213e;
+ background: color-mix(in srgb, var(--c-primary) 22%, var(--p-zinc-950));
}
.portal-editor-hero__logo {
diff --git a/frontend/editor/src/portal/components/WelcomeBanner.css b/frontend/editor/src/portal/components/WelcomeBanner.css
index 5c63462754..c2e8cd40bc 100644
--- a/frontend/editor/src/portal/components/WelcomeBanner.css
+++ b/frontend/editor/src/portal/components/WelcomeBanner.css
@@ -18,7 +18,7 @@
gap: 1rem;
flex-wrap: wrap;
padding: 0.875rem 1.25rem;
- background: #16213e;
+ background: color-mix(in srgb, var(--c-primary) 22%, var(--p-zinc-950));
}
.portal-welcome__brand {
diff --git a/frontend/editor/src/portal/contexts/ThemeContext.tsx b/frontend/editor/src/portal/contexts/ThemeContext.tsx
index ad979f5e91..5b0dcf3d31 100644
--- a/frontend/editor/src/portal/contexts/ThemeContext.tsx
+++ b/frontend/editor/src/portal/contexts/ThemeContext.tsx
@@ -6,6 +6,12 @@ import {
useState,
type ReactNode,
} from "react";
+import { preferencesService } from "@app/services/preferencesService";
+import {
+ getSystemTheme,
+ resolveColorScheme,
+ type ThemeMode,
+} from "@app/constants/theme";
export type Theme = "light" | "dark";
@@ -17,33 +23,43 @@ interface ThemeContextValue {
const ThemeContext = createContext(null);
-const STORAGE_KEY = "stirling.portal.theme";
-
-function readInitialTheme(): Theme {
- if (typeof window === "undefined") return "light";
- const stored = window.localStorage.getItem(STORAGE_KEY);
- if (stored === "light" || stored === "dark") return stored;
- return window.matchMedia("(prefers-color-scheme: dark)").matches
- ? "dark"
- : "light";
-}
-
export function ThemeProvider({ children }: { children: ReactNode }) {
- const [theme, setTheme] = useState(readInitialTheme);
+ // Mirror the shared theme MODE; re-read on cross-tab storage changes and,
+ // while on "system", on OS scheme changes.
+ const [mode, setMode] = useState(() =>
+ preferencesService.getPreference("theme"),
+ );
+ const [systemScheme, setSystemScheme] = useState(getSystemTheme);
+
+ useEffect(() => {
+ const syncMode = () => setMode(preferencesService.getPreference("theme"));
+ window.addEventListener("storage", syncMode);
+ const media = window.matchMedia?.("(prefers-color-scheme: dark)");
+ const syncSystem = () => setSystemScheme(media?.matches ? "dark" : "light");
+ media?.addEventListener("change", syncSystem);
+ return () => {
+ window.removeEventListener("storage", syncMode);
+ media?.removeEventListener("change", syncSystem);
+ };
+ }, []);
+
+ const theme = resolveColorScheme(mode, systemScheme);
useEffect(() => {
document.documentElement.setAttribute("data-theme", theme);
- window.localStorage.setItem(STORAGE_KEY, theme);
}, [theme]);
- const value = useMemo(
- () => ({
+ const value = useMemo(() => {
+ const setTheme = (next: Theme) => {
+ preferencesService.setPreference("theme", next);
+ setMode(next);
+ };
+ return {
theme,
setTheme,
- toggle: () => setTheme((t) => (t === "light" ? "dark" : "light")),
- }),
- [theme],
- );
+ toggle: () => setTheme(theme === "light" ? "dark" : "light"),
+ };
+ }, [theme]);
return (
{children}
diff --git a/frontend/editor/src/proprietary/auth/ui/auth-theme.css b/frontend/editor/src/proprietary/auth/ui/auth-theme.css
index f62691f518..b13e19d348 100644
--- a/frontend/editor/src/proprietary/auth/ui/auth-theme.css
+++ b/frontend/editor/src/proprietary/auth/ui/auth-theme.css
@@ -2,25 +2,25 @@
:root {
/* Auth page colors (light mode) */
- --auth-bg-color-light-only: #f3f4f6;
+ --auth-bg-color-light-only: var(--p-gray-100);
--auth-card-bg: #ffffff;
--auth-card-bg-light-only: #ffffff;
- --auth-label-text-light-only: #374151;
- --auth-input-border-light-only: #d1d5db;
+ --auth-label-text-light-only: var(--p-gray-700);
+ --auth-input-border-light-only: var(--p-gray-300);
--auth-input-bg-light-only: #ffffff;
- --auth-input-text-light-only: #111827;
- --auth-border-focus-light-only: #3b82f6;
- --auth-focus-ring-light-only: rgba(59, 130, 246, 0.1);
- --auth-button-bg-light-only: #af3434;
+ --auth-input-text-light-only: var(--p-gray-900);
+ --auth-border-focus-light-only: var(--p-blue-500);
+ --auth-focus-ring-light-only: color-mix(in srgb, var(--p-blue-500) 10%, transparent);
+ --auth-button-bg-light-only: var(--p-red-600);
--auth-button-text-light-only: #ffffff;
- --auth-magic-button-bg-light-only: #e5e7eb;
- --auth-magic-button-text-light-only: #374151;
- --auth-text-primary-light-only: #111827;
- --auth-text-secondary-light-only: #6b7280;
+ --auth-magic-button-bg-light-only: var(--p-gray-200);
+ --auth-magic-button-text-light-only: var(--p-gray-700);
+ --auth-text-primary-light-only: var(--p-gray-900);
+ --auth-text-secondary-light-only: var(--p-gray-500);
--text-divider-rule-rgb-light: 229, 231, 235;
--text-divider-label-rgb-light: 156, 163, 175;
- --tool-subcategory-rule-color-light: #e5e7eb;
- --tool-subcategory-text-color-light: #9ca3af;
+ --tool-subcategory-rule-color-light: var(--p-gray-200);
+ --tool-subcategory-text-color-light: var(--p-gray-400);
}
[data-mantine-color-scheme="dark"] {
@@ -31,9 +31,9 @@
--auth-input-border-light-only: var(--border-default);
--auth-input-bg-light-only: var(--bg-raised);
--auth-input-text-light-only: var(--text-primary);
- --auth-border-focus-light-only: #3b82f6;
- --auth-focus-ring-light-only: rgba(59, 130, 246, 0.2);
- --auth-button-bg-light-only: #af3434;
+ --auth-border-focus-light-only: var(--p-blue-500);
+ --auth-focus-ring-light-only: color-mix(in srgb, var(--p-blue-500) 20%, transparent);
+ --auth-button-bg-light-only: var(--p-red-600);
--auth-button-text-light-only: #ffffff;
--auth-magic-button-bg-light-only: var(--bg-raised);
--auth-magic-button-text-light-only: var(--text-primary);
diff --git a/frontend/editor/src/saas/styles/saas-theme.css b/frontend/editor/src/saas/styles/saas-theme.css
index 654fe5fe17..e60670ece1 100644
--- a/frontend/editor/src/saas/styles/saas-theme.css
+++ b/frontend/editor/src/saas/styles/saas-theme.css
@@ -2,101 +2,93 @@
:root {
/* Orange scale (used for warning toasts) */
- --color-orange-50: #fff4ed;
- --color-orange-100: #ffe1cc;
- --color-orange-200: #ffb089;
- --color-orange-300: #ff7a45;
- --color-orange-400: #d84a1b;
+ --color-orange-50: var(--p-amber-400);
+ --color-orange-100: var(--p-amber-400);
+ --color-orange-200: var(--p-red-400);
+ --color-orange-300: var(--p-red-400);
+ --color-orange-400: var(--p-red-600);
/* Amber scale (trial/warning emphasis) */
- --color-amber-50: #fffbeb;
- --color-amber-100: #fef3c7;
- --color-amber-200: #fde68a;
- --color-amber-300: #fcd34d;
- --color-amber-400: #fbbf24;
- --color-amber-500: #f59e0b;
- --color-amber-600: #d97706;
- --color-amber-700: #b45309;
- --color-amber-800: #92400e;
- --color-amber-900: #78350f;
+ --color-amber-50: var(--p-amber-400);
+ --color-amber-100: var(--p-amber-400);
+ --color-amber-200: var(--p-amber-400);
+ --color-amber-300: var(--p-amber-400);
+ --color-amber-400: var(--p-amber-400);
+ --color-amber-500: var(--p-amber-500);
+ --color-amber-600: var(--p-amber-600);
+ --color-amber-700: var(--p-amber-600);
+ --color-amber-800: var(--p-amber-600);
+ --color-amber-900: var(--p-amber-600);
/* Subcategory / divider vars (light) */
- --tool-subcategory-text-color-light: #9ca3af;
- --tool-subcategory-rule-color-light: #e5e7eb;
- --text-divider-rule-color: var(--color-gray-200);
- --text-divider-label-color: var(--color-gray-400);
- --text-divider-rule-color-light: var(--color-gray-200);
- --text-divider-label-color-light: var(--color-gray-400);
+ --tool-subcategory-text-color-light: var(--p-gray-400);
+ --tool-subcategory-rule-color-light: var(--p-gray-200);
/* Auth color vars */
- --auth-input-bg: #f9fafb;
- --auth-input-border: #e5e7eb;
- --auth-input-text: #1f2937;
- --auth-label-text: #2b3230;
- --auth-button-bg: #af3434;
+ --auth-input-bg: var(--p-gray-50);
+ --auth-input-border: var(--p-gray-200);
+ --auth-input-text: var(--p-gray-800);
+ --auth-label-text: var(--p-zinc-650);
+ --auth-button-bg: var(--p-red-600);
--auth-button-text: #ffffff;
- --auth-magic-button-bg: #af3434;
+ --auth-magic-button-bg: var(--p-red-600);
--auth-magic-button-text: #ffffff;
/* Light-only auth colors (no dark mode equivalents) used for login/signup */
- --auth-input-bg-light-only: #f9fafb;
- --auth-input-border-light-only: #e5e7eb;
- --auth-input-text-light-only: #1f2937;
- --auth-label-text-light-only: #2b3230;
- --auth-button-bg-light-only: #af3434;
+ --auth-input-bg-light-only: var(--p-gray-50);
+ --auth-input-border-light-only: var(--p-gray-200);
+ --auth-input-text-light-only: var(--p-gray-800);
+ --auth-label-text-light-only: var(--p-zinc-650);
+ --auth-button-bg-light-only: var(--p-red-600);
--auth-button-text-light-only: #ffffff;
- --auth-magic-button-bg-light-only: #af3434;
+ --auth-magic-button-bg-light-only: var(--p-red-600);
--auth-magic-button-text-light-only: #ffffff;
--auth-bg-color-light-only: #ffffff;
--auth-card-bg-light-only: #ffffff;
- --auth-text-primary-light-only: #2b3230;
- --auth-text-secondary-light-only: #1f2937;
- --auth-border-color-light-only: #e5e7eb;
- --auth-border-focus-light-only: #cbd5e1;
- --auth-focus-ring-light-only: rgba(59, 130, 246, 0.15);
+ --auth-text-primary-light-only: var(--p-zinc-650);
+ --auth-text-secondary-light-only: var(--p-gray-800);
+ --auth-border-focus-light-only: var(--p-gray-300);
+ --auth-focus-ring-light-only: color-mix(in srgb, var(--p-blue-500) 15%, transparent);
/* App Config Modal colors (light mode) */
- --modal-nav-bg: #f5f6f8;
- --modal-nav-section-title: #6b7280;
- --modal-nav-item: #374151;
- --modal-nav-item-active: #0a8bff;
- --modal-nav-item-active-bg: rgba(10, 139, 255, 0.08);
+ --modal-nav-bg: var(--p-gray-100);
+ --modal-nav-section-title: var(--p-gray-500);
+ --modal-nav-item: var(--p-gray-700);
+ --modal-nav-item-active: var(--p-blue-500);
+ --modal-nav-item-active-bg: color-mix(in srgb, var(--p-blue-500) 8%, transparent);
--modal-content-bg: #ffffff;
--modal-header-border: rgba(0, 0, 0, 0.06);
/* API usage progress bar colors (light mode) */
- --usage-weekly-active: #3b82f6;
- --usage-bought-active: #14b8a6;
- --usage-total-used: #3b82f6;
- --usage-inactive: #e5e7eb;
+ --usage-inactive: var(--p-gray-200);
/* API Keys section colors (light mode) */
--api-keys-card-bg: #ffffff;
- --api-keys-card-border: #e0e0e0;
+ --api-keys-card-border: var(--p-gray-200);
--api-keys-card-shadow: rgba(0, 0, 0, 0.06);
- --api-keys-input-bg: #f8f8f8;
- --api-keys-input-border: #e0e0e0;
+ --api-keys-input-bg: var(--p-gray-50);
+ --api-keys-input-border: var(--p-gray-200);
}
[data-mantine-color-scheme="dark"] {
/* Compare highlight colors (dark mode) */
- --spdf-compare-removed-bg: rgba(255, 107, 107, 0.45);
- --spdf-compare-added-bg: rgba(81, 207, 102, 0.35);
- --spdf-compare-removed-badge-bg: rgba(255, 59, 48, 0.15);
+ --spdf-compare-removed-bg: color-mix(in srgb, var(--p-red-400) 45%, transparent);
+ --spdf-compare-added-bg: color-mix(in srgb, var(--p-green-500) 35%, transparent);
+ --spdf-compare-removed-badge-bg: color-mix(in srgb, var(--p-red-500) 15%, transparent);
--spdf-compare-removed-badge-fg: var(--color-red-500);
- --spdf-compare-added-badge-bg: rgba(52, 199, 89, 0.18);
+ --spdf-compare-added-badge-bg: color-mix(in srgb, var(--p-green-500) 18%, transparent);
--spdf-compare-added-badge-fg: var(--color-green-500);
- --spdf-compare-inline-removed-bg: rgba(255, 59, 48, 0.25);
- --spdf-compare-inline-added-bg: rgba(52, 199, 89, 0.25);
- --compare-page-label-bg: #0d1020;
- --compare-page-label-fg: #c2c8e0;
+ --spdf-compare-inline-removed-bg: color-mix(in srgb, var(--p-red-500) 25%, transparent);
+ --spdf-compare-inline-added-bg: color-mix(in srgb, var(--p-green-500) 25%, transparent);
+ --compare-page-label-bg: var(--p-zinc-850);
+ --compare-page-label-fg: var(--p-gray-300);
/* Orange scale (dark mode mirrors light values to match UI) */
- --color-orange-50: #fff4ed;
- --color-orange-100: #ffe1cc;
- --color-orange-200: #ffb089;
- --color-orange-300: #ff7a45;
- --color-orange-400: #d84a1b;
+ --color-orange-50: var(--p-amber-400);
+ --color-orange-100: var(--p-amber-400);
+ --color-orange-200: var(--p-red-400);
+ --color-orange-300: var(--p-red-400);
+ --color-orange-400: var(--p-red-600);
/* Auth page colors (dark mode) — mirror proprietary so the auth card themes dark */
--auth-bg-color-light-only: var(--bg-muted);
@@ -106,9 +98,9 @@
--auth-input-border-light-only: var(--border-default);
--auth-input-bg-light-only: var(--bg-raised);
--auth-input-text-light-only: var(--text-primary);
- --auth-border-focus-light-only: #3b82f6;
- --auth-focus-ring-light-only: rgba(59, 130, 246, 0.2);
- --auth-button-bg-light-only: #af3434;
+ --auth-border-focus-light-only: var(--p-blue-500);
+ --auth-focus-ring-light-only: color-mix(in srgb, var(--p-blue-500) 20%, transparent);
+ --auth-button-bg-light-only: var(--p-red-600);
--auth-button-text-light-only: #ffffff;
--auth-magic-button-bg-light-only: var(--bg-raised);
--auth-magic-button-text-light-only: var(--text-primary);
@@ -116,31 +108,26 @@
--auth-text-secondary-light-only: var(--text-secondary);
--text-divider-rule-rgb-light: 229, 231, 235;
--text-divider-label-rgb-light: 156, 163, 175;
- --tool-subcategory-rule-color-light: #e5e7eb;
- --tool-subcategory-text-color-light: #9ca3af;
+ --tool-subcategory-rule-color-light: var(--p-gray-200);
+ --tool-subcategory-text-color-light: var(--p-gray-400);
/* API usage progress bar colors (dark mode) */
- --usage-weekly-active: #4f8ef5;
- --usage-bought-active: #34d399;
- --usage-total-used: #e8eaf6;
- --usage-inactive: #1c2340;
+ --usage-inactive: var(--p-zinc-650);
/* API Keys section colors (dark mode) */
- --api-keys-card-bg: #131729;
- --api-keys-card-border: #1c2340;
+ --api-keys-card-bg: var(--p-zinc-800);
+ --api-keys-card-border: var(--p-zinc-650);
--api-keys-card-shadow: none;
- --api-keys-input-bg: #0d1020;
- --api-keys-input-border: #1c2340;
+ --api-keys-input-bg: var(--p-zinc-850);
+ --api-keys-input-border: var(--p-zinc-650);
- --text-divider-rule-color: var(--tool-subcategory-rule-color);
- --text-divider-label-color: var(--text-muted);
/* App Config Modal colors (dark mode) */
- --modal-nav-bg: #0d1020;
- --modal-nav-section-title: #5b6280;
- --modal-nav-item: #c2c8e0;
- --modal-nav-item-active: #4f8ef5;
- --modal-nav-item-active-bg: rgba(79, 142, 245, 0.15);
- --modal-content-bg: #131729;
+ --modal-nav-bg: var(--p-zinc-850);
+ --modal-nav-section-title: var(--p-zinc-300);
+ --modal-nav-item: var(--p-gray-300);
+ --modal-nav-item-active: var(--p-blue-500);
+ --modal-nav-item-active-bg: color-mix(in srgb, var(--p-blue-500) 15%, transparent);
+ --modal-content-bg: var(--p-zinc-800);
--modal-header-border: rgba(255, 255, 255, 0.05);
}
diff --git a/frontend/eslint.config.mjs b/frontend/eslint.config.mjs
index 9793c60ac8..6a65e970d9 100644
--- a/frontend/eslint.config.mjs
+++ b/frontend/eslint.config.mjs
@@ -323,5 +323,10 @@ export default defineConfig(
...globals.node,
},
},
+ rules: {
+ // Standalone node scripts have no bundler, so workspace aliases (@app/*)
+ // don't resolve — relative imports are correct here.
+ "no-restricted-imports": "off",
+ },
},
);