+
{canUndo && (
) : null}
@@ -963,7 +963,7 @@ export const MessagesTimeline = memo(function MessagesTimeline({
{row.kind === "working" && (
@@ -981,9 +981,9 @@ export const MessagesTimeline = memo(function MessagesTimeline({
)}
-
-
-
+
+
+
)}
@@ -1880,7 +1880,7 @@ const SimpleWorkEntryRow = memo(function SimpleWorkEntryRow(props: {
"group flex w-full max-w-full items-baseline gap-1 text-left transition-opacity duration-150",
compact
? "px-0 py-[1px] hover:opacity-95"
- : "rounded-md border border-border/45 bg-background/65 px-2 py-1 hover:bg-background/80",
+ : "rounded-md border border-[color:var(--app-work-row-border)] bg-[var(--app-work-row-bg)] px-2 py-1 hover:bg-[var(--app-work-row-hover-bg)]",
canOpenEditedDiff ? "cursor-pointer" : "cursor-default",
)}
title={changedFilePath}
@@ -1891,7 +1891,7 @@ const SimpleWorkEntryRow = memo(function SimpleWorkEntryRow(props: {
}}
>
Edited
@@ -1930,7 +1930,7 @@ const SimpleWorkEntryRow = memo(function SimpleWorkEntryRow(props: {
>
@@ -1940,7 +1940,7 @@ const SimpleWorkEntryRow = memo(function SimpleWorkEntryRow(props: {
{subagentMeta ? (
@@ -1961,7 +1961,7 @@ const SimpleWorkEntryRow = memo(function SimpleWorkEntryRow(props: {
{visibleSubagents.length > 0 || hiddenSubagentCount > 0 ? (
@@ -1976,7 +1976,7 @@ const SimpleWorkEntryRow = memo(function SimpleWorkEntryRow(props: {
return (
@@ -1996,14 +1996,14 @@ const SimpleWorkEntryRow = memo(function SimpleWorkEntryRow(props: {
{presentation.nickname ?? primaryLabel}
{presentation.role ? (
-
+
({presentation.role})
) : null}
{secondaryLabel ? (
@@ -2012,11 +2012,11 @@ const SimpleWorkEntryRow = memo(function SimpleWorkEntryRow(props: {
) : null}
{subagent.latestUpdate ? (
-
+
Latest
{subagent.latestUpdate}
@@ -2041,9 +2041,9 @@ const SimpleWorkEntryRow = memo(function SimpleWorkEntryRow(props: {
0 ? (
-
+
+{hiddenSubagentCount} more
) : null}
@@ -2080,7 +2080,7 @@ const SimpleWorkEntryRow = memo(function SimpleWorkEntryRow(props: {
{showIconLeft && (
@@ -2091,13 +2091,13 @@ const SimpleWorkEntryRow = memo(function SimpleWorkEntryRow(props: {
{showInlineWebSearchIcon || showInlineGitHubIcon || showInlineMcpIcon ? (
) : null}
-
+
{displayTextParts ? (
<>
{displayTextParts.action}
@@ -2147,7 +2150,7 @@ const SimpleWorkEntryRow = memo(function SimpleWorkEntryRow(props: {
{showIconRight && (
diff --git a/apps/web/src/index.css b/apps/web/src/index.css
index a34e0703a..d0289ef7e 100644
--- a/apps/web/src/index.css
+++ b/apps/web/src/index.css
@@ -58,12 +58,12 @@
@layer components {
.sidebar-icon-button {
- @apply items-center justify-center rounded-sm p-0.5 text-muted-foreground/60 transition-colors hover:text-foreground/82 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring;
- background: transparent;
+ @apply items-center justify-center rounded-sm p-0.5 text-[var(--app-control-icon-fg,var(--muted-foreground))] transition-colors hover:text-[var(--app-control-icon-hover-fg,var(--foreground))] focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[color:var(--app-state-focus,var(--ring))];
+ background: var(--app-control-icon-bg, transparent);
}
.sidebar-icon-button:hover {
- background: var(--color-background-button-secondary-hover);
+ background: var(--app-control-icon-hover-bg, var(--color-background-button-secondary-hover));
}
/* Let the renderer switch between Codex-style translucent and opaque shells
@@ -234,10 +234,42 @@
--app-surface-card: var(--card);
--app-surface-card-header: var(--muted);
--app-surface-composer: var(--card);
+ --app-surface-toolbar: var(--app-surface-topbar, var(--background));
+ --app-surface-toolbar-hover: var(--app-state-hover, var(--accent));
+ --app-surface-toolbar-active: var(--app-state-selected, var(--accent));
+ --app-surface-toolbar-border: var(--border);
--app-state-hover: var(--accent);
--app-state-selected: var(--sidebar-accent-active);
--app-state-selected-border: var(--primary);
--app-state-focus: var(--ring);
+ --app-text-metadata: var(--muted-foreground);
+ --app-text-metadata-strong: var(--foreground);
+ --app-control-icon-fg: var(--app-text-metadata, var(--muted-foreground));
+ --app-control-icon-hover-fg: var(--app-text-metadata-strong, var(--foreground));
+ --app-control-icon-bg: transparent;
+ --app-control-icon-hover-bg: var(--app-surface-toolbar-hover, var(--accent));
+ --app-control-icon-border: transparent;
+ --app-chrome-control-bg: var(
+ --color-background-control,
+ var(--app-surface-toolbar-hover, var(--accent))
+ );
+ --app-chrome-control-border: var(--app-surface-toolbar-border, var(--border));
+ --app-chrome-control-fg: var(--color-text-foreground-secondary, var(--foreground));
+ --app-chrome-control-hover-bg: var(
+ --color-background-button-secondary-hover,
+ var(--app-surface-toolbar-active, var(--accent))
+ );
+ --app-chrome-control-hover-fg: var(--color-text-foreground, var(--foreground));
+ --app-chrome-control-active-bg: var(
+ --color-background-button-secondary-active,
+ var(--app-state-selected, var(--accent))
+ );
+ --app-metadata-fg: var(--app-text-metadata-strong, var(--foreground));
+ --app-metadata-muted-fg: var(--app-text-metadata, var(--muted-foreground));
+ --app-work-row-bg: var(--app-surface-card, var(--card));
+ --app-work-row-hover-bg: var(--app-surface-card-header, var(--muted));
+ --app-work-row-border: var(--border);
+ --app-work-row-icon: var(--app-text-metadata, var(--muted-foreground));
--app-status-error-bg: var(--destructive);
--app-status-error-border: var(--destructive);
--app-status-error-dot: var(--destructive-foreground);
diff --git a/apps/web/src/theme/theme.logic.test.ts b/apps/web/src/theme/theme.logic.test.ts
index b168faaf3..faaa16b1b 100644
--- a/apps/web/src/theme/theme.logic.test.ts
+++ b/apps/web/src/theme/theme.logic.test.ts
@@ -32,10 +32,33 @@ const REQUIRED_APP_DEPTH_TOKENS = [
"--app-surface-card",
"--app-surface-card-header",
"--app-surface-composer",
+ "--app-surface-toolbar",
+ "--app-surface-toolbar-hover",
+ "--app-surface-toolbar-active",
+ "--app-surface-toolbar-border",
+ "--app-chrome-control-bg",
+ "--app-chrome-control-border",
+ "--app-chrome-control-fg",
+ "--app-chrome-control-hover-bg",
+ "--app-chrome-control-hover-fg",
+ "--app-chrome-control-active-bg",
"--app-state-hover",
"--app-state-selected",
"--app-state-selected-border",
"--app-state-focus",
+ "--app-metadata-fg",
+ "--app-metadata-muted-fg",
+ "--app-text-metadata",
+ "--app-text-metadata-strong",
+ "--app-control-icon-fg",
+ "--app-control-icon-hover-fg",
+ "--app-control-icon-bg",
+ "--app-control-icon-hover-bg",
+ "--app-control-icon-border",
+ "--app-work-row-bg",
+ "--app-work-row-hover-bg",
+ "--app-work-row-border",
+ "--app-work-row-icon",
"--app-status-error-bg",
"--app-status-error-border",
"--app-status-error-dot",
@@ -441,6 +464,33 @@ describe("buildThemeCssVariables", () => {
expect(tokens.derived.textButtonPrimary).not.toBe(tokens.derived.buttonPrimaryBackground);
});
+ it("derives semantic chrome control tokens from non-palette theme math", () => {
+ const importedTheme = parseThemeShareString(PROVIDED_THEME_STRING);
+ const pack = {
+ codeThemeId: importedTheme.codeThemeId,
+ theme: importedTheme.theme,
+ };
+ const tokens = buildResolvedThemeTokens(pack, importedTheme.variant);
+ const cssVariables = buildThemeCssVariables(pack, importedTheme.variant);
+
+ expect(cssVariables.variables["--app-chrome-control-bg"]).toBe(
+ tokens.derived.controlBackground,
+ );
+ expect(cssVariables.variables["--app-chrome-control-border"]).toBe(tokens.derived.borderLight);
+ expect(cssVariables.variables["--app-chrome-control-fg"]).toBe(
+ tokens.derived.textForegroundSecondary,
+ );
+ expect(cssVariables.variables["--app-chrome-control-hover-bg"]).toBe(
+ tokens.derived.buttonSecondaryBackgroundHover,
+ );
+ expect(cssVariables.variables["--app-chrome-control-hover-fg"]).toBe(
+ tokens.derived.textForeground,
+ );
+ expect(cssVariables.variables["--app-chrome-control-active-bg"]).toBe(
+ tokens.derived.buttonSecondaryBackgroundActive,
+ );
+ });
+
it("emits Catppuccin Mocha app-depth tokens from official palette layers", () => {
const cssVariables = buildThemeCssVariables(
{
@@ -455,6 +505,31 @@ describe("buildThemeCssVariables", () => {
expect(cssVariables.variables["--app-surface-topbar"]).toBe("#181825");
expect(cssVariables.variables["--app-surface-card"]).toBe("#1e1e2e");
expect(cssVariables.variables["--app-surface-card-header"]).toBe("#313244");
+ expect(cssVariables.variables["--app-surface-toolbar"]).toBe("#181825");
+ expect(cssVariables.variables["--app-surface-toolbar-hover"]).toBe("#313244");
+ expect(cssVariables.variables["--app-surface-toolbar-active"]).toBe("#45475a");
+ expect(cssVariables.variables["--app-surface-toolbar-border"]).toBe("#45475a");
+ expect(cssVariables.variables["--app-chrome-control-bg"]).toBe("#313244");
+ expect(cssVariables.variables["--app-chrome-control-border"]).toBe("#45475a");
+ expect(cssVariables.variables["--app-chrome-control-fg"]).toBe("#cdd6f4");
+ expect(cssVariables.variables["--app-chrome-control-hover-bg"]).toBe("#45475a");
+ expect(cssVariables.variables["--app-chrome-control-hover-fg"]).toBe("#cdd6f4");
+ expect(cssVariables.variables["--app-chrome-control-active-bg"]).toBe(
+ "rgba(203, 166, 247, 0.14)",
+ );
+ expect(cssVariables.variables["--app-metadata-fg"]).toBe("rgba(205, 214, 244, 0.86)");
+ expect(cssVariables.variables["--app-metadata-muted-fg"]).toBe("rgba(205, 214, 244, 0.62)");
+ expect(cssVariables.variables["--app-text-metadata"]).toBe("rgba(205, 214, 244, 0.62)");
+ expect(cssVariables.variables["--app-text-metadata-strong"]).toBe("rgba(205, 214, 244, 0.86)");
+ expect(cssVariables.variables["--app-control-icon-fg"]).toBe("rgba(205, 214, 244, 0.62)");
+ expect(cssVariables.variables["--app-control-icon-hover-fg"]).toBe("rgba(205, 214, 244, 0.86)");
+ expect(cssVariables.variables["--app-control-icon-bg"]).toBe("transparent");
+ expect(cssVariables.variables["--app-control-icon-hover-bg"]).toBe("#313244");
+ expect(cssVariables.variables["--app-control-icon-border"]).toBe("rgba(69, 71, 90, 0.55)");
+ expect(cssVariables.variables["--app-work-row-bg"]).toBe("rgba(30, 30, 46, 0.82)");
+ expect(cssVariables.variables["--app-work-row-hover-bg"]).toBe("#313244");
+ expect(cssVariables.variables["--app-work-row-border"]).toBe("rgba(69, 71, 90, 0.52)");
+ expect(cssVariables.variables["--app-work-row-icon"]).toBe("rgba(205, 214, 244, 0.48)");
expect(cssVariables.variables["--app-state-hover"]).toBe("#313244");
expect(cssVariables.variables["--app-state-selected"]).toBe("rgba(203, 166, 247, 0.14)");
expect(cssVariables.variables["--app-state-selected-border"]).toBe("#cba6f7");
diff --git a/apps/web/src/theme/theme.logic.ts b/apps/web/src/theme/theme.logic.ts
index 853b6d6ac..455506d97 100644
--- a/apps/web/src/theme/theme.logic.ts
+++ b/apps/web/src/theme/theme.logic.ts
@@ -982,10 +982,25 @@ function buildAppDepthVariables(
"--app-scroll-button-hover-fg": palette.mauve,
"--app-scrollbar-thumb": formatRgba(surface1, variant === "dark" ? 0.72 : 0.64),
"--app-scrollbar-thumb-hover": formatRgba(surface1, variant === "dark" ? 0.92 : 0.82),
+ "--app-chrome-control-bg": palette.surface0,
+ "--app-chrome-control-border": palette.surface1,
+ "--app-chrome-control-fg": pack.theme.ink,
+ "--app-chrome-control-hover-bg": palette.surface1,
+ "--app-chrome-control-hover-fg": pack.theme.ink,
+ "--app-chrome-control-active-bg": formatRgba(accent, variant === "dark" ? 0.14 : 0.12),
+ "--app-control-icon-bg": "transparent",
+ "--app-control-icon-border": formatRgba(surface1, variant === "dark" ? 0.55 : 0.46),
+ "--app-control-icon-fg": formatRgba(parseHexColor(pack.theme.ink), 0.62),
+ "--app-control-icon-hover-bg": palette.surface0,
+ "--app-control-icon-hover-fg": formatRgba(parseHexColor(pack.theme.ink), 0.86),
"--app-state-focus": palette.blue,
"--app-state-hover": palette.surface0,
"--app-state-selected": formatRgba(accent, variant === "dark" ? 0.14 : 0.12),
"--app-state-selected-border": pack.theme.accent,
+ "--app-metadata-fg": formatRgba(parseHexColor(pack.theme.ink), 0.86),
+ "--app-metadata-muted-fg": formatRgba(parseHexColor(pack.theme.ink), 0.62),
+ "--app-text-metadata": formatRgba(parseHexColor(pack.theme.ink), 0.62),
+ "--app-text-metadata-strong": formatRgba(parseHexColor(pack.theme.ink), 0.86),
"--app-status-error-bg": formatRgba(diffRemoved, variant === "dark" ? 0.14 : 0.1),
"--app-status-error-border": formatRgba(diffRemoved, variant === "dark" ? 0.42 : 0.32),
...buildStatusVariables(
@@ -1016,6 +1031,10 @@ function buildAppDepthVariables(
"--app-surface-panel": variant === "dark" ? palette.mantle : "#ffffff",
"--app-surface-sidebar": variant === "dark" ? palette.mantle : palette.mantle,
"--app-surface-topbar": variant === "dark" ? palette.mantle : palette.mantle,
+ "--app-surface-toolbar": variant === "dark" ? palette.mantle : palette.mantle,
+ "--app-surface-toolbar-active": palette.surface1,
+ "--app-surface-toolbar-border": palette.surface1,
+ "--app-surface-toolbar-hover": palette.surface0,
...buildSubagentAccentVariables([
palette.red,
palette.green,
@@ -1032,6 +1051,10 @@ function buildAppDepthVariables(
"--app-terminal-search-match-bg": palette.surface1,
"--app-terminal-search-match-border": palette.blue,
"--app-terminal-search-match-overview": palette.peach,
+ "--app-work-row-bg": formatRgba(parseHexColor(palette.base), 0.82),
+ "--app-work-row-border": formatRgba(surface1, 0.52),
+ "--app-work-row-hover-bg": palette.surface0,
+ "--app-work-row-icon": formatRgba(parseHexColor(pack.theme.ink), 0.48),
"--app-wordmark-prefix": APP_WORDMARK_PREFIX_BLOOD_RED,
};
}
@@ -1115,6 +1138,7 @@ function buildProfileAppDepthVariables(
const accent = parseHexColor(pack.theme.accent);
const diffRemoved = parseHexColor(pack.theme.semanticColors.diffRemoved);
const diffAdded = parseHexColor(pack.theme.semanticColors.diffAdded);
+ const ink = parseHexColor(pack.theme.ink);
const warningColor = profile.warning ?? (variant === "dark" ? "#f5b44a" : "#d97706");
const warning = parseHexColor(warningColor);
const toneLift = getThemeDepthToneLift(profile.tone, variant);
@@ -1180,10 +1204,25 @@ function buildProfileAppDepthVariables(
"--app-scroll-button-hover-fg": pack.theme.accent,
"--app-scrollbar-thumb": resolvedTokens.derived.iconTertiary,
"--app-scrollbar-thumb-hover": resolvedTokens.derived.iconSecondary,
+ "--app-chrome-control-bg": resolvedTokens.derived.controlBackground,
+ "--app-chrome-control-border": resolvedTokens.derived.borderLight,
+ "--app-chrome-control-fg": resolvedTokens.derived.textForegroundSecondary,
+ "--app-chrome-control-hover-bg": resolvedTokens.derived.buttonSecondaryBackgroundHover,
+ "--app-chrome-control-hover-fg": resolvedTokens.derived.textForeground,
+ "--app-chrome-control-active-bg": resolvedTokens.derived.buttonSecondaryBackgroundActive,
+ "--app-control-icon-bg": "transparent",
+ "--app-control-icon-border": resolvedTokens.derived.borderLight,
+ "--app-control-icon-fg": resolvedTokens.derived.textForegroundTertiary,
+ "--app-control-icon-hover-bg": resolvedTokens.derived.buttonSecondaryBackgroundHover,
+ "--app-control-icon-hover-fg": resolvedTokens.derived.textForeground,
"--app-state-focus": resolvedTokens.derived.borderFocus,
"--app-state-hover": resolvedTokens.derived.buttonSecondaryBackgroundHover,
"--app-state-selected": formatRgba(accent, selectedAlpha),
"--app-state-selected-border": pack.theme.accent,
+ "--app-metadata-fg": formatRgba(ink, variant === "dark" ? 0.86 : 0.88),
+ "--app-metadata-muted-fg": formatRgba(ink, variant === "dark" ? 0.62 : 0.66),
+ "--app-text-metadata": formatRgba(ink, variant === "dark" ? 0.62 : 0.66),
+ "--app-text-metadata-strong": formatRgba(ink, variant === "dark" ? 0.86 : 0.88),
"--app-status-error-bg": formatRgba(diffRemoved, variant === "dark" ? 0.12 : 0.08),
"--app-status-error-border": formatRgba(diffRemoved, variant === "dark" ? 0.36 : 0.28),
...buildStatusVariables(
@@ -1222,12 +1261,20 @@ function buildProfileAppDepthVariables(
"--app-surface-panel": panel,
"--app-surface-sidebar": sidebar,
"--app-surface-topbar": topbar,
+ "--app-surface-toolbar": topbar,
+ "--app-surface-toolbar-active": resolvedTokens.derived.buttonSecondaryBackgroundActive,
+ "--app-surface-toolbar-border": resolvedTokens.derived.border,
+ "--app-surface-toolbar-hover": resolvedTokens.derived.buttonSecondaryBackgroundHover,
"--app-terminal-search-active-match-bg": activeSearchBackground,
"--app-terminal-search-active-match-border": warningColor,
"--app-terminal-search-active-match-overview": warningColor,
"--app-terminal-search-match-bg": cardHeader,
"--app-terminal-search-match-border": pack.theme.accent,
"--app-terminal-search-match-overview": pack.theme.semanticColors.diffAdded,
+ "--app-work-row-bg": formatRgba(parseHexColor(panel), variant === "dark" ? 0.82 : 0.88),
+ "--app-work-row-border": resolvedTokens.derived.borderLight,
+ "--app-work-row-hover-bg": resolvedTokens.derived.elevatedSecondary,
+ "--app-work-row-icon": formatRgba(ink, variant === "dark" ? 0.48 : 0.52),
"--app-wordmark-prefix": APP_WORDMARK_PREFIX_BLOOD_RED,
};
}
diff --git a/docs/architecture/README.md b/docs/architecture/README.md
index 6087f0ed6..ed0dc7514 100644
--- a/docs/architecture/README.md
+++ b/docs/architecture/README.md
@@ -15,17 +15,19 @@
## Start Here By Scenario
-| If you are doing this | Start here | Then check |
-| ----------------------------- | ------------------------------------------------------------------------------ | ------------------------------------------------------------ |
-| Getting oriented | [System Overview](system-overview.md) | [`../../AGENTS.md`](../../AGENTS.md) |
-| Checking workspace boundaries | [Runtime Boundaries](runtime-boundaries.md) | [System Overview](system-overview.md) |
-| Changing providers | [Provider Runtime Architecture](provider-runtime.md) | [`../../apps/server/AGENTS.md`](../../apps/server/AGENTS.md) |
-| Changing server boundaries | [Server Architecture Migration Inventory](../server-architecture-migration.md) | [Testing Strategy](../testing/strategy.md) |
-| Recording a durable decision | [ADR Index](../adr/README.md) | [Repo Governance](../governance/repo-governance.md) |
+| If you are doing this | Start here | Then check |
+| ----------------------------- | ------------------------------------------------------------------------------ | --------------------------------------------------------------------- |
+| Getting oriented | [System Overview](system-overview.md) | [`../../AGENTS.md`](../../AGENTS.md) |
+| Checking workspace boundaries | [Runtime Boundaries](runtime-boundaries.md) | [System Overview](system-overview.md) |
+| Changing providers | [Provider Runtime Architecture](provider-runtime.md) | [`../../apps/server/AGENTS.md`](../../apps/server/AGENTS.md) |
+| Changing server boundaries | [Server Architecture Migration Inventory](../server-architecture-migration.md) | [Testing Strategy](../testing/strategy.md) |
+| Changing theme surface tokens | [Theme Surface Token Rules](theme-surface-tokens.md) | [Appearance Regression Testing](../testing/appearance-regressions.md) |
+| Recording a durable decision | [ADR Index](../adr/README.md) | [Repo Governance](../governance/repo-governance.md) |
## Documents
- [System Overview](system-overview.md)
- [Runtime Boundaries](runtime-boundaries.md)
- [Provider Runtime Architecture](provider-runtime.md)
+- [Theme Surface Token Rules](theme-surface-tokens.md)
- [Server Architecture Migration Inventory](../server-architecture-migration.md)
diff --git a/docs/architecture/theme-surface-tokens.md b/docs/architecture/theme-surface-tokens.md
new file mode 100644
index 000000000..d24a2a2b1
--- /dev/null
+++ b/docs/architecture/theme-surface-tokens.md
@@ -0,0 +1,70 @@
+# Theme Surface Token Rules
+
+| Field | Value |
+| --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| Status | Active |
+| Type | Architecture reference |
+| Owner | Engineering |
+| Audience | Web UI engineers, reviewers, and automation agents |
+| Scope | Theme surface, chrome control, metadata, work row, and status token ownership for `apps/web` |
+| Canonical path | `docs/architecture/theme-surface-tokens.md` |
+| Last reviewed | 2026-05-31 |
+| Review cadence | Event-driven; review when theme token derivation, semantic CSS variables, Appearance settings, or shared chrome components change |
+| Source of truth | `apps/web/src/theme/theme.logic.ts`, `apps/web/src/theme/theme.logic.test.ts`, `apps/web/src/index.css`, and components that consume semantic `--app-*` tokens |
+| Verification | Cross-check token derivation and component class usage; keep Appearance contracts aligned with [Appearance Regression Testing](../testing/appearance-regressions.md) |
+
+## Purpose
+
+Theme surface tokens give the web UI one shared language for app depth, compact controls, metadata, work rows, and status markers. They keep theme math in the theme layer and keep component classes focused on semantic intent.
+
+Use this page when changing theme color derivation, adding a visible chrome surface, or reviewing component classes that use `--app-*` CSS variables.
+
+## Token Layers And Ownership
+
+| Layer | Owner | Source | Rule |
+| --------------------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
+| Theme state and seed inputs | Theme logic | `apps/web/src/theme/theme.logic.ts` | `ChromeTheme`, bundled seeds, and seed patch metadata own stored or imported theme values. |
+| Derived app surface tokens | Theme logic | `buildThemeCssVariables`, `buildProfileAppDepthVariables`, Catppuccin branch in `apps/web/src/theme/theme.logic.ts` | Theme code derives semantic `--app-*` variables from the resolved theme, not from component local choices. |
+| CSS component helpers | Base stylesheet | `apps/web/src/index.css` | Shared CSS helpers, such as `.sidebar-icon-button`, may compose semantic tokens with fallbacks for common chrome. |
+| Component classes | React components | `ChatHeader.tsx`, `GitActionsControl.tsx`, `BranchToolbar.tsx`, `MessagesTimeline.tsx`, `MessageActionButton.tsx`, `Sidebar.tsx` | Components should consume semantic tokens that describe the UI role they are rendering. |
+| Token contract tests | Theme tests | `apps/web/src/theme/theme.logic.test.ts` | Tests lock required app depth tokens and example values for derived surfaces, controls, metadata, work rows, and statuses. |
+
+## Surface Families
+
+| Family | Tokens | Use for | Source evidence |
+| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| App surfaces | `--app-surface-canvas`, `--app-surface-sidebar`, `--app-surface-topbar`, `--app-surface-panel`, `--app-surface-card`, `--app-surface-card-header`, `--app-surface-composer`, `--app-surface-toolbar`, `--app-surface-toolbar-hover`, `--app-surface-toolbar-active`, `--app-surface-toolbar-border` | App shell depth, panels, cards, composer surfaces, and compact toolbar chrome | Derived in `apps/web/src/theme/theme.logic.ts`; required by `REQUIRED_APP_DEPTH_TOKENS` in `apps/web/src/theme/theme.logic.test.ts`; used by toolbar and git action classes. |
+| Chrome controls | `--app-chrome-control-bg`, `--app-chrome-control-border`, `--app-chrome-control-fg`, `--app-chrome-control-hover-bg`, `--app-chrome-control-hover-fg`, `--app-chrome-control-active-bg` | Compact controls embedded in app chrome where background, border, foreground, hover, and active states need to move together | Derived in `buildProfileAppDepthVariables`; tested in the Catppuccin token expectations. |
+| Metadata text | `--app-metadata-fg`, `--app-metadata-muted-fg`, `--app-text-metadata`, `--app-text-metadata-strong` | Secondary labels, compact badges, headers, environment labels, and stronger metadata text | Derived from theme ink alpha in `apps/web/src/theme/theme.logic.ts`; used by `BranchToolbar.tsx`. |
+| Control icons | `--app-control-icon-bg`, `--app-control-icon-border`, `--app-control-icon-fg`, `--app-control-icon-hover-bg`, `--app-control-icon-hover-fg` | Small icon buttons and compact message actions | Derived in theme logic; `.sidebar-icon-button` in `apps/web/src/index.css` applies the shared icon background, text, hover, and focus tokens; `MessageActionButton.tsx` adds the shared border token. |
+| Work rows | `--app-work-row-bg`, `--app-work-row-hover-bg`, `--app-work-row-border`, `--app-work-row-icon` | Branch/worktree picker rows plus chat file-change, subagent, tool, environment, handoff, and related work row affordances | Derived from the current panel and ink colors in `apps/web/src/theme/theme.logic.ts`; used by `BranchToolbar.tsx` and `MessagesTimeline.tsx`. |
+| Status tokens | `--app-status-{kind}-fg`, `--app-status-{kind}-dot`, `--app-status-{kind}-bg`, `--app-status-{kind}-border` | Thread, terminal, PR, plan, input, warning, success, working, error, and muted status markers | `APP_STATUS_TOKEN_KINDS` defines `working`, `success`, `warning`, `input`, `plan`, `error`, and `muted`; `buildStatusVariables` emits each state shape; `Sidebar.tsx` consumes status foreground and dot tokens. |
+
+## Component Usage Rules
+
+| Rule | Why | Verify in source |
+| ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
+| Use `--app-surface-*` tokens for app depth, not raw palette names or generic card tokens. | Surface tokens preserve depth differences across custom themes and special palette branches. | `GitActionsControl.tsx` uses toolbar surface, hover, active, and border tokens for header controls. |
+| Use `--app-text-metadata*` or `--app-metadata-*` for secondary chrome text. | Metadata text alpha is derived from theme ink and stays readable across light and dark variants. | `BranchToolbar.tsx` uses metadata text tokens for runtime, environment, and picker labels. |
+| Use `.sidebar-icon-button` or `--app-control-icon-*` for compact icon actions. | Shared control icon tokens keep border, background, hover, and focus behavior consistent. | `apps/web/src/index.css` defines `.sidebar-icon-button`; `MessageActionButton.tsx` composes it with the border token. |
+| Use `--app-work-row-*` for rows that represent a working context. | Work rows need a distinct row background, hover background, border, and icon tint without copying panel math into components. | `BranchToolbar.tsx` uses work row icon and row hover tokens in environment and worktree choices. |
+| Use `--app-status-*` tokens for status meaning, not one-off semantic colors. | Status foreground, dot, background, and border values are emitted as a set for each status kind. | `Sidebar.tsx` uses status tokens for fork, sidechat, terminal, PR, and thread activity markers. |
+
+## Accessibility And Contrast Guardrails
+
+| Guardrail | Reason | Source-grounded check |
+| --------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
+| Keep foreground tokens derived from theme ink, accent, or semantic status colors. | Components should inherit theme contrast decisions instead of hardcoding per-component text colors. | `--app-text-metadata`, `--app-text-metadata-strong`, and `--app-chrome-control-fg` are derived in `apps/web/src/theme/theme.logic.ts`. |
+| Keep hover and active tokens paired with their base surface. | Interaction states must remain visible without changing the component's semantic role. | Toolbar and control hover tokens are emitted beside base toolbar and control tokens. |
+| Keep focus styling tied to `--app-state-focus`. | Keyboard focus should follow the active theme and stay separate from hover or selected states. | `.sidebar-icon-button` in `apps/web/src/index.css` uses `--app-state-focus` for its focus ring. |
+| Do not use status foreground tokens as large backgrounds without the matching background or border token. | Status foreground colors are optimized for marks and text. The generated `bg` and `border` tokens carry the proper alpha for surfaces. | `buildStatusVariables` emits `fg`, `dot`, `bg`, and `border` for each status kind. |
+
+## Testing And Verification Expectations
+
+| Change | Expected verification |
+| --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| Add, rename, or remove a required surface token | Update `REQUIRED_APP_DEPTH_TOKENS` and matching expectations in `apps/web/src/theme/theme.logic.test.ts`. |
+| Change token derivation math | Cross-check `buildThemeCssVariables`, the Catppuccin branch, and `buildProfileAppDepthVariables` for both general themes and special palette handling. |
+| Reroute a visible component class to semantic tokens | Check the touched component class and the shared helper in `apps/web/src/index.css` when applicable. |
+| Change Appearance settings, theme import behavior, or browser-visible token inheritance | Review [Appearance Regression Testing](../testing/appearance-regressions.md) and keep its contract chain current. |
+| Documentation-only edits | Follow `docs/AGENTS.md`: run `git diff --check` and inspect changed relative links. |
diff --git a/docs/testing/README.md b/docs/testing/README.md
index 4e31c392b..ab8dcc6f1 100644
--- a/docs/testing/README.md
+++ b/docs/testing/README.md
@@ -26,4 +26,3 @@
- [Testing Strategy](strategy.md)
- [Appearance Regression Testing](appearance-regressions.md)
-- [Appearance Regression Testing](appearance-regressions.md)