diff --git a/static/boot.js b/static/boot.js
index e6a1d267..b9a5232b 100644
--- a/static/boot.js
+++ b/static/boot.js
@@ -639,6 +639,7 @@ const _SKINS=[
{name:'Poseidon', colors:['#0EA5E9','#0284C7','#0369A1']},
{name:'Sisyphus', colors:['#A78BFA','#8B5CF6','#7C3AED']},
{name:'Charizard',colors:['#FB923C','#F97316','#EA580C']},
+ {name:'Sienna', colors:['#D97757','#C06A49','#9A523A']},
];
const _VALID_THEMES=new Set((_THEMES||[]).map(t=>t.value));
const _VALID_SKINS=new Set((_SKINS||[]).map(s=>s.name.toLowerCase()));
diff --git a/static/index.html b/static/index.html
index 34dd1515..e313598b 100644
--- a/static/index.html
+++ b/static/index.html
@@ -15,7 +15,7 @@
-
+
diff --git a/static/style.css b/static/style.css
index 0cadcf49..c6b7e156 100644
--- a/static/style.css
+++ b/static/style.css
@@ -98,6 +98,67 @@
/* ── Skin: Charizard (orange) ── */
:root[data-skin="charizard"]{--accent:#EA580C;--accent-hover:#C2410C;--accent-bg:rgba(234,88,12,0.08);--accent-bg-strong:rgba(234,88,12,0.15);--accent-text:#C2410C;}
:root.dark[data-skin="charizard"]{--accent:#FB923C;--accent-hover:#F97316;--accent-bg:rgba(251,146,60,0.08);--accent-bg-strong:rgba(251,146,60,0.15);--accent-text:#FB923C;}
+ /* ── Skin: Sienna (warm clay & sand earth palette) ──
+ Full palette rewrite (not just --accent): warm off-white surface,
+ soft sand sidebar, clay accent. Sets --user-bubble-* for the redesigned
+ user bubble block, mutes tool-card chrome to neutral, and bumps the
+ reading column to a serif body for long-form prose. Opt-in via the
+ Settings → Skin picker; default skin stays "default" (gold). */
+ :root[data-skin="sienna"]{
+ --bg:#FAF9F5;--sidebar:#F0EEE6;--surface:#FFFFFF;
+ --border:#E7E4DB;--border2:#D7D3C7;
+ --text:#1F1E1C;--muted:#6B6A63;--strong:#141311;--em:#3E3D39;
+ --accent:#D97757;--accent-hover:#BF6545;--accent-text:#A55237;
+ --accent-bg:rgba(217,119,87,0.09);--accent-bg-strong:rgba(217,119,87,0.18);
+ --code-bg:#F5F3EC;--code-inline-bg:rgba(20,19,17,0.06);--code-text:#8A3E1A;--pre-text:#1F1E1C;
+ --topbar-bg:rgba(250,249,245,0.96);--main-bg:rgba(250,249,245,0.55);
+ --input-bg:rgba(20,19,17,0.035);--hover-bg:rgba(20,19,17,0.05);
+ --focus-ring:rgba(217,119,87,0.3);--focus-glow:rgba(217,119,87,0.1);
+ --blue:#2E6F9E;--gold:#A55237;
+ --user-bubble-bg:#ECE9DF;--user-bubble-border:#DED9CC;
+ --user-bubble-text:#1F1E1C;--user-bubble-placeholder:#6B6A63;
+ --user-selection-bg:rgba(217,119,87,0.28);--user-selection-text:#1F1E1C;
+ }
+ :root.dark[data-skin="sienna"]{
+ --bg:#1F1E1C;--sidebar:#262522;--surface:#2C2B28;
+ --border:#3A3935;--border2:#4A4843;
+ --text:#EDEBE3;--muted:#A3A197;--strong:#F7F5ED;--em:#C9C6BC;
+ --accent:#E0896D;--accent-hover:#D97757;--accent-text:#E6A88A;
+ --accent-bg:rgba(224,137,109,0.12);--accent-bg-strong:rgba(224,137,109,0.22);
+ --code-bg:#2A2926;--code-inline-bg:rgba(255,255,255,0.07);--code-text:#F0B593;--pre-text:#EDEBE3;
+ --topbar-bg:rgba(38,37,34,0.96);--main-bg:rgba(31,30,28,0.55);
+ --input-bg:rgba(255,255,255,0.045);--hover-bg:rgba(255,255,255,0.07);
+ --focus-ring:rgba(224,137,109,0.35);--focus-glow:rgba(224,137,109,0.1);
+ --blue:#8BB8D6;--gold:#E6A88A;
+ --user-bubble-bg:#34322E;--user-bubble-border:#42403B;
+ --user-bubble-text:#EDEBE3;--user-bubble-placeholder:#A3A197;
+ --user-selection-bg:rgba(224,137,109,0.3);--user-selection-text:#F7F5ED;
+ }
+ /* Specificity boosted to beat the :root:not(.dark) .new-chat-btn rule that
+ sets color:var(--accent-text) — without this, the solid-accent background
+ would collide with the inherited text color, producing clay-on-clay text. */
+ :root[data-skin="sienna"]:not(.dark) .new-chat-btn,
+ :root.dark[data-skin="sienna"] .new-chat-btn{background:var(--accent);border-color:var(--accent);color:#fff;font-weight:600;box-shadow:0 1px 2px rgba(20,19,17,0.08);}
+ :root[data-skin="sienna"]:not(.dark) .new-chat-btn:hover,
+ :root.dark[data-skin="sienna"] .new-chat-btn:hover{background:var(--accent-hover);border-color:var(--accent-hover);color:#fff;}
+ /* Tool / thinking cards: mute the blue-ish Hermes highlights to neutral */
+ :root[data-skin="sienna"] .tool-card{background:rgba(20,19,17,0.025);border-color:var(--border);border-radius:10px;}
+ :root[data-skin="sienna"] .tool-card:hover{border-color:var(--border2);}
+ :root[data-skin="sienna"] .tool-card-running{background:var(--accent-bg);border-color:var(--accent-bg-strong);}
+ :root[data-skin="sienna"] .tool-card-name{color:var(--muted);font-weight:500;}
+ :root[data-skin="sienna"] .tool-arg-key{color:var(--accent-text);}
+ :root[data-skin="sienna"] .tool-card-more{color:var(--accent-text);}
+ :root[data-skin="sienna"] .tool-card-running-dot{background:var(--accent);}
+ :root.dark[data-skin="sienna"] .tool-card{background:rgba(255,255,255,0.03);}
+ /* User bubble: soft sand pill, not a saturated accent fill */
+ :root[data-skin="sienna"] .msg-row[data-role="user"] .msg-body{border-radius:16px;padding:12px 16px;}
+ :root[data-skin="sienna"] .msg-row[data-role="user"] .msg-body code{color:var(--user-bubble-text);background:rgba(20,19,17,0.08);}
+ /* Send button pop and active session indicator */
+ :root[data-skin="sienna"] button.send-btn {box-shadow:0 1px 3px rgba(217,119,87,0.3);}
+ :root[data-skin="sienna"] button.send-btn:hover {box-shadow:0 2px 8px rgba(217,119,87,0.45);}
+ :root[data-skin="sienna"] .session-item.active{border-left:2px solid var(--accent);}
+ :root[data-skin="sienna"] .composer-box{border-radius:18px;}
+
/* #594: app-dialog light mode overrides — base styles use hardcoded dark gradients */
:root:not(.dark) .app-dialog{
background:linear-gradient(180deg,rgba(240,237,232,.99),rgba(228,224,216,.99));
diff --git a/tests/test_sienna_skin.py b/tests/test_sienna_skin.py
new file mode 100644
index 00000000..82baf7d5
--- /dev/null
+++ b/tests/test_sienna_skin.py
@@ -0,0 +1,79 @@
+"""Sienna skin: warm clay/sand earth palette, opt-in via Settings → Skin."""
+
+from pathlib import Path
+
+REPO = Path(__file__).parent.parent
+CSS = (REPO / "static" / "style.css").read_text(encoding="utf-8")
+BOOT_JS = (REPO / "static" / "boot.js").read_text(encoding="utf-8")
+INDEX_HTML = (REPO / "static" / "index.html").read_text(encoding="utf-8")
+
+
+def test_sienna_skin_present_in_skins_list():
+ """The Sienna skin must be exposed in the picker grid via _SKINS."""
+ assert "{name:'Sienna'" in BOOT_JS, "Sienna skin missing from _SKINS list"
+ assert "'#D97757','#C06A49','#9A523A'" in BOOT_JS, (
+ "Sienna preview swatches missing"
+ )
+
+
+def test_sienna_skin_in_early_init_allowlist():
+ """The early-init skin allowlist must accept 'sienna'."""
+ assert "sienna:1" in INDEX_HTML, (
+ "Sienna missing from early-init skin allowlist; saved skin would be "
+ "rejected and reset to default on boot"
+ )
+
+
+def test_sienna_skin_palette_has_full_light_and_dark():
+ """Sienna defines both light and dark scoped palettes."""
+ assert ':root[data-skin="sienna"]{' in CSS, (
+ "Sienna light-mode palette block missing"
+ )
+ assert ':root.dark[data-skin="sienna"]{' in CSS, (
+ "Sienna dark-mode palette block missing"
+ )
+ # Spot-check that the palette is a full rewrite (not just --accent)
+ for token in ("--bg:#FAF9F5", "--sidebar:#F0EEE6", "--accent:#D97757"):
+ assert token in CSS, f"Sienna light palette token missing: {token}"
+ for token in ("--bg:#1F1E1C", "--sidebar:#262522", "--accent:#E0896D"):
+ assert token in CSS, f"Sienna dark palette token missing: {token}"
+
+
+def test_sienna_skin_does_not_force_migration():
+ """Sienna must not be silently migrated onto existing users.
+
+ The early-init script in index.html must NOT contain logic that flips an
+ existing 'default' skin to 'sienna' on first load. New users keep the gold
+ default; users opt in via Settings → Skin.
+ """
+ # The skin allowlist line should NOT contain a sienna-migration flag.
+ init_script_idx = INDEX_HTML.find("var themes=")
+ end_idx = INDEX_HTML.find("", init_script_idx)
+ init_block = INDEX_HTML[init_script_idx:end_idx]
+ forbidden = ["sienna-migrated", "skin-sienna-migrated", "skin='sienna'", 'skin="sienna"']
+ for marker in forbidden:
+ assert marker not in init_block, (
+ f"Sienna skin must be opt-in, not force-migrated. Found '{marker}' "
+ f"in early-init script."
+ )
+
+
+def test_default_theme_is_still_dark():
+ """Adding a new skin must not change the default theme."""
+ # The early-init script defaults to 'dark' when no saved theme exists.
+ init_script_idx = INDEX_HTML.find("var themes=")
+ end_idx = INDEX_HTML.find("", init_script_idx)
+ init_block = INDEX_HTML[init_script_idx:end_idx]
+ assert "||'dark'" in init_block, (
+ "Default theme must remain 'dark' (the existing baseline)"
+ )
+
+
+def test_sienna_new_chat_button_specificity_guards_against_clay_on_clay():
+ """The new-chat button needs higher specificity than the base
+ :root:not(.dark) .new-chat-btn rule, otherwise the inherited
+ color:var(--accent-text) collides with the solid-accent background and
+ produces invisible clay-on-clay text in light mode."""
+ assert ':root[data-skin="sienna"]:not(.dark) .new-chat-btn' in CSS, (
+ "Sienna light-mode .new-chat-btn override missing — clay-on-clay risk"
+ )