diff --git a/frontend/design-system/preview/components-buttons.html b/frontend/design-system/preview/components-buttons.html
index ca025d3809..b5eaa7b179 100644
--- a/frontend/design-system/preview/components-buttons.html
+++ b/frontend/design-system/preview/components-buttons.html
@@ -92,6 +92,7 @@
}
.chip.is-active .chip__count { opacity:.8; }
+
diff --git a/frontend/src/App.css b/frontend/src/App.css
index b25711db71..390190cb96 100644
--- a/frontend/src/App.css
+++ b/frontend/src/App.css
@@ -162,7 +162,7 @@ body.modern-ui:not(.v2-canvas) input:focus {
box-shadow: 0 0 0 3px rgba(96, 165, 250, 0.25);
}
-body.modern-ui:not(.v2-canvas) button {
+body.modern-ui:not(.v2-canvas):not(.guide-canvas) button {
background-color: #2563eb;
color: #f8fafc;
padding: 12px 20px;
@@ -174,12 +174,12 @@ body.modern-ui:not(.v2-canvas) button {
transition: background-color 0.2s, transform 0.1s;
}
-body.modern-ui:not(.v2-canvas) button:hover {
+body.modern-ui:not(.v2-canvas):not(.guide-canvas) button:hover {
background-color: #1d4ed8;
transform: translateY(-1px);
}
-body.modern-ui:not(.v2-canvas) button:active {
+body.modern-ui:not(.v2-canvas):not(.guide-canvas) button:active {
transform: translateY(1px);
}
diff --git a/frontend/src/components/landing/GuidePage.tsx b/frontend/src/components/landing/GuidePage.tsx
index efab082b34..9e8037a679 100644
--- a/frontend/src/components/landing/GuidePage.tsx
+++ b/frontend/src/components/landing/GuidePage.tsx
@@ -10,9 +10,8 @@ import {
} from '@mui/material';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
-import commonlyLogo from '../../assets/commonly-logo.png';
import guides from '../../content/guides.json';
-import { guidePalette, useGuideCanvas } from './guideShell';
+import { guidePalette, guideTextButtonSx, useGuideCanvas } from './guideShell';
interface GuideStrongParagraph {
strong: string;
@@ -89,7 +88,7 @@ const GuidePage: React.FC = () => {
return (
- } onClick={() => navigate('/')}>
+ } onClick={() => navigate('/')}>
Back to landing
@@ -107,11 +106,11 @@ const GuidePage: React.FC = () => {
- } onClick={() => navigate('/')}>
+ } onClick={() => navigate('/')}>
Back to landing
-
+
Commonly
@@ -194,7 +193,7 @@ const GuidePage: React.FC = () => {
))}
{section.links && (
- {section.links.map((link) => )}
+ {section.links.map((link) => )}
)}
@@ -236,8 +235,8 @@ const GuidePage: React.FC = () => {
// Compare is emitted as a static document. React Router would
// otherwise hand this path to the V2 catch-all instead of
// requesting build/compare/index.html.
- ?
- :
+ ?
+ :
))}
diff --git a/frontend/src/components/landing/GuidesIndexPage.tsx b/frontend/src/components/landing/GuidesIndexPage.tsx
index fca1e32419..b09a6ff705 100644
--- a/frontend/src/components/landing/GuidesIndexPage.tsx
+++ b/frontend/src/components/landing/GuidesIndexPage.tsx
@@ -9,9 +9,8 @@ import {
} from '@mui/material';
import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
-import commonlyLogo from '../../assets/commonly-logo.png';
import guides from '../../content/guides.json';
-import { guidePalette, useGuideCanvas } from './guideShell';
+import { guidePalette, guideTextButtonSx, useGuideCanvas } from './guideShell';
interface Guide {
eyebrow: string;
@@ -29,11 +28,11 @@ const GuidesIndexPage: React.FC = () => {
- } onClick={() => navigate('/')}>
+ } onClick={() => navigate('/')}>
Back to landing
-
+
Commonly
@@ -60,7 +59,7 @@ const GuidesIndexPage: React.FC = () => {
{guide.summary}
- } onClick={() => navigate(`/guides/${id}/`)}>
+ } onClick={() => navigate(`/guides/${id}/`)}>
Read the guide
diff --git a/frontend/src/components/landing/guideShell.ts b/frontend/src/components/landing/guideShell.ts
index 2acbfc6cf4..a2cc5b1058 100644
--- a/frontend/src/components/landing/guideShell.ts
+++ b/frontend/src/components/landing/guideShell.ts
@@ -9,11 +9,17 @@ export const guidePalette = {
textPrimary: '#111827',
textSecondary: '#4b5563',
textTertiary: '#7b8494',
- accent: '#2f6feb',
- accentStrong: '#1f55c9',
- accentSoft: '#e8efff',
- accentText: '#2456c8',
- accentDeep: '#14306f',
+ accent: '#1d3fd1',
+ accentStrong: '#1633a8',
+ accentSoft: '#e9edfb',
+ accentText: '#1d3fd1',
+ accentDeep: '#0f2378',
+} as const;
+
+// Text-button state colors must not fall back to the legacy root MUI theme.
+export const guideTextButtonSx = {
+ color: guidePalette.accentText,
+ '&:hover': { backgroundColor: guidePalette.accentSoft },
} as const;
// Guides render outside V2's component tree, so keep their page canvas light
diff --git a/frontend/src/v2/__tests__/signalAccent.test.ts b/frontend/src/v2/__tests__/signalAccent.test.ts
new file mode 100644
index 0000000000..c97eb4a176
--- /dev/null
+++ b/frontend/src/v2/__tests__/signalAccent.test.ts
@@ -0,0 +1,126 @@
+import fs from 'fs';
+import path from 'path';
+import { guidePalette, guideTextButtonSx } from '../../components/landing/guideShell';
+
+const frontend = path.resolve(__dirname, '../../..');
+const read = (name: string) => fs.readFileSync(path.join(frontend, name), 'utf8');
+const retiredAccent = /#2f6feb|rgba?\(\s*47\s*,\s*111\s*,\s*235/i;
+
+// These standalone assets and legacy bridges cannot all inherit .v2-root.
+// Historical preview cards and stable avatar colors are outside this sweep.
+describe('Signal accent on standalone and bridged surfaces', () => {
+ test('the standalone favicon paints all four mark shapes cobalt', () => {
+ const svg = new DOMParser().parseFromString(read('public/favicon.svg'), 'image/svg+xml');
+ expect(svg.querySelector('parsererror')).toBeNull();
+ const shapes = Array.from(svg.querySelectorAll('path, circle'));
+ expect(shapes).toHaveLength(4);
+ shapes.forEach((shape) => {
+ expect(shape.getAttribute(shape.tagName === 'path' ? 'stroke' : 'fill')).toBe('#1d3fd1');
+ });
+ });
+
+ // Structural source guard: these fixtures prove the absence check can detect old colors.
+ test.each(['color: #2F6FEB;', 'color: rgb(47, 111, 235);', 'background: rgba(47,111,235,.2);'])(
+ 'the structural retired-accent guard detects %s', (fixture) => {
+ expect(fixture).toMatch(retiredAccent);
+ },
+ );
+
+ test.each([
+ 'index.html',
+ 'public/favicon.svg',
+ 'src/components/landing/guideShell.ts',
+ 'src/v2/components/V2FeaturePage.tsx',
+ 'src/v2/components/V2CliAuthorize.css',
+ 'src/v2/v2.css',
+ 'design-system/tokens.css',
+ ])('%s excludes the retired accent and its RGB channels', (name) => {
+ const source = read(name);
+ expect(source).not.toMatch(retiredAccent);
+ });
+
+ test('the static shell uses Signal for browser chrome and SEO accent states', () => {
+ const html = read('index.html');
+ expect(html).toContain('');
+ const expected = [
+ ['a', 'color', '#1d3fd1'], ['a:hover', 'color', '#0f2378'],
+ ['.seo-kicker', 'color', '#1d3fd1'], ['.seo-primary', 'background', '#1d3fd1'],
+ ['.seo-primary:hover', 'background', '#1633a8'],
+ ['.seo-band', 'color', '#0f2378'], ['.seo-band', 'background', '#e9edfb'],
+ ];
+ expected.forEach(([selector, property, value]) => {
+ const rule = html.split(`#seo-page ${selector} {`)[1]?.split('}')[0];
+ expect(rule).toContain(`${property}: ${value};`);
+ });
+ });
+
+ test.each([
+ ['src/components/landing/guideShell.ts', /accent:\s*'#1d3fd1'/],
+ ['src/v2/components/V2FeaturePage.tsx', /accent:\s*'#1d3fd1'/],
+ ['design-system/tokens.css', /::selection\s*\{\s*background:\s*rgba\(29, 63, 209, 0\.20\)/],
+ ['src/v2/v2.css', /\.v2-activity__compose:focus-within\s*\{\s*box-shadow: 0 0 0 4px rgba\(29, 63, 209, 0\.10\)/],
+ ])('%s retains its intended cobalt accent', (name, expected) => {
+ expect(read(name as string)).toMatch(expected as RegExp);
+ });
+
+ test.each([
+ ['src/components/landing/guideShell.ts', ['accentStrong', 'accentSoft', 'accentText', 'accentDeep']],
+ ['src/v2/components/V2FeaturePage.tsx', ['accentStrong', 'accentSoft']],
+ ])('%s uses Signal accent variants', (name, keys) => {
+ const source = read(name as string);
+ const expected: Record = {
+ accentStrong: '#1633a8', accentSoft: '#e9edfb', accentText: '#1d3fd1', accentDeep: '#0f2378',
+ };
+ (keys as string[]).forEach((key) => {
+ expect(source).toContain(`${key}: '${expected[key]}'`);
+ });
+ });
+
+ test('guide buttons are excluded from the legacy blue reset in every interaction state', () => {
+ const css = read('src/App.css');
+ ['', ':hover', ':active'].forEach((state) => {
+ expect(css).toContain(`body.modern-ui:not(.v2-canvas):not(.guide-canvas) button${state} {`);
+ expect(css).not.toContain(`body.modern-ui:not(.v2-canvas) button${state} {`);
+ });
+ });
+
+ test.each(['GuidePage.tsx', 'GuidesIndexPage.tsx'])('%s uses the cobalt mark instead of the legacy PNG', (name) => {
+ const source = read(`src/components/landing/${name}`);
+ expect(source).toContain('src="/favicon.svg"');
+ expect(source).not.toContain('commonly-logo.png');
+ });
+
+ test('guide text buttons bind their hover tint to Signal instead of the root theme', () => {
+ expect(guideTextButtonSx.color).toBe('#1d3fd1');
+ expect(guideTextButtonSx['&:hover'].backgroundColor).toBe(guidePalette.accentSoft);
+ expect(guidePalette.accentSoft).toBe('#e9edfb');
+ ['GuidePage.tsx', 'GuidesIndexPage.tsx'].forEach((name) => {
+ const source = read(`src/components/landing/${name}`);
+ expect(source).toContain('sx={guideTextButtonSx}');
+ expect(source).not.toContain('sx={{ color: guidePalette.accentText }}');
+ });
+ const guidePage = read('src/components/landing/GuidePage.tsx');
+ expect(guidePage).toContain("sx={{ ...guideTextButtonSx, alignSelf: 'flex-start' }}");
+ expect(guidePage).toContain('href={link.path} sx={guideTextButtonSx}>');
+ expect(guidePage).toContain('sx={guideTextButtonSx} onClick={() => navigate(link.path)}>');
+ });
+
+ test('the CLI authorize surface uses shared tokens with standalone fallbacks', () => {
+ const css = read('src/v2/components/V2CliAuthorize.css');
+ expect(css).toContain('var(--v2-accent, #1d3fd1)');
+ expect(css).toContain('var(--v2-focus-ring, 0 0 0 3px rgba(29, 63, 209, .18))');
+ });
+
+ test.each([
+ 'colors-accent.html',
+ 'colors-semantic.html',
+ 'colors-agent-roles.html',
+ 'brand-logo.html',
+ 'components-buttons.html',
+ ])('the legacy %s preview identifies its values as historical', (name) => {
+ const html = read(`design-system/preview/${name}`);
+ expect(html).toContain('Historical, pre-Signal reference.');
+ expect(html).toContain('href="../../../docs/design/signal-identity.md"');
+ });
+
+});
diff --git a/frontend/src/v2/components/V2CliAuthorize.css b/frontend/src/v2/components/V2CliAuthorize.css
index 90ff28b459..d457b39dbd 100644
--- a/frontend/src/v2/components/V2CliAuthorize.css
+++ b/frontend/src/v2/components/V2CliAuthorize.css
@@ -25,19 +25,19 @@
height: 36px;
place-items: center;
border-radius: 10px;
- background: #2f6feb;
+ background: var(--v2-accent, #1d3fd1);
color: #fff;
font-weight: 800;
}
-.v2-cli-authorize__eyebrow { margin: 16px 0 0 !important; color: #2f6feb !important; font-size: 11px; font-weight: 800; letter-spacing: .11em; }
+.v2-cli-authorize__eyebrow { margin: 16px 0 0 !important; color: var(--v2-accent, #1d3fd1) !important; font-size: 11px; font-weight: 800; letter-spacing: .11em; }
.v2-cli-authorize__card h1 { margin: 18px 0 8px; font-size: 24px; }
.v2-cli-authorize__card p { margin: 0 0 20px; color: #4b5563; line-height: 1.5; }
.v2-cli-authorize__field { display: grid; gap: 7px; margin-bottom: 18px; font-size: 13px; font-weight: 700; }
.v2-cli-authorize__field input { width: 100%; box-sizing: border-box; padding: 12px; border: 1px solid #d7dce7; border-radius: 9px; font: 600 18px/1.2 ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: .08em; }
-.v2-cli-authorize__field input:focus { outline: none; border-color: #2f6feb; box-shadow: 0 0 0 3px rgba(47, 111, 235, .18); }
+.v2-cli-authorize__field input:focus { outline: none; border-color: var(--v2-accent, #1d3fd1); box-shadow: var(--v2-focus-ring, 0 0 0 3px rgba(29, 63, 209, .18)); }
.v2-cli-authorize__primary, .v2-cli-authorize__secondary { display: inline-flex; justify-content: center; align-items: center; min-height: 42px; padding: 0 16px; border-radius: 9px; font: inherit; font-weight: 700; text-decoration: none; cursor: pointer; }
-.v2-cli-authorize__primary { border: 1px solid #2f6feb; background: #2f6feb; color: #fff; }
+.v2-cli-authorize__primary { border: 1px solid var(--v2-accent, #1d3fd1); background: var(--v2-accent, #1d3fd1); color: #fff; }
.v2-cli-authorize__primary:disabled { cursor: wait; opacity: .65; }
.v2-cli-authorize__secondary { border: 1px solid #d7dce7; background: #fff; color: #374151; }
.v2-cli-authorize__facts { margin: 0 0 18px; padding: 14px; border-radius: 10px; background: #f7f7fa; }
diff --git a/frontend/src/v2/components/V2FeaturePage.tsx b/frontend/src/v2/components/V2FeaturePage.tsx
index 1c461843f5..ca1625e908 100644
--- a/frontend/src/v2/components/V2FeaturePage.tsx
+++ b/frontend/src/v2/components/V2FeaturePage.tsx
@@ -33,9 +33,9 @@ const V2_COLORS = {
textPrimary: '#111827',
textSecondary: '#4b5563',
textMuted: '#6b7280',
- accent: '#2f6feb',
- accentStrong: '#1f55c9',
- accentSoft: '#e8efff',
+ accent: '#1d3fd1',
+ accentStrong: '#1633a8',
+ accentSoft: '#e9edfb',
success: '#10b981',
warning: '#f4a23a',
danger: '#ef4444',
@@ -50,12 +50,12 @@ const v2FeatureTheme = createTheme({
mode: 'light',
primary: {
main: V2_COLORS.accent,
- light: '#5b8dff',
+ light: '#e9edfb',
dark: V2_COLORS.accentStrong,
contrastText: V2_COLORS.surface,
},
secondary: {
- main: '#14306f',
+ main: '#0f2378',
contrastText: V2_COLORS.surface,
},
success: { main: V2_COLORS.success },
diff --git a/frontend/src/v2/v2.css b/frontend/src/v2/v2.css
index 665496d1d0..1354f28505 100644
--- a/frontend/src/v2/v2.css
+++ b/frontend/src/v2/v2.css
@@ -9677,7 +9677,7 @@ body.modern-ui.v2-canvas {
}
.v2-activity__compose:focus-within {
- box-shadow: 0 0 0 4px rgba(47, 111, 235, 0.10);
+ box-shadow: 0 0 0 4px rgba(29, 63, 209, 0.10);
}
.v2-activity__compose-top,