diff --git a/test/simpleshop_theme_web/live/theme_css_consistency_test.exs b/test/simpleshop_theme_web/live/theme_css_consistency_test.exs
new file mode 100644
index 0000000..67a0fa9
--- /dev/null
+++ b/test/simpleshop_theme_web/live/theme_css_consistency_test.exs
@@ -0,0 +1,163 @@
+defmodule SimpleshopThemeWeb.ThemeCSSConsistencyTest do
+ @moduledoc """
+ Tests that verify CSS selectors work correctly for both the theme editor
+ preview (.preview-frame) and the shop pages (.shop-root).
+
+ These tests ensure that the theme-layer2-attributes.css file has correct
+ selectors for both contexts, and that CSS custom properties are resolved.
+ """
+
+ use SimpleshopThemeWeb.ConnCase, async: false
+
+ import Phoenix.LiveViewTest
+ import SimpleshopTheme.AccountsFixtures
+
+ alias SimpleshopTheme.Settings
+
+ setup do
+ user = user_fixture()
+ %{user: user}
+ end
+
+ describe "CSS selector consistency" do
+ test "shop home page has .shop-root with data attributes", %{conn: conn} do
+ {:ok, _view, html} = live(conn, ~p"/")
+
+ # Verify shop-root element exists with theme data attributes
+ assert html =~ ~r/
]*class="shop-root/
+ assert html =~ ~r/data-mood="/
+ assert html =~ ~r/data-typography="/
+ assert html =~ ~r/data-shape="/
+ assert html =~ ~r/data-density="/
+ end
+
+ test "theme editor has .preview-frame with data attributes", %{conn: conn, user: user} do
+ conn = log_in_user(conn, user)
+ {:ok, _view, html} = live(conn, ~p"/admin/theme")
+
+ # Verify preview-frame element exists with theme data attributes
+ assert html =~ ~r/
]*class="preview-frame/
+ assert html =~ ~r/data-mood="/
+ assert html =~ ~r/data-typography="/
+ assert html =~ ~r/data-shape="/
+ assert html =~ ~r/data-density="/
+ end
+
+ test "shop page uses same theme settings as preview", %{conn: conn, user: user} do
+ # Set a specific theme configuration
+ {:ok, _settings} = Settings.apply_preset(:night)
+
+ # Check shop page
+ {:ok, _view, shop_html} = live(conn, ~p"/")
+
+ # Check preview (authenticated)
+ conn = log_in_user(conn, user)
+ {:ok, _view, preview_html} = live(conn, ~p"/admin/theme")
+
+ # Extract data-mood values from both
+ [_, shop_mood] = Regex.run(~r/data-mood="([^"]+)"/, shop_html)
+ [_, preview_mood] = Regex.run(~r/data-mood="([^"]+)"/, preview_html)
+
+ # They should match
+ assert shop_mood == preview_mood
+ assert shop_mood == "dark"
+ end
+
+ test "theme settings changes are reflected on shop page", %{conn: conn} do
+ # Start with minimal preset (neutral mood)
+ {:ok, _settings} = Settings.apply_preset(:minimal)
+
+ {:ok, _view, html} = live(conn, ~p"/")
+ assert html =~ ~s(data-mood="neutral")
+
+ # Change to night preset (dark mood)
+ {:ok, _settings} = Settings.apply_preset(:night)
+
+ {:ok, _view, html} = live(conn, ~p"/")
+ assert html =~ ~s(data-mood="dark")
+
+ # Change to gallery preset (warm mood)
+ {:ok, _settings} = Settings.apply_preset(:gallery)
+
+ {:ok, _view, html} = live(conn, ~p"/")
+ assert html =~ ~s(data-mood="warm")
+ end
+ end
+
+ describe "CSS file structure" do
+ test "theme-layer2-attributes.css has both .preview-frame and .shop-root selectors" do
+ css_path = Path.join([File.cwd!(), "assets", "css", "theme-layer2-attributes.css"])
+ css_content = File.read!(css_path)
+
+ # Check that mood selectors exist for both
+ assert css_content =~ ".preview-frame[data-mood=\"dark\"]"
+ assert css_content =~ ".shop-root[data-mood=\"dark\"]"
+
+ assert css_content =~ ".preview-frame[data-mood=\"warm\"]"
+ assert css_content =~ ".shop-root[data-mood=\"warm\"]"
+
+ # Check typography selectors
+ assert css_content =~ ".preview-frame[data-typography=\"modern\"]"
+ assert css_content =~ ".shop-root[data-typography=\"modern\"]"
+
+ # Check shape selectors
+ assert css_content =~ ".preview-frame[data-shape=\"sharp\"]"
+ assert css_content =~ ".shop-root[data-shape=\"sharp\"]"
+
+ # Check descendant selectors (important for specificity)
+ assert css_content =~ ".preview-frame .product-grid"
+ assert css_content =~ ".shop-root .product-grid"
+
+ assert css_content =~ ".preview-frame .product-card"
+ assert css_content =~ ".shop-root .product-card"
+ end
+
+ test "default selectors include both .preview-frame and .shop-root" do
+ css_path = Path.join([File.cwd!(), "assets", "css", "theme-layer2-attributes.css"])
+ css_content = File.read!(css_path)
+
+ # The default (neutral) mood should apply to both
+ # This regex checks for the pattern where both selectors are grouped
+ assert Regex.match?(
+ ~r/\.preview-frame,\s*\n\.shop-root\s*\{[^}]*--t-surface-base/,
+ css_content
+ )
+ end
+ end
+
+ describe "generated CSS cache" do
+ test "generated CSS includes theme variables" do
+ # Apply a preset
+ {:ok, settings} = Settings.apply_preset(:bold)
+
+ # Generate CSS
+ css = SimpleshopTheme.Theme.CSSGenerator.generate(settings)
+
+ # Check that key variables are present
+ assert css =~ "--t-accent-h:"
+ assert css =~ "--t-accent-s:"
+ assert css =~ "--t-accent-l:"
+ assert css =~ "--t-font-size-scale:"
+ assert css =~ "--t-heading-weight-override:"
+ end
+
+ test "CSS cache is warmed on startup and invalidated on settings change" do
+ # Ensure cache has content
+ SimpleshopTheme.Theme.CSSCache.warm()
+
+ {:ok, css1} = SimpleshopTheme.Theme.CSSCache.get()
+ assert is_binary(css1)
+ assert css1 =~ "--t-accent-h:"
+
+ # Change settings (this should invalidate and rewarm cache)
+ {:ok, _settings} = Settings.apply_preset(:night)
+
+ {:ok, css2} = SimpleshopTheme.Theme.CSSCache.get()
+ assert is_binary(css2)
+
+ # The CSS should be different (different accent color)
+ # Note: this may or may not be true depending on preset colors
+ assert css2 =~ "--t-accent-h:"
+ end
+ end
+end