diff --git a/src/components/layout/AppLayout.vue b/src/components/layout/AppLayout.vue
index a477f85..d32127c 100644
--- a/src/components/layout/AppLayout.vue
+++ b/src/components/layout/AppLayout.vue
@@ -3,6 +3,9 @@ import { RouterView } from 'vue-router'
import { Toaster } from '@/components/ui/sonner'
import SiteHeader from './SiteHeader.vue'
import SiteFooter from './SiteFooter.vue'
+import PaletteSwitcher from './PaletteSwitcher.vue'
+
+const isDev = import.meta.env.DEV
@@ -13,5 +16,6 @@ import SiteFooter from './SiteFooter.vue'
+
diff --git a/src/components/layout/PaletteSwitcher.vue b/src/components/layout/PaletteSwitcher.vue
new file mode 100644
index 0000000..7e48aaf
--- /dev/null
+++ b/src/components/layout/PaletteSwitcher.vue
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+ Palette (dev only)
+
+
+
+ {{ p.label }}
+ {{ p.description }}
+
+
+
+
diff --git a/src/composables/usePalette.ts b/src/composables/usePalette.ts
new file mode 100644
index 0000000..85ab782
--- /dev/null
+++ b/src/composables/usePalette.ts
@@ -0,0 +1,54 @@
+import { ref, watchEffect } from 'vue'
+
+export const PALETTES = [
+ {
+ slug: 'stone-warm',
+ label: 'Stone Warm',
+ description: 'Bone background, matte-black ink. Gallery-quiet.',
+ },
+ {
+ slug: 'desert-clay',
+ label: 'Desert Clay',
+ description: 'Warm cream with terracotta undertone. Evening light.',
+ },
+ {
+ slug: 'forest-ash',
+ label: 'Forest & Ash',
+ description: 'Sage-tinted neutral, mossy accent. Nature integrated.',
+ },
+ {
+ slug: 'charcoal-cream',
+ label: 'Charcoal & Cream',
+ description: 'High-contrast monochrome. Moody architectural.',
+ },
+] as const
+
+export type PaletteSlug = (typeof PALETTES)[number]['slug']
+
+const STORAGE_KEY = 'ewd:palette'
+const DEFAULT: PaletteSlug = 'stone-warm'
+
+function initial(): PaletteSlug {
+ if (typeof window === 'undefined') return DEFAULT
+ const url = new URL(window.location.href)
+ const fromQuery = url.searchParams.get('palette') as PaletteSlug | null
+ if (fromQuery && PALETTES.some((p) => p.slug === fromQuery)) return fromQuery
+ const stored = window.localStorage.getItem(STORAGE_KEY) as PaletteSlug | null
+ if (stored && PALETTES.some((p) => p.slug === stored)) return stored
+ return DEFAULT
+}
+
+const palette = ref(initial())
+
+watchEffect(() => {
+ if (typeof document === 'undefined') return
+ document.documentElement.setAttribute('data-palette', palette.value)
+ window.localStorage.setItem(STORAGE_KEY, palette.value)
+})
+
+export function usePalette() {
+ function set(slug: PaletteSlug) {
+ palette.value = slug
+ }
+ return { palette, set }
+}
diff --git a/src/style.css b/src/style.css
index 669819b..c2630ae 100644
--- a/src/style.css
+++ b/src/style.css
@@ -1,53 +1,9 @@
@import 'tailwindcss';
@import 'tw-animate-css';
+@import './themes/index.css';
@custom-variant dark (&:is(.dark *));
-:root {
- --background: oklch(0.985 0.005 80);
- --foreground: oklch(0.2 0.012 60);
- --card: oklch(0.985 0.005 80);
- --card-foreground: oklch(0.2 0.012 60);
- --popover: oklch(0.985 0.005 80);
- --popover-foreground: oklch(0.2 0.012 60);
- --primary: oklch(0.18 0.008 60);
- --primary-foreground: oklch(0.97 0.008 80);
- --secondary: oklch(0.94 0.008 75);
- --secondary-foreground: oklch(0.2 0.012 60);
- --muted: oklch(0.94 0.008 75);
- --muted-foreground: oklch(0.5 0.015 60);
- --accent: oklch(0.92 0.018 65);
- --accent-foreground: oklch(0.2 0.012 60);
- --destructive: oklch(0.55 0.18 25);
- --destructive-foreground: oklch(0.97 0.008 80);
- --border: oklch(0.88 0.01 70);
- --input: oklch(0.88 0.01 70);
- --ring: oklch(0.2 0.012 60);
- --radius: 0.25rem;
-}
-
-.dark {
- --background: oklch(0.16 0.008 60);
- --foreground: oklch(0.95 0.01 80);
- --card: oklch(0.18 0.008 60);
- --card-foreground: oklch(0.95 0.01 80);
- --popover: oklch(0.18 0.008 60);
- --popover-foreground: oklch(0.95 0.01 80);
- --primary: oklch(0.95 0.01 80);
- --primary-foreground: oklch(0.18 0.008 60);
- --secondary: oklch(0.24 0.012 60);
- --secondary-foreground: oklch(0.95 0.01 80);
- --muted: oklch(0.24 0.012 60);
- --muted-foreground: oklch(0.7 0.012 70);
- --accent: oklch(0.3 0.022 65);
- --accent-foreground: oklch(0.95 0.01 80);
- --destructive: oklch(0.45 0.18 25);
- --destructive-foreground: oklch(0.95 0.01 80);
- --border: oklch(0.28 0.012 60);
- --input: oklch(0.28 0.012 60);
- --ring: oklch(0.85 0.012 75);
-}
-
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
diff --git a/src/themes/charcoal-cream.css b/src/themes/charcoal-cream.css
new file mode 100644
index 0000000..6faa89d
--- /dev/null
+++ b/src/themes/charcoal-cream.css
@@ -0,0 +1,50 @@
+/* Palette: Charcoal & Cream
+ * High-contrast monochrome. Warm cream background, near-black warm
+ * charcoal ink, deep-bronze accent for the active states. The
+ * Asheville mood by default — moody, architectural, restrained.
+ * In dark mode it goes deeper still.
+ */
+
+:root[data-palette='charcoal-cream'] {
+ --background: oklch(0.95 0.013 75);
+ --foreground: oklch(0.16 0.005 60);
+ --card: oklch(0.95 0.013 75);
+ --card-foreground: oklch(0.16 0.005 60);
+ --popover: oklch(0.95 0.013 75);
+ --popover-foreground: oklch(0.16 0.005 60);
+ --primary: oklch(0.16 0.005 60);
+ --primary-foreground: oklch(0.95 0.013 75);
+ --secondary: oklch(0.9 0.014 75);
+ --secondary-foreground: oklch(0.16 0.005 60);
+ --muted: oklch(0.9 0.014 75);
+ --muted-foreground: oklch(0.45 0.012 60);
+ --accent: oklch(0.86 0.025 70);
+ --accent-foreground: oklch(0.16 0.005 60);
+ --destructive: oklch(0.55 0.18 25);
+ --destructive-foreground: oklch(0.95 0.013 75);
+ --border: oklch(0.82 0.014 70);
+ --input: oklch(0.82 0.014 70);
+ --ring: oklch(0.16 0.005 60);
+}
+
+:root[data-palette='charcoal-cream'].dark {
+ --background: oklch(0.12 0.004 60);
+ --foreground: oklch(0.94 0.013 75);
+ --card: oklch(0.14 0.004 60);
+ --card-foreground: oklch(0.94 0.013 75);
+ --popover: oklch(0.14 0.004 60);
+ --popover-foreground: oklch(0.94 0.013 75);
+ --primary: oklch(0.94 0.013 75);
+ --primary-foreground: oklch(0.14 0.004 60);
+ --secondary: oklch(0.2 0.006 60);
+ --secondary-foreground: oklch(0.94 0.013 75);
+ --muted: oklch(0.2 0.006 60);
+ --muted-foreground: oklch(0.68 0.012 70);
+ --accent: oklch(0.26 0.015 65);
+ --accent-foreground: oklch(0.94 0.013 75);
+ --destructive: oklch(0.45 0.18 25);
+ --destructive-foreground: oklch(0.94 0.013 75);
+ --border: oklch(0.24 0.008 60);
+ --input: oklch(0.24 0.008 60);
+ --ring: oklch(0.85 0.014 75);
+}
diff --git a/src/themes/desert-clay.css b/src/themes/desert-clay.css
new file mode 100644
index 0000000..b95773e
--- /dev/null
+++ b/src/themes/desert-clay.css
@@ -0,0 +1,49 @@
+/* Palette: Desert Clay
+ * Warm creamy background with a terracotta undertone, espresso ink,
+ * burnt-clay accent. Matches Boulder's reclaimed wood / warm pendant
+ * mood. Reads as evening light, not gallery white.
+ */
+
+:root[data-palette='desert-clay'] {
+ --background: oklch(0.97 0.012 65);
+ --foreground: oklch(0.22 0.018 45);
+ --card: oklch(0.97 0.012 65);
+ --card-foreground: oklch(0.22 0.018 45);
+ --popover: oklch(0.97 0.012 65);
+ --popover-foreground: oklch(0.22 0.018 45);
+ --primary: oklch(0.4 0.07 35);
+ --primary-foreground: oklch(0.97 0.012 65);
+ --secondary: oklch(0.92 0.018 60);
+ --secondary-foreground: oklch(0.22 0.018 45);
+ --muted: oklch(0.93 0.014 60);
+ --muted-foreground: oklch(0.5 0.025 45);
+ --accent: oklch(0.86 0.04 45);
+ --accent-foreground: oklch(0.22 0.018 45);
+ --destructive: oklch(0.55 0.18 25);
+ --destructive-foreground: oklch(0.97 0.012 65);
+ --border: oklch(0.86 0.018 55);
+ --input: oklch(0.86 0.018 55);
+ --ring: oklch(0.4 0.07 35);
+}
+
+:root[data-palette='desert-clay'].dark {
+ --background: oklch(0.17 0.014 40);
+ --foreground: oklch(0.94 0.014 70);
+ --card: oklch(0.2 0.014 40);
+ --card-foreground: oklch(0.94 0.014 70);
+ --popover: oklch(0.2 0.014 40);
+ --popover-foreground: oklch(0.94 0.014 70);
+ --primary: oklch(0.85 0.05 50);
+ --primary-foreground: oklch(0.2 0.014 40);
+ --secondary: oklch(0.27 0.018 45);
+ --secondary-foreground: oklch(0.94 0.014 70);
+ --muted: oklch(0.27 0.018 45);
+ --muted-foreground: oklch(0.7 0.02 55);
+ --accent: oklch(0.34 0.045 45);
+ --accent-foreground: oklch(0.94 0.014 70);
+ --destructive: oklch(0.45 0.18 25);
+ --destructive-foreground: oklch(0.94 0.014 70);
+ --border: oklch(0.3 0.018 45);
+ --input: oklch(0.3 0.018 45);
+ --ring: oklch(0.8 0.04 50);
+}
diff --git a/src/themes/forest-ash.css b/src/themes/forest-ash.css
new file mode 100644
index 0000000..c93af44
--- /dev/null
+++ b/src/themes/forest-ash.css
@@ -0,0 +1,49 @@
+/* Palette: Forest & Ash
+ * Cool sage-tinted background, charcoal ink, mossy accent. The
+ * "nature integrated" word from the brief made literal — picks up
+ * the Boulder emerald tile and the Asheville pine-window framing.
+ */
+
+:root[data-palette='forest-ash'] {
+ --background: oklch(0.97 0.008 150);
+ --foreground: oklch(0.2 0.012 150);
+ --card: oklch(0.97 0.008 150);
+ --card-foreground: oklch(0.2 0.012 150);
+ --popover: oklch(0.97 0.008 150);
+ --popover-foreground: oklch(0.2 0.012 150);
+ --primary: oklch(0.32 0.04 150);
+ --primary-foreground: oklch(0.97 0.008 150);
+ --secondary: oklch(0.93 0.014 145);
+ --secondary-foreground: oklch(0.2 0.012 150);
+ --muted: oklch(0.93 0.012 145);
+ --muted-foreground: oklch(0.5 0.018 145);
+ --accent: oklch(0.86 0.03 150);
+ --accent-foreground: oklch(0.2 0.012 150);
+ --destructive: oklch(0.55 0.18 25);
+ --destructive-foreground: oklch(0.97 0.008 150);
+ --border: oklch(0.87 0.014 150);
+ --input: oklch(0.87 0.014 150);
+ --ring: oklch(0.32 0.04 150);
+}
+
+:root[data-palette='forest-ash'].dark {
+ --background: oklch(0.16 0.012 150);
+ --foreground: oklch(0.94 0.01 150);
+ --card: oklch(0.19 0.012 150);
+ --card-foreground: oklch(0.94 0.01 150);
+ --popover: oklch(0.19 0.012 150);
+ --popover-foreground: oklch(0.94 0.01 150);
+ --primary: oklch(0.86 0.04 150);
+ --primary-foreground: oklch(0.19 0.012 150);
+ --secondary: oklch(0.26 0.016 150);
+ --secondary-foreground: oklch(0.94 0.01 150);
+ --muted: oklch(0.26 0.016 150);
+ --muted-foreground: oklch(0.7 0.014 145);
+ --accent: oklch(0.33 0.03 150);
+ --accent-foreground: oklch(0.94 0.01 150);
+ --destructive: oklch(0.45 0.18 25);
+ --destructive-foreground: oklch(0.94 0.01 150);
+ --border: oklch(0.29 0.016 150);
+ --input: oklch(0.29 0.016 150);
+ --ring: oklch(0.8 0.04 150);
+}
diff --git a/src/themes/index.css b/src/themes/index.css
new file mode 100644
index 0000000..950bca9
--- /dev/null
+++ b/src/themes/index.css
@@ -0,0 +1,60 @@
+/* Palette registry — each module declares its tokens scoped to
+ * :root[data-palette='']. usePalette() sets the attribute.
+ * A bare :root fallback below copies the stone-warm tokens so the
+ * page renders correctly before JS attaches the attribute and for
+ * users with JS disabled.
+ */
+
+@import './stone-warm.css';
+@import './desert-clay.css';
+@import './forest-ash.css';
+@import './charcoal-cream.css';
+
+/* Pre-hydration fallback: same as stone-warm. Kept here (not in
+ * stone-warm.css under :root) so swapping which palette is the
+ * default is one edit — point this block at the values from a
+ * different theme module. */
+:root {
+ --background: oklch(0.985 0.005 80);
+ --foreground: oklch(0.2 0.012 60);
+ --card: oklch(0.985 0.005 80);
+ --card-foreground: oklch(0.2 0.012 60);
+ --popover: oklch(0.985 0.005 80);
+ --popover-foreground: oklch(0.2 0.012 60);
+ --primary: oklch(0.18 0.008 60);
+ --primary-foreground: oklch(0.97 0.008 80);
+ --secondary: oklch(0.94 0.008 75);
+ --secondary-foreground: oklch(0.2 0.012 60);
+ --muted: oklch(0.94 0.008 75);
+ --muted-foreground: oklch(0.5 0.015 60);
+ --accent: oklch(0.92 0.018 65);
+ --accent-foreground: oklch(0.2 0.012 60);
+ --destructive: oklch(0.55 0.18 25);
+ --destructive-foreground: oklch(0.97 0.008 80);
+ --border: oklch(0.88 0.01 70);
+ --input: oklch(0.88 0.01 70);
+ --ring: oklch(0.2 0.012 60);
+ --radius: 0.25rem;
+}
+
+.dark {
+ --background: oklch(0.16 0.008 60);
+ --foreground: oklch(0.95 0.01 80);
+ --card: oklch(0.18 0.008 60);
+ --card-foreground: oklch(0.95 0.01 80);
+ --popover: oklch(0.18 0.008 60);
+ --popover-foreground: oklch(0.95 0.01 80);
+ --primary: oklch(0.95 0.01 80);
+ --primary-foreground: oklch(0.18 0.008 60);
+ --secondary: oklch(0.24 0.012 60);
+ --secondary-foreground: oklch(0.95 0.01 80);
+ --muted: oklch(0.24 0.012 60);
+ --muted-foreground: oklch(0.7 0.012 70);
+ --accent: oklch(0.3 0.022 65);
+ --accent-foreground: oklch(0.95 0.01 80);
+ --destructive: oklch(0.45 0.18 25);
+ --destructive-foreground: oklch(0.95 0.01 80);
+ --border: oklch(0.28 0.012 60);
+ --input: oklch(0.28 0.012 60);
+ --ring: oklch(0.85 0.012 75);
+}
diff --git a/src/themes/stone-warm.css b/src/themes/stone-warm.css
new file mode 100644
index 0000000..185e07c
--- /dev/null
+++ b/src/themes/stone-warm.css
@@ -0,0 +1,48 @@
+/* Palette: Stone Warm
+ * Bone background, matte-black ink, taupe muted. Gallery-quiet, the
+ * most conservative of the four. Closest to Studio McGee's chrome.
+ */
+
+:root[data-palette='stone-warm'] {
+ --background: oklch(0.985 0.005 80);
+ --foreground: oklch(0.2 0.012 60);
+ --card: oklch(0.985 0.005 80);
+ --card-foreground: oklch(0.2 0.012 60);
+ --popover: oklch(0.985 0.005 80);
+ --popover-foreground: oklch(0.2 0.012 60);
+ --primary: oklch(0.18 0.008 60);
+ --primary-foreground: oklch(0.97 0.008 80);
+ --secondary: oklch(0.94 0.008 75);
+ --secondary-foreground: oklch(0.2 0.012 60);
+ --muted: oklch(0.94 0.008 75);
+ --muted-foreground: oklch(0.5 0.015 60);
+ --accent: oklch(0.92 0.018 65);
+ --accent-foreground: oklch(0.2 0.012 60);
+ --destructive: oklch(0.55 0.18 25);
+ --destructive-foreground: oklch(0.97 0.008 80);
+ --border: oklch(0.88 0.01 70);
+ --input: oklch(0.88 0.01 70);
+ --ring: oklch(0.2 0.012 60);
+}
+
+:root[data-palette='stone-warm'].dark {
+ --background: oklch(0.16 0.008 60);
+ --foreground: oklch(0.95 0.01 80);
+ --card: oklch(0.18 0.008 60);
+ --card-foreground: oklch(0.95 0.01 80);
+ --popover: oklch(0.18 0.008 60);
+ --popover-foreground: oklch(0.95 0.01 80);
+ --primary: oklch(0.95 0.01 80);
+ --primary-foreground: oklch(0.18 0.008 60);
+ --secondary: oklch(0.24 0.012 60);
+ --secondary-foreground: oklch(0.95 0.01 80);
+ --muted: oklch(0.24 0.012 60);
+ --muted-foreground: oklch(0.7 0.012 70);
+ --accent: oklch(0.3 0.022 65);
+ --accent-foreground: oklch(0.95 0.01 80);
+ --destructive: oklch(0.45 0.18 25);
+ --destructive-foreground: oklch(0.95 0.01 80);
+ --border: oklch(0.28 0.012 60);
+ --input: oklch(0.28 0.012 60);
+ --ring: oklch(0.85 0.012 75);
+}