From cfd1903c6b0f2ce9ab4ea6eaceb2de960c69fa20 Mon Sep 17 00:00:00 2001 From: Kpa-clawbot Date: Thu, 7 May 2026 08:29:02 -0700 Subject: [PATCH] fix(logo): default sage/teal brand colors, customizer mirrors accent (#1162) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Restores sage/teal as default logo colors while preserving customizer theming. Closes the gap from #1157 (closed) and the user's complaint about lost two-tone. Out-of-the-box, the navbar + hero CORE/SCOPE wordmarks now render the brand-identity duotone — `#cfd9c9` (sage / fog) and `#2c8c8c` (teal / water). When an operator picks a theme via the customizer (or sets a custom accent color), the wordmark recolors to follow. ## Approach (Option C — decoupled defaults + customizer mirror) - **`public/style.css` `:root`** — set `--logo-accent: #cfd9c9` and `--logo-accent-hi: #2c8c8c` as literal defaults. Removes the previous `var(--accent)` cascade so blue-by-default no longer leaks into the brand mark. - **`public/customize-v2.js`** — `applyTheme()`, the early-apply path, and the live color-picker `input` handler now mirror `themeSection.accent` → `--logo-accent` and `themeSection.accentHover` → `--logo-accent-hi`. - **`public/customize.js`** (legacy) — same mirroring in `applyThemePreview()` and the early localStorage replay. - **`.github/workflows/deploy.yml`** — adds the new e2e to the Chromium batch. This preserves `--accent` as the canonical app-wide accent token (no other UI changes) while giving the logo its own brand-defaulted tokens that the customizer still drives. ## Tests Red → green commit pair on the branch. - **NEW: `test-logo-default-sage-teal-e2e.js`** — gates both halves of the contract: 1. Clean localStorage → navbar + hero CORE = `rgb(207, 217, 201)`, SCOPE = `rgb(44, 140, 140)`. 2. Seeded `cs-theme-overrides` with red accent → navbar + hero recolor to red. - **UPDATED: `test-logo-theme-e2e.js`** — replaces the old "must NOT be sage" sentinel (sage was a regression marker; it's now the brand default) with a theme-reactivity probe that overrides `--logo-accent` / `--logo-accent-hi` directly and asserts the wordmark fill changes. Duotone, mobile-fit, and clip checks are unchanged. ## Verification - Default load: sage CORE + teal SCOPE in navbar AND hero ✔ (asserted by step 1 of the new e2e). - Customizer override: wordmark follows `accent` / `accentHover` ✔ (asserted by step 2 of the new e2e + the theme-reactivity probe in `test-logo-theme-e2e.js`). - Preflight: all hard gates green (PII, branch scope, red commit, CSS-var defined, CSS self-fallback, LIKE-on-JSON, sync migration); all warnings green. ## Browser verified E2E assertion added: `test-logo-default-sage-teal-e2e.js:73` (default sage), `test-logo-default-sage-teal-e2e.js:124` (customizer override). CI runs both via `deploy.yml:243`. Browser verified: covered by Chromium e2e against `http://localhost:13581` in CI; staging URL TBD on merge. --------- Co-authored-by: openclaw-bot --- .github/workflows/deploy.yml | 1 + public/customize-v2.js | 27 ++++- public/customize.js | 6 + public/style.css | 30 ++--- test-logo-default-sage-teal-e2e.js | 177 +++++++++++++++++++++++++++++ test-logo-theme-e2e.js | 77 ++++++++++--- 6 files changed, 289 insertions(+), 29 deletions(-) create mode 100644 test-logo-default-sage-teal-e2e.js diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 7172abe4..1a6c1b79 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -240,6 +240,7 @@ jobs: BASE_URL=http://localhost:13581 node test-issue-1147-section-order-e2e.js 2>&1 | tee -a e2e-output.txt CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-logo-rebrand-e2e.js 2>&1 | tee -a e2e-output.txt CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-logo-theme-e2e.js 2>&1 | tee -a e2e-output.txt + CHROMIUM_REQUIRE=1 BASE_URL=http://localhost:13581 node test-logo-default-sage-teal-e2e.js 2>&1 | tee -a e2e-output.txt - name: Collect frontend coverage (parallel) if: success() && github.event_name == 'push' diff --git a/public/customize-v2.js b/public/customize-v2.js index 577c0514..42eee405 100644 --- a/public/customize-v2.js +++ b/public/customize-v2.js @@ -514,7 +514,7 @@ return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; } - function applyCSS(effectiveConfig) { + function applyCSS(effectiveConfig, userOverrides) { var dark = isDarkMode(); var themeSection = dark ? Object.assign({}, effectiveConfig.theme || {}, effectiveConfig.themeDark || {}) @@ -529,6 +529,19 @@ } } + // Logo brand colors mirror --accent / --accent-hover ONLY when an + // operator has actually overridden them via the customizer. We check + // userOverrides (not the merged effective config), so the server-default + // accent (#4a9eff) does NOT clobber the sage/teal :root brand defaults + // out-of-the-box. When an operator picks a theme, customizer writes the + // override to localStorage, the override flows through here, and the + // wordmark recolors to follow the chosen accent. + var ovTheme = (userOverrides && (dark + ? Object.assign({}, userOverrides.theme || {}, userOverrides.themeDark || {}) + : (userOverrides.theme || {}))) || {}; + if (ovTheme.accent) root.setProperty('--logo-accent', ovTheme.accent); + if (ovTheme.accentHover) root.setProperty('--logo-accent-hi', ovTheme.accentHover); + // Derived vars if (themeSection.background) root.setProperty('--content-bg', themeSection.contentBg || themeSection.background); if (themeSection.surface1) root.setProperty('--card-bg', themeSection.cardBg || themeSection.surface1); @@ -614,7 +627,7 @@ var overrides = readOverrides(); var effective = computeEffective(_serverDefaults || {}, overrides); window.SITE_CONFIG = effective; - applyCSS(effective); + applyCSS(effective, overrides); } // ── setOverride / clearOverride ── @@ -1392,6 +1405,9 @@ // Optimistic CSS update (Decision #12) var cssVar = THEME_CSS_MAP[key]; if (cssVar) document.documentElement.style.setProperty(cssVar, inp.value); + // Mirror to logo brand vars so the wordmark recolors live too. + if (key === 'accent') document.documentElement.style.setProperty('--logo-accent', inp.value); + if (key === 'accentHover') document.documentElement.style.setProperty('--logo-accent-hi', inp.value); // Update hex display var hex = inp.parentElement.querySelector('.cust-hex'); if (hex) hex.textContent = inp.value; @@ -1656,6 +1672,13 @@ for (var key in THEME_CSS_MAP) { if (themeSection[key]) root.setProperty(THEME_CSS_MAP[key], themeSection[key]); } + // Mirror accent → logo brand vars ONLY when present in overrides (so the + // server-default accent never clobbers the sage/teal :root brand defaults). + var ovTheme = dark + ? Object.assign({}, earlyOverrides.theme || {}, earlyOverrides.themeDark || {}) + : (earlyOverrides.theme || {}); + if (ovTheme.accent) root.setProperty('--logo-accent', ovTheme.accent); + if (ovTheme.accentHover) root.setProperty('--logo-accent-hi', ovTheme.accentHover); if (themeSection.background) root.setProperty('--content-bg', themeSection.contentBg || themeSection.background); if (themeSection.surface1) root.setProperty('--card-bg', themeSection.cardBg || themeSection.surface1); // Apply node/type colors from overrides early diff --git a/public/customize.js b/public/customize.js index 3dd0f805..f3657393 100644 --- a/public/customize.js +++ b/public/customize.js @@ -543,6 +543,9 @@ for (var key in THEME_CSS_MAP) { if (t[key]) document.documentElement.style.setProperty(THEME_CSS_MAP[key], t[key]); } + // Mirror accent → logo brand vars so the wordmark follows the theme. + if (t.accent) document.documentElement.style.setProperty('--logo-accent', t.accent); + if (t.accentHover) document.documentElement.style.setProperty('--logo-accent-hi', t.accentHover); // Derived vars that reference other vars — need explicit override if (t.background) { document.documentElement.style.setProperty('--content-bg', t.background); @@ -1447,6 +1450,9 @@ for (const [key, val] of Object.entries(themeData)) { if (THEME_CSS_MAP[key]) document.documentElement.style.setProperty(THEME_CSS_MAP[key], val); } + // Mirror accent → logo brand vars (matches applyThemePreview()). + if (themeData.accent) document.documentElement.style.setProperty('--logo-accent', themeData.accent); + if (themeData.accentHover) document.documentElement.style.setProperty('--logo-accent-hi', themeData.accentHover); // Derived vars if (themeData.background) document.documentElement.style.setProperty('--content-bg', themeData.background); if (themeData.surface1) document.documentElement.style.setProperty('--card-bg', themeData.surface1); diff --git a/public/style.css b/public/style.css index 28691ef6..07f6f848 100644 --- a/public/style.css +++ b/public/style.css @@ -128,25 +128,29 @@ --input-bg: #fff; --selected-bg: #dbeafe; - /* --- Logo theme tokens (PR #1137) ------------------------ + /* --- Logo theme tokens (PR #1137 + brand-default follow-up) ----------- * The CoreScope SVG logos are inlined into the DOM (navbar + * home hero), so they inherit page CSS custom properties. The * legacy --logo-* names are kept so existing themes / brand - * customizations that override them still work; defaults map - * to the active theme so wordmark/arc colors track --accent / - * --nav-text / --text-muted on light AND dark themes. + * customizations that override them still work. * --logo-text → wordmark "CORE" / "SCOPE" / labels - * --logo-accent → primary node + left-side arcs (default --accent) - * --logo-accent-hi → secondary node + right-side arcs (default --accent-hover) + * --logo-accent → primary node + left-side arcs (default sage) + * --logo-accent-hi → secondary node + right-side arcs (default teal) * --logo-muted → tagline + sine wave - * No --logo-bg here on purpose — the inlined SVGs MUST be - * transparent so they sit on whatever bg the page paints. - * --logo-text defaults to --text (the page foreground) so the - * hero wordmark is readable on any theme; the navbar scopes it - * to --nav-text below so it stays white on the dark navbar. */ + * Sage (#cfd9c9 — fog) and teal (#2c8c8c — water) are the OUT-OF-BOX + * brand identity. They are NOT cascaded from --accent any more, so + * changing the app accent color via dev tools alone will NOT recolor + * the logo. The customizer (public/customize-v2.js) mirrors --accent + * and --accent-hover into --logo-accent / --logo-accent-hi when an + * operator picks a theme, preserving full theme-driven recoloring. + * No --logo-bg here on purpose — the inlined SVGs MUST be transparent + * so they sit on whatever bg the page paints. --logo-text defaults to + * --text (the page foreground) so the hero wordmark is readable on + * any theme; the navbar scopes it to --nav-text below so it stays + * white on the dark navbar. */ --logo-text: var(--text); - --logo-accent: var(--accent); - --logo-accent-hi: var(--accent-hover); + --logo-accent: #cfd9c9; + --logo-accent-hi: #2c8c8c; --logo-muted: var(--text-muted); --surface-0: #f4f5f7; diff --git a/test-logo-default-sage-teal-e2e.js b/test-logo-default-sage-teal-e2e.js new file mode 100644 index 00000000..517e61bd --- /dev/null +++ b/test-logo-default-sage-teal-e2e.js @@ -0,0 +1,177 @@ +#!/usr/bin/env node +/* Logo default-brand E2E — verifies that the navbar + hero wordmarks + * render the sage/teal brand identity OUT OF THE BOX (no operator + * customizer override active), AND that the customizer can still + * override those colors when an operator picks a theme. + * + * Asserts: + * 1. Default load (clean localStorage, no overrides): + * navbar CORE.fill === rgb(207, 217, 201) // sage / fog + * navbar SCOPE.fill === rgb(44, 140, 140) // teal / water + * hero CORE/SCOPE same. + * 2. After a customizer override that sets accent=red (#dc2626) and + * accentHover=red-hover (#ef4444), the wordmark CORE+SCOPE recolors + * to follow the override (NOT sage/teal anymore). + * + * This is the contract from PR #1157 follow-up: sage/teal are the brand + * default, but the customizer remains the canonical theming surface. + * + * On master this test FAILS step 1 because the default --accent is + * #4a9eff (blue), so --logo-accent resolves to blue — not sage. + */ +'use strict'; + +const { chromium } = require('playwright'); + +const BASE = process.env.BASE_URL || 'http://localhost:13581'; +const SAGE = 'rgb(207, 217, 201)'; +const TEAL = 'rgb(44, 140, 140)'; + +function fail(msg) { + console.error(`test-logo-default-sage-teal-e2e.js: FAIL — ${msg}`); + process.exit(1); +} + +async function readWordmark(page, sel) { + return await page.evaluate((s) => { + const root = document.querySelector(s); + if (!root) return { error: s + ' missing' }; + const out = {}; + root.querySelectorAll('svg text').forEach((t) => { + const tc = (t.textContent || '').trim(); + if (tc === 'CORE' || tc === 'SCOPE') out[tc] = getComputedStyle(t).fill; + }); + return { out }; + }, sel); +} + +async function main() { + const requireChromium = process.env.CHROMIUM_REQUIRE === '1'; + let browser; + try { + browser = await chromium.launch({ + headless: true, + executablePath: process.env.CHROMIUM_PATH || undefined, + args: ['--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage'], + }); + } catch (err) { + if (requireChromium) { + console.error(`test-logo-default-sage-teal-e2e.js: FAIL — Chromium required but unavailable: ${err.message}`); + process.exit(1); + } + console.log(`test-logo-default-sage-teal-e2e.js: SKIP (Chromium unavailable: ${err.message.split('\n')[0]})`); + process.exit(0); + } + + let passed = 0; + const total = 4; + try { + // ── Step 1: clean localStorage, default load → sage/teal ── + const ctx1 = await browser.newContext({ viewport: { width: 1280, height: 900 } }); + const page1 = await ctx1.newPage(); + page1.setDefaultTimeout(10000); + // Defensive: ensure no customizer overrides leak in. + await page1.addInitScript(() => { + try { localStorage.removeItem('cs-theme-overrides'); } catch (_) {} + try { localStorage.setItem('meshcore-user-level', 'experienced'); } catch (_) {} + }); + await page1.goto(BASE + '/#/', { waitUntil: 'domcontentloaded' }); + await page1.waitForSelector('.nav-brand svg.brand-logo text', { timeout: 8000 }); + + const navDefault = await readWordmark(page1, '.nav-brand'); + if (navDefault.error) fail(navDefault.error); + if (!navDefault.out.CORE || !navDefault.out.SCOPE) { + fail(`default navbar CORE/SCOPE missing: ${JSON.stringify(navDefault.out)}`); + } + if (navDefault.out.CORE !== SAGE) { + fail(`default navbar CORE fill = ${navDefault.out.CORE}; expected sage ${SAGE}`); + } + if (navDefault.out.SCOPE !== TEAL) { + fail(`default navbar SCOPE fill = ${navDefault.out.SCOPE}; expected teal ${TEAL}`); + } + console.log(` ✅ default navbar wordmark is sage/teal (CORE=${navDefault.out.CORE}, SCOPE=${navDefault.out.SCOPE})`); + passed++; + + await page1.evaluate(() => { window.location.hash = '#/home'; }); + await page1.waitForFunction(() => location.hash === '#/home'); + await page1.waitForSelector('.home-hero', { timeout: 8000 }); + // Hero SVG can render after the route swap; wait for the wordmark text + // to actually exist before reading fills. + await page1.waitForFunction(() => { + const h = document.querySelector('.home-hero'); + return !!(h && h.querySelector('svg text')); + }, null, { timeout: 8000 }); + const heroDefault = await readWordmark(page1, '.home-hero'); + if (heroDefault.error) fail(heroDefault.error); + if (heroDefault.out.CORE !== SAGE) { + fail(`default hero CORE fill = ${heroDefault.out.CORE}; expected sage ${SAGE}`); + } + if (heroDefault.out.SCOPE !== TEAL) { + fail(`default hero SCOPE fill = ${heroDefault.out.SCOPE}; expected teal ${TEAL}`); + } + console.log(` ✅ default hero wordmark is sage/teal (CORE=${heroDefault.out.CORE}, SCOPE=${heroDefault.out.SCOPE})`); + passed++; + await ctx1.close(); + + // ── Step 2: customizer override → red wordmark ── + const ctx2 = await browser.newContext({ viewport: { width: 1280, height: 900 } }); + const page2 = await ctx2.newPage(); + page2.setDefaultTimeout(10000); + // Seed the customizer override BEFORE first paint. customize-v2.js reads + // 'cs-theme-overrides' from localStorage on init and writes the matching + // CSS vars (including --logo-accent / --logo-accent-hi after this fix). + await page2.addInitScript(() => { + try { + localStorage.setItem('cs-theme-overrides', JSON.stringify({ + theme: { accent: '#dc2626', accentHover: '#ef4444' }, + themeDark: { accent: '#dc2626', accentHover: '#ef4444' }, + })); + localStorage.setItem('meshcore-user-level', 'experienced'); + } catch (_) {} + }); + await page2.goto(BASE + '/#/', { waitUntil: 'domcontentloaded' }); + await page2.waitForSelector('.nav-brand svg.brand-logo text', { timeout: 8000 }); + // Settle one frame for early-apply to run. + await page2.waitForTimeout(200); + + const navOverride = await readWordmark(page2, '.nav-brand'); + if (navOverride.error) fail(navOverride.error); + if (navOverride.out.CORE === SAGE || navOverride.out.SCOPE === TEAL) { + fail(`customizer override did NOT reach the logo — still sage/teal: ${JSON.stringify(navOverride.out)}. Customizer must mirror --accent → --logo-accent.`); + } + // Both halves should follow the override (CORE ← accent, SCOPE ← accentHover). + if (navOverride.out.CORE !== 'rgb(220, 38, 38)') { + fail(`navbar CORE under customizer override = ${navOverride.out.CORE}; expected rgb(220, 38, 38)`); + } + if (navOverride.out.SCOPE !== 'rgb(239, 68, 68)') { + fail(`navbar SCOPE under customizer override = ${navOverride.out.SCOPE}; expected rgb(239, 68, 68)`); + } + console.log(` ✅ navbar wordmark follows customizer override (CORE=${navOverride.out.CORE}, SCOPE=${navOverride.out.SCOPE})`); + passed++; + + await page2.evaluate(() => { window.location.hash = '#/home'; }); + await page2.waitForFunction(() => location.hash === '#/home'); + await page2.waitForSelector('.home-hero', { timeout: 8000 }); + await page2.waitForFunction(() => { + const h = document.querySelector('.home-hero'); + return !!(h && h.querySelector('svg text')); + }, null, { timeout: 8000 }); + const heroOverride = await readWordmark(page2, '.home-hero'); + if (heroOverride.error) fail(heroOverride.error); + if (heroOverride.out.CORE !== 'rgb(220, 38, 38)' || heroOverride.out.SCOPE !== 'rgb(239, 68, 68)') { + fail(`hero wordmark under customizer override = ${JSON.stringify(heroOverride.out)}; expected CORE=rgb(220,38,38), SCOPE=rgb(239,68,68)`); + } + console.log(` ✅ hero wordmark follows customizer override (CORE=${heroOverride.out.CORE}, SCOPE=${heroOverride.out.SCOPE})`); + passed++; + + await ctx2.close(); + await browser.close(); + console.log(`\ntest-logo-default-sage-teal-e2e.js: ${passed}/${total} PASS`); + } catch (err) { + try { await browser.close(); } catch (_) {} + console.error(`test-logo-default-sage-teal-e2e.js: FAIL — ${err.message}`); + process.exit(1); + } +} + +main(); diff --git a/test-logo-theme-e2e.js b/test-logo-theme-e2e.js index 102905af..c491608f 100644 --- a/test-logo-theme-e2e.js +++ b/test-logo-theme-e2e.js @@ -32,7 +32,13 @@ const { chromium } = require('playwright'); const BASE = process.env.BASE_URL || 'http://localhost:13581'; -const LEGACY_SAGE = 'rgb(207, 217, 201)'; +// Note: rgb(207, 217, 201) is the brand sage default for --logo-accent +// (see test-logo-default-sage-teal-e2e.js). It is NO LONGER a failure +// signal here; the original "must not be sage" assertion was written +// when sage meant "baked-into-SVG-attr regression" and the wordmark was +// supposed to follow --accent (then blue). Now sage is the intentional +// brand identity and the test below asserts theme-reactivity by mutating +// --logo-accent directly and observing the fill change instead. function fail(msg) { console.error(`test-logo-theme-e2e.js: FAIL — ${msg}`); @@ -74,8 +80,8 @@ async function main() { await page.evaluate(() => { document.documentElement.setAttribute('data-theme', 'light'); }); // 1. Navbar wordmark must be inline-SVG (not ) and computed - // fill must NOT be the legacy hardcoded sage. We grep for any - // with textContent CORE or SCOPE inside .nav-brand. + // fill must be theme-reactive: setting --logo-accent / --logo-accent-hi + // on :root must repaint the wordmark. const navWordmarkFills = await page.evaluate(() => { const out = []; const root = document.querySelector('.nav-brand'); @@ -93,12 +99,35 @@ async function main() { if (!navWordmarkFills.out || navWordmarkFills.out.length < 2) { fail(`navbar inline-SVG wordmark CORE/SCOPE not found (found: ${JSON.stringify(navWordmarkFills.out)}). Navbar logo must be inline so CSS vars apply.`); } - for (const w of navWordmarkFills.out) { - if (w.fill === LEGACY_SAGE) { - fail(`navbar wordmark "${w.tc}" still computes legacy sage fill ${LEGACY_SAGE} — wordmark fill must theme via CSS var`); - } + // Theme-reactivity probe: override --logo-accent / --logo-accent-hi and + // confirm fills change. This replaces the old "must not be legacy sage" + // assertion (sage is now the brand default — see test-logo-default-sage-teal-e2e.js). + const navReact = await page.evaluate(() => { + const root = document.querySelector('.nav-brand'); + const before = {}; + root.querySelectorAll('svg text').forEach((t) => { + const tc = (t.textContent || '').trim(); + if (tc === 'CORE' || tc === 'SCOPE') before[tc] = getComputedStyle(t).fill; + }); + document.documentElement.style.setProperty('--logo-accent', '#123456'); + document.documentElement.style.setProperty('--logo-accent-hi', '#abcdef'); + const after = {}; + root.querySelectorAll('svg text').forEach((t) => { + const tc = (t.textContent || '').trim(); + if (tc === 'CORE' || tc === 'SCOPE') after[tc] = getComputedStyle(t).fill; + }); + // Reset so later assertions on default colors aren't polluted. + document.documentElement.style.removeProperty('--logo-accent'); + document.documentElement.style.removeProperty('--logo-accent-hi'); + return { before, after }; + }); + if (navReact.before.CORE === navReact.after.CORE) { + fail(`navbar CORE fill did not change when --logo-accent was overridden (${navReact.before.CORE} → ${navReact.after.CORE}); wordmark must theme via --logo-accent`); } - console.log(` ✅ navbar wordmark fills are theme-reactive (${navWordmarkFills.out.map((w) => w.tc + '=' + w.fill).join(', ')})`); + if (navReact.before.SCOPE === navReact.after.SCOPE) { + fail(`navbar SCOPE fill did not change when --logo-accent-hi was overridden (${navReact.before.SCOPE} → ${navReact.after.SCOPE}); wordmark must theme via --logo-accent-hi`); + } + console.log(` ✅ navbar wordmark fills are theme-reactive (CORE ${navReact.before.CORE}→${navReact.after.CORE}, SCOPE ${navReact.before.SCOPE}→${navReact.after.SCOPE})`); passed++; // 2. Hero SVG must NOT have a full-canvas opaque background rect. @@ -136,7 +165,8 @@ async function main() { console.log(` ✅ hero SVG has no full-canvas opaque background rect`); passed++; - // 3. Hero wordmark CORE/SCOPE must not compute legacy sage fill on light theme. + // 3. Hero wordmark CORE/SCOPE must be theme-reactive — overriding + // --logo-accent / --logo-accent-hi must repaint the hero wordmark too. const heroWordmarkFills = await page.evaluate(() => { const hero = document.querySelector('.home-hero'); if (!hero) return { error: '.home-hero missing' }; @@ -153,12 +183,31 @@ async function main() { if (!heroWordmarkFills.out || heroWordmarkFills.out.length < 2) { fail(`hero inline-SVG wordmark CORE/SCOPE not found (found: ${JSON.stringify(heroWordmarkFills.out)})`); } - for (const w of heroWordmarkFills.out) { - if (w.fill === LEGACY_SAGE) { - fail(`hero wordmark "${w.tc}" still computes legacy sage fill ${LEGACY_SAGE} — invisible on light theme`); - } + const heroReact = await page.evaluate(() => { + const hero = document.querySelector('.home-hero'); + const before = {}; + hero.querySelectorAll('svg text').forEach((t) => { + const tc = (t.textContent || '').trim(); + if (tc === 'CORE' || tc === 'SCOPE') before[tc] = getComputedStyle(t).fill; + }); + document.documentElement.style.setProperty('--logo-accent', '#654321'); + document.documentElement.style.setProperty('--logo-accent-hi', '#fedcba'); + const after = {}; + hero.querySelectorAll('svg text').forEach((t) => { + const tc = (t.textContent || '').trim(); + if (tc === 'CORE' || tc === 'SCOPE') after[tc] = getComputedStyle(t).fill; + }); + document.documentElement.style.removeProperty('--logo-accent'); + document.documentElement.style.removeProperty('--logo-accent-hi'); + return { before, after }; + }); + if (heroReact.before.CORE === heroReact.after.CORE) { + fail(`hero CORE fill did not change when --logo-accent was overridden (${heroReact.before.CORE} → ${heroReact.after.CORE})`); } - console.log(` ✅ hero wordmark fills are theme-reactive (${heroWordmarkFills.out.map((w) => w.tc + '=' + w.fill).join(', ')})`); + if (heroReact.before.SCOPE === heroReact.after.SCOPE) { + fail(`hero SCOPE fill did not change when --logo-accent-hi was overridden (${heroReact.before.SCOPE} → ${heroReact.after.SCOPE})`); + } + console.log(` ✅ hero wordmark fills are theme-reactive (CORE ${heroReact.before.CORE}→${heroReact.after.CORE}, SCOPE ${heroReact.before.SCOPE}→${heroReact.after.SCOPE})`); passed++; // 4 & 5. Duotone — CORE fill must differ from SCOPE fill in BOTH navbar