You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by as...@apache.org on 2026/09/01 21:07:12 UTC

(maka) 01/08: refactor(design-system): collapse the ink ladder to two tiers

This is an automated email from the ASF dual-hosted git repository.

Astro-Han pushed a commit to branch fix/3446-secondary-grey
in repository https://gitbox.apache.org/repos/asf/maka.git

commit d84a23bc232c613c9b00eb0834b7d7c45f9481ec
Author: AstroHan <le...@outlook.com>
AuthorDate: Wed Sep 2 04:42:16 2026 +0800

    refactor(design-system): collapse the ink ladder to two tiers
    
    Prose had three greys standing on a 4.5:1 floor that only two of them could
    clear. `--muted-foreground` at 54% ink measured 4.21:1 light and 3.89:1 dark on
    the default palette, and failed AA in light on nine of the eleven palettes
    (onedark 3.4, nord 3.6, catppuccin 3.6) — it sat that low only to stay apart
    from `--foreground-secondary` above it.
    
    So the middle rung goes and muted moves up to 68% ink, one mix for both modes.
    68% puts the default palette on Astryx's own `--color-text-secondary`
    (#525252 / #a3a3a3, 7.8:1 / 7.1:1 — the mix measures #525153 / #a2a2a4,
    7.9:1 / 7.0:1), and every palette then clears AA in both modes, worst onedark
    light at 5.0:1.
    
    Three call sites move UP to `--foreground` instead, because the retired rung was
    carrying state there and flattening it would erase the distinction: the memory
    page's active inject state (its inactive sibling is already muted), plan mode's
    pending step (completed and skipped are the ones that recede), and the diff
    context line (it reads at the same ink as the code it frames).
    
    The contract test is rebuilt around what the invariant is now: neither
    `--foreground-secondary` nor `--foreground-dimmed` may return, as a definition
    or as a call site, anywhere in renderer or @maka/ui CSS. Two names for one grey
    is how dimmed drifted from secondary in the first place.
---
 DESIGN.md                                          |   8 +-
 .../src/main/__tests__/ink-ladder-contract.test.ts | 114 +++++++++++++++++++++
 apps/desktop/src/renderer/README.md                |   2 +-
 apps/desktop/src/renderer/maka-tokens.css          |  74 ++++++-------
 apps/desktop/src/renderer/styles/agent-graph.css   |   2 +-
 apps/desktop/src/renderer/styles/composer.css      |   6 +-
 apps/desktop/src/renderer/styles/daily-review.css  |   2 +-
 apps/desktop/src/renderer/styles/deep-research.css |  10 +-
 apps/desktop/src/renderer/styles/error.css         |   2 +-
 apps/desktop/src/renderer/styles/help.css          |   4 +-
 apps/desktop/src/renderer/styles/hero.css          |   2 +-
 .../desktop/src/renderer/styles/model-switcher.css |   2 +-
 .../src/renderer/styles/module-pages/mcp.css       |   2 +-
 .../renderer/styles/module-pages/module-shell.css  |   2 +-
 .../src/renderer/styles/module-pages/skills.css    |   2 +-
 apps/desktop/src/renderer/styles/plan-mode.css     |  12 ++-
 apps/desktop/src/renderer/styles/search-modal.css  |   2 +-
 .../src/renderer/styles/session-todo-panel.css     |   4 +-
 .../desktop/src/renderer/styles/settings/about.css |   2 +-
 apps/desktop/src/renderer/styles/settings/bot.css  |   6 +-
 apps/desktop/src/renderer/styles/settings/form.css |   4 +-
 .../src/renderer/styles/settings/health.css        |   4 +-
 .../src/renderer/styles/settings/memory.css        |  20 ++--
 .../src/renderer/styles/settings/permission.css    |   2 +-
 apps/desktop/src/renderer/styles/settings/rows.css |   4 +-
 .../src/renderer/styles/settings/runtime-host.css  |  22 ++--
 .../src/renderer/styles/settings/web-search.css    |   2 +-
 apps/desktop/src/renderer/styles/shell-layout.css  |   6 +-
 apps/desktop/src/renderer/styles/sidebar.css       |   4 +-
 .../src/renderer/styles/workbar/artifacts.css      |  10 +-
 .../src/renderer/styles/workbar/inspector.css      |  10 +-
 apps/desktop/src/renderer/styles/workbar/shell.css |   2 +-
 packages/ui/src/styles.css                         |  44 ++++----
 .../ui/stories/composite-row-states.stories.tsx    |   2 +-
 packages/ui/stories/functional-motion.stories.tsx  |   4 +-
 packages/ui/stories/icons.stories.tsx              |   8 +-
 packages/ui/stories/palette-matrix.stories.tsx     |   2 +-
 37 files changed, 269 insertions(+), 141 deletions(-)

diff --git a/DESIGN.md b/DESIGN.md
index 452e810153..e1c4f00cbb 100644
--- a/DESIGN.md
+++ b/DESIGN.md
@@ -108,12 +108,12 @@ Depth is a ladder, not a decoration. Every background resolves to one of four se
 
 ## 3. Ink
 
-Prose uses exactly three tiers — `--foreground`, `--foreground-secondary`, `--muted-foreground` — spaced at an even ~2× contrast rhythm against `--surface-raised`, every one of them clearing WCAG AA in both modes. Any new tier must hold that rhythm and that floor.
+Prose uses exactly two tiers — `--foreground` and `--muted-foreground` — which is Astryx's own shape. `--muted-foreground` is 68% ink in `oklch`, one mix for both modes, chosen so the default palette lands on Astryx's `--color-text-secondary` (`#525252` / `#a3a3a3`, 7.8:1 / 7.1:1) and so all eleven palettes clear WCAG AA in both modes — the worst is onedark light at 5.0:1. Any new tier must hold that floor on every palette, which is exactly what the retired middle rung could not do.
 
-- **The Three-Tier Reading Rule.** Prose uses primary, secondary, or muted. Neutral washes are surfaces, not extra text tiers. `--foreground-dimmed` is retired — its call sites name secondary directly — and must never come back with a definition of its own (contract-tested).
+- **The Two-Tier Reading Rule.** Prose is primary or muted. Hierarchy below that comes from size, weight and case — never a third grey. Neutral washes are surfaces, not extra text tiers. `--foreground-secondary` and `--foreground-dimmed` are retired and must not return, as a definition or as a call site (contract-tested). Both retirements are the same lesson: dimmed and secondary were both written as "80% ink" in different colorspaces and resolved 10.51:1 against 12.01:1, and secondary t [...]
 - **The One Colorspace Rule.** Every derivation inside a token family uses one colorspace (`oklch` for ink, contract-tested). Mixing `srgb` and `oklch` derivations produces "same literal, different value" drift, and the ladder is where it bites hardest because its rungs only mean anything relative to each other.
-- **Astryx's `secondary` is not this ladder's secondary.** `Text color="secondary"` reads `--color-text-secondary`, which resolves to a fixed neutral literal outside the containers `astryx-mount.css` bridges and to `--muted-foreground` inside them. Neither is `--foreground-secondary`. One word, three results — so pick an ink tier by the token, and treat a primitive's color prop as its own vocabulary.
-- **`--color-text-disabled` is a deliberate exemption, not an oversight.** Astryx's `light-dark(#a3a3a3, #525252)` measures 2.52:1 / 2.29:1 — under the AA floor the three prose tiers hold. It stays: a disabled control read at prose contrast stops reading as disabled, and the versions of this token that clear 4.5:1 looked wrong beside the enabled rows they sit in. It is not prose and it is not a fourth tier; it is the one place the floor is traded for the state being legible *as a state*. [...]
+- **Astryx text/icon primary and secondary ARE this ladder.** `makaTheme.ts` tokens point `--color-text-primary` / `--color-icon-primary` at `--foreground` and `--color-text-secondary` / `--color-icon-secondary` at `--muted-foreground`, at the theme root. No bridge, no third value: `Text color="secondary"` and `color: var(--muted-foreground)` are the same grey everywhere in the app.
+- **`--color-text-disabled` is a deliberate exemption, not an oversight.** Astryx's `light-dark(#a3a3a3, #525252)` measures 2.52:1 / 2.29:1 — under the AA floor the two prose tiers hold. It stays: a disabled control read at prose contrast stops reading as disabled, and the versions of this token that clear 4.5:1 looked wrong beside the enabled rows they sit in. It is not prose and it is not a third tier; it is the one place the floor is traded for the state being legible *as a state*. Do [...]
 - **Links use the solid accent tier** (`--accent-solid`), never raw `--accent` — the accent identifies interaction, and the solid tier is the only accent variant that clears text contrast on every palette. A selection wash or outline is not link text and derives from `--accent` instead — deriving a surface from the link name lets a text-contrast rule silently govern a background.
 
 ## 4. Borders
diff --git a/apps/desktop/src/main/__tests__/ink-ladder-contract.test.ts b/apps/desktop/src/main/__tests__/ink-ladder-contract.test.ts
new file mode 100644
index 0000000000..0d6eaf221f
--- /dev/null
+++ b/apps/desktop/src/main/__tests__/ink-ladder-contract.test.ts
@@ -0,0 +1,114 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import assert from 'node:assert/strict';
+import { readdir, readFile } from 'node:fs/promises';
+import { dirname, join, relative, resolve } from 'node:path';
+import { describe, it } from 'node:test';
+import { fileURLToPath } from 'node:url';
+
+/**
+ * The ink ladder has exactly TWO prose tiers, and the retired names stay gone.
+ *
+ * Both retirements are the same failure. `--foreground-dimmed` and
+ * `--foreground-secondary` were both written as "80% ink" while one mixed in
+ * srgb and the other in oklch, so the identical words resolved 10.51:1 against
+ * 12.01:1 — two names for one grey, drifting because nothing said they had to
+ * agree. Then secondary itself proved to be a rung the ladder could not hold:
+ * with an AA floor under it, muted had to sit at 54% ink to stay distinguishable
+ * and failed 4.5:1 on nine of the eleven palettes in light mode (issue #3446).
+ *
+ * So the invariant is not "dimmed must alias secondary" any more — it is that
+ * neither name comes back at all, as a definition or as a `var()` reference.
+ * A reintroduced call site is how a third grey re-enters: someone needs a rung
+ * between primary and muted, writes the old name, and CSS silently resolves it
+ * to nothing (or, worse, to a fresh local definition that drifts again).
+ *
+ * Asserted on the source text rather than a rendered page: the point is that
+ * the names have no definition to drift from, and a screenshot would only prove
+ * that today's pixels happen to match.
+ */
+const testDir = dirname(fileURLToPath(import.meta.url));
+// From either src/main/__tests__ or dist/main/__tests__ this lands on
+// apps/desktop; the tests run from dist, so it must hold for both.
+const DESKTOP_ROOT = resolve(testDir, '../../..');
+const REPO_ROOT = resolve(DESKTOP_ROOT, '../..');
+
+const TOKENS_PATH = join(DESKTOP_ROOT, 'src', 'renderer', 'maka-tokens.css');
+const CSS_ROOTS = [
+  join(DESKTOP_ROOT, 'src', 'renderer'),
+  join(REPO_ROOT, 'packages', 'ui', 'src'),
+];
+
+const RETIRED_INK = ['--foreground-secondary', '--foreground-dimmed'];
+
+async function cssFilesUnder(root: string): Promise<string[]> {
+  const entries = await readdir(root, { withFileTypes: true });
+  const found: string[] = [];
+  for (const entry of entries) {
+    const full = join(root, entry.name);
+    if (entry.isDirectory()) {
+      found.push(...(await cssFilesUnder(full)));
+    } else if (entry.name.endsWith('.css')) {
+      found.push(full);
+    }
+  }
+  return found;
+}
+
+describe('ink ladder', () => {
+  it('keeps the retired ink names out of product CSS', async () => {
+    const files = (await Promise.all(CSS_ROOTS.map(cssFilesUnder))).flat();
+    assert.ok(files.length > 0, 'found no CSS to check — the roots moved');
+
+    const offenders: string[] = [];
+    for (const file of files) {
+      const css = await readFile(file, 'utf8');
+      // Comments may name a retired token to explain WHY it is retired; a
+      // declaration or a var() reference is what actually revives it.
+      for (const name of RETIRED_INK) {
+        const live = new RegExp(`(^\\s*${name}\\s*:)|(var\\(\\s*${name}\\s*[,)])`, 'm');
+        if (live.test(css)) {
+          offenders.push(`${relative(REPO_ROOT, file)} → ${name}`);
+        }
+      }
+    }
+
+    assert.deepEqual(
+      offenders,
+      [],
+      'prose has two tiers: --foreground and --muted-foreground. A third grey is how dimmed and secondary drifted apart before',
+    );
+  });
+
+  it('derives every ink tier in one colour space', async () => {
+    const css = await readFile(TOKENS_PATH, 'utf8');
+    // A tier mixed in srgb while its siblings mix in oklch is the same defect
+    // in a different costume, and it is invisible in review because the
+    // percentages match.
+    const srgbInk = [
+      ...css.matchAll(/^\s*(--[a-z-]*foreground[a-z-]*):\s*color-mix\(in srgb[^;]*;/gm),
+    ];
+    assert.deepEqual(
+      srgbInk.map((match) => match[1]),
+      [],
+      'ink tiers derive in oklch; an srgb mix gives the same words a different colour',
+    );
+  });
+});
diff --git a/apps/desktop/src/renderer/README.md b/apps/desktop/src/renderer/README.md
index c9ea51eaca..de57d720a3 100644
--- a/apps/desktop/src/renderer/README.md
+++ b/apps/desktop/src/renderer/README.md
@@ -177,7 +177,7 @@ Composer mount semantics, Session switching, or Workbar resource lifecycles.
 
 Token authoring rule: custom CSS variables go in `maka-tokens.css`. New component-local vars should carry `/* local: ... */` (existing ones don't all have it yet). No new hardcoded color / radius / z-index.
 
-Note the `--foreground-N` split: the wash stops (`-2/-3/-5/-8/-10`) are surface fills for backgrounds and borders, **not** text. The 3-tier semantic aliases (`--foreground` / `--foreground-secondary` / `--muted-foreground`) are the text-color vocabulary. They are separate concerns — don't collapse the wash stops into the text aliases.
+Note the `--foreground-N` split: the wash stops (`-2/-3/-5/-8/-10`) are surface fills for backgrounds and borders, **not** text. The two semantic aliases (`--foreground` / `--muted-foreground`) are the text-color vocabulary. They are separate concerns — don't collapse the wash stops into the text aliases.
 
 ## New code: primitive first, CSS last
 
diff --git a/apps/desktop/src/renderer/maka-tokens.css b/apps/desktop/src/renderer/maka-tokens.css
index 1e15005fe7..c4b9b90ddd 100644
--- a/apps/desktop/src/renderer/maka-tokens.css
+++ b/apps/desktop/src/renderer/maka-tokens.css
@@ -146,19 +146,15 @@
      built on. zinc-family hue 286 at the same chroma reads clean and
      cool like the reference sidebar. */
   --foreground: oklch(0.17 0.005 286);
-  /* Alias, not a tier. It was written as "80% ink" like --foreground-secondary
-     but mixed in srgb where that one mixes in oklch, so the same words gave
-     two different colours (10.51:1 vs 12.01:1 measured). One family, one
-     colour space: derive every ink in oklch. */
-  /* `--foreground-dimmed` is gone as of Visual System 2.0 (T4). It was an alias
-     of --foreground-secondary kept alive for five call sites in the former
-     task-ledger.css (now session-todo-panel.css), which were migrated to name
-     the tier directly. Two names for one ink
-     tier is how the original bug happened: both were written as "80% ink",
-     one mixed in srgb and the other in oklch, and they resolved 10.51:1 against
-     12.01:1. The contract test still runs — it now asserts the token has no
-     independent definition, which an absent token satisfies and a
-     reintroduced color-mix does not. */
+  /* `--foreground-dimmed` and `--foreground-secondary` are both retired, and
+     the contract test keeps them out. They are the same lesson twice: dimmed
+     and secondary were both written as "80% ink", one mixed in srgb and the
+     other in oklch, so the identical words resolved 10.51:1 against 12.01:1;
+     then secondary itself turned out to be a tier nobody could hold apart from
+     muted without one of the two failing AA. Two names for one grey is how the
+     drift starts. Prose has exactly two rungs now — --foreground and
+     --muted-foreground — and everything below that is size, weight and case.
+     See the ink ladder note further down for the numbers. */
 
   /* Brand accent = logo blue (oklch L0.70 C0.135 h250), sampled from
      apps/desktop/assets/icon.png (owner decision 2026-07-03). --success stays
@@ -234,8 +230,8 @@
   /* === foreground solid mix scale ===
      foreground-N = N% of foreground interpolated INTO background.
      These are surface wash stops for backgrounds, borders, and other
-     non-text surfaces only. Text color uses --foreground (primary),
-     --foreground-secondary, or --muted-foreground — never these. */
+     non-text surfaces only. Text color uses --foreground (primary) or
+     --muted-foreground — never these. */
   --foreground-2:  color-mix(in oklch, var(--foreground) 2%,  var(--background));
   --foreground-3:  color-mix(in oklch, var(--foreground) 3%,  var(--background));
   --foreground-5:  color-mix(in oklch, var(--foreground) 5%,  var(--background));
@@ -247,11 +243,12 @@
       30% ink fails 4.5:1); decorative uses inlined their color-mix.
       Fewer tiers = crisper hierarchy.
       P-FOREGROUND-TIER-CONVERGE (issue #430 PR4, 2026-07-03): the 5-step
-      text ladder (40/50/60/70/80) collapsed to 3 semantic tiers —
-      primary (var(--foreground), 100% ink), secondary (--foreground-
-      secondary, 80% ink), muted (--muted-foreground, 50% ink). The raw
-      mix stops are gone; the aliases ARE the definitions. Text call
-      sites use the semantic aliases only. */
+      text ladder (40/50/60/70/80) collapsed to semantic tiers, and
+      issue #3446 collapsed those to the two that survive —
+      primary (var(--foreground), 100% ink) and muted
+      (--muted-foreground, 68% ink). The raw mix stops are gone; the
+      aliases ARE the definitions. Text call sites use the semantic
+      aliases only. */
 
   /* === alpha overlays via oklch(from ...) ===
      Use these when stacking on a tinted surface (e.g. a card on background-elevated).
@@ -288,18 +285,23 @@
   --border-width-hairline: 1px;
   --border-width-thick:    2px;
   --muted:         oklch(from var(--foreground) l c h / 0.05);
-  /* Ink ladder (visual system 2.0, T1): three tiers about 2x apart, all above
-     4.5:1. Measured against --surface-raised, light reads 19.1 / 9.8 / 4.8.
-
-     The audit asked for a 3x gap, copied from a reference that reaches 3.6x
-     with only TWO text tiers. Three tiers standing on a 4.5:1 AA floor cannot
-     span 3x without the bottom two colliding, so the rhythm is even instead:
-     roughly geometric, which is what three tiers can actually hold.
-
-     50% muted was the one real defect — 4.21:1 in light and 3.89:1 in dark,
-     already under AA in the mode nobody checked. */
-  --muted-foreground: color-mix(in oklch, var(--foreground) 54%, var(--background));
-  --foreground-secondary: color-mix(in oklch, var(--foreground) 74%, var(--background));
+  /* Ink ladder (issue #3446): TWO tiers, --foreground and --muted-foreground,
+     which is Astryx's own shape. One mix serves both modes.
+
+     Three tiers standing on a 4.5:1 AA floor never had room to space
+     themselves: 54% muted measured 4.21:1 light and 3.89:1 dark on the default
+     palette, and across the eleven palettes it failed AA in nine of them in
+     light — onedark 3.4, nord 3.6, catppuccin 3.6. That is the direct reason
+     the middle rung goes: a ladder whose bottom rung is illegible on most
+     palettes is not a hierarchy, it is a bug with a token name.
+
+     68% is chosen so the DEFAULT palette lands on Astryx's own
+     --color-text-secondary (#525252 light / #a3a3a3 dark, 7.8:1 / 7.1:1):
+     68% ink measures #525153 / #a2a2a4, 7.9:1 / 7.0:1. Every one of the eleven
+     palettes then clears AA in both modes; the worst is onedark light at
+     5.0:1. Hierarchy below these two rungs comes from size, weight and case —
+     never a third grey. */
+  --muted-foreground: color-mix(in oklch, var(--foreground) 68%, var(--background));
   --state-hover-bg:    oklch(from var(--foreground) l c h / 0.04);
   --state-selected-bg: oklch(from var(--foreground) l c h / 0.065);
   /* PR5 FOREGROUND-ALPHA-TOKEN-0: raw foreground-alpha tiers for component
@@ -936,9 +938,11 @@
   --background-elevated: var(--surface-overlay);
   /* 0.92 measured 14.11:1, just under the 15:1 the reading tier asks for. */
   --foreground: oklch(0.95 0.004 286);
-  /* Dark's ink-to-surface gap differs, so one percentage cannot put secondary
-     in the 8.5-10:1 band for both modes: 74% gives 9.8 light but 8.2 dark. */
-  --foreground-secondary: color-mix(in oklch, var(--foreground) 78%, var(--background));
+  /* No dark override for the muted rung: 68% ink is the one mix that puts both
+     modes on Astryx's --color-text-secondary (7.9:1 light, 7.0:1 dark on the
+     default palette). The per-mode percentage that used to live here existed
+     to keep a THIRD tier from colliding with muted; with the middle rung gone
+     there is nothing left to separate. */
 
   --accent: oklch(0.74 0.15 250);
   /* Dark mode keeps the same pale-blue CTA chip + deep-blue text as light
diff --git a/apps/desktop/src/renderer/styles/agent-graph.css b/apps/desktop/src/renderer/styles/agent-graph.css
index 8049ee693b..2316f31fb2 100644
--- a/apps/desktop/src/renderer/styles/agent-graph.css
+++ b/apps/desktop/src/renderer/styles/agent-graph.css
@@ -191,7 +191,7 @@
   font: var(--maka-text-supporting);
   min-width: 0;
   overflow: hidden;
-  color: var(--foreground-secondary);
+  color: var(--muted-foreground);
   text-overflow: ellipsis;
   white-space: nowrap;
 }
diff --git a/apps/desktop/src/renderer/styles/composer.css b/apps/desktop/src/renderer/styles/composer.css
index 6841a8ef8d..1e7af4adcf 100644
--- a/apps/desktop/src/renderer/styles/composer.css
+++ b/apps/desktop/src/renderer/styles/composer.css
@@ -311,7 +311,7 @@
   border: var(--border-width-hairline) solid transparent;
   border-radius: var(--radius-pill);
   background: transparent;
-  color: var(--foreground-secondary);
+  color: var(--muted-foreground);
   white-space: nowrap;
   max-width: min(180px, 28vw);
   transition:
@@ -392,7 +392,7 @@
   padding: var(--space-1) var(--space-2-5);
   border: var(--border-width-hairline) solid var(--warning);
   border-radius: var(--radius-control);
-  color: var(--foreground-secondary);
+  color: var(--muted-foreground);
 }
 
 .maka-composer-no-model-hint-action {
@@ -435,7 +435,7 @@
   border: var(--border-width-hairline) solid var(--border);
   border-radius: var(--radius-pill);
   background: var(--background-elevated);
-  color: var(--foreground-secondary);
+  color: var(--muted-foreground);
 }
 
 .maka-composer-revision-notice > svg {
diff --git a/apps/desktop/src/renderer/styles/daily-review.css b/apps/desktop/src/renderer/styles/daily-review.css
index fb88127585..614f5a7750 100644
--- a/apps/desktop/src/renderer/styles/daily-review.css
+++ b/apps/desktop/src/renderer/styles/daily-review.css
@@ -90,7 +90,7 @@
 }
 
 .maka-daily-review-report-prose {
-  color: var(--foreground-secondary);
+  color: var(--muted-foreground);
   overflow-wrap: anywhere;
 }
 
diff --git a/apps/desktop/src/renderer/styles/deep-research.css b/apps/desktop/src/renderer/styles/deep-research.css
index 75a545af71..d66de3c1cd 100644
--- a/apps/desktop/src/renderer/styles/deep-research.css
+++ b/apps/desktop/src/renderer/styles/deep-research.css
@@ -45,7 +45,7 @@
 .maka-deep-research-workflow-body {
   font: var(--maka-text-body);
   display: block;
-  color: var(--foreground-secondary);
+  color: var(--muted-foreground);
 }
 
 .maka-deep-research-report,
@@ -104,7 +104,7 @@
 .maka-deep-research-progress-body {
   font: var(--maka-text-body);
   min-width: 0;
-  color: var(--foreground-secondary);
+  color: var(--muted-foreground);
 }
 
 .maka-deep-research-run-panel {
@@ -140,7 +140,7 @@
 .maka-deep-research-run-summary span,
 .maka-deep-research-run-panel p {
   font: var(--maka-text-supporting);
-  color: var(--foreground-secondary);
+  color: var(--muted-foreground);
 }
 
 .maka-deep-research-run-actions {
@@ -206,7 +206,7 @@
 .maka-deep-research-run-grid h3 {
   font: var(--maka-text-heading-5);
   margin: 0 0 var(--space-1-5);
-  color: var(--foreground-secondary);
+  color: var(--muted-foreground);
 }
 
 .maka-deep-research-run-grid ul {
@@ -222,7 +222,7 @@
   display: flex;
   gap: var(--space-1-5);
   min-width: 0;
-  color: var(--foreground-secondary);
+  color: var(--muted-foreground);
 }
 
 .maka-deep-research-run-grid li[data-status="completed"] > span {
diff --git a/apps/desktop/src/renderer/styles/error.css b/apps/desktop/src/renderer/styles/error.css
index 5f95c1f648..4fae62d399 100644
--- a/apps/desktop/src/renderer/styles/error.css
+++ b/apps/desktop/src/renderer/styles/error.css
@@ -72,7 +72,7 @@
 .maka-error-copy p {
     font: var(--maka-text-body);
     margin: 0;
-    color: var(--foreground-secondary);
+    color: var(--muted-foreground);
     max-width: 56ch;
   }
 
diff --git a/apps/desktop/src/renderer/styles/help.css b/apps/desktop/src/renderer/styles/help.css
index 2f5b189f64..af7e349252 100644
--- a/apps/desktop/src/renderer/styles/help.css
+++ b/apps/desktop/src/renderer/styles/help.css
@@ -34,7 +34,7 @@
 .maka-help-section h3 {
   font: var(--maka-text-heading-5);
   margin: 0 0 var(--space-1);
-  color: var(--foreground-secondary);
+  color: var(--muted-foreground);
   letter-spacing: var(--tracking-normal);
   text-transform: none;
 }
@@ -52,7 +52,7 @@
 
 .maka-help-section dt {
   font: var(--maka-text-supporting);
-  color: var(--foreground-secondary);
+  color: var(--muted-foreground);
 }
 
 .maka-help-section dd {
diff --git a/apps/desktop/src/renderer/styles/hero.css b/apps/desktop/src/renderer/styles/hero.css
index 41d6308e13..f9b2e259a5 100644
--- a/apps/desktop/src/renderer/styles/hero.css
+++ b/apps/desktop/src/renderer/styles/hero.css
@@ -107,7 +107,7 @@
     font: var(--maka-text-body);
     margin: 0;
     max-width: 54ch;
-    color: var(--foreground-secondary);
+    color: var(--muted-foreground);
     text-wrap: pretty;
   }
 /* #1433: the daily empty chat is a returning user's landing surface, not a
diff --git a/apps/desktop/src/renderer/styles/model-switcher.css b/apps/desktop/src/renderer/styles/model-switcher.css
index c3c3d61550..06b95b61a2 100644
--- a/apps/desktop/src/renderer/styles/model-switcher.css
+++ b/apps/desktop/src/renderer/styles/model-switcher.css
@@ -40,7 +40,7 @@
   padding-block: var(--space-1);
   padding-inline: var(--space-2);
   font: var(--maka-text-supporting);
-  color: var(--foreground-secondary);
+  color: var(--muted-foreground);
   user-select: none;
   white-space: nowrap;
 }
diff --git a/apps/desktop/src/renderer/styles/module-pages/mcp.css b/apps/desktop/src/renderer/styles/module-pages/mcp.css
index 7fde363b6a..4627d5f7b4 100644
--- a/apps/desktop/src/renderer/styles/module-pages/mcp.css
+++ b/apps/desktop/src/renderer/styles/module-pages/mcp.css
@@ -98,7 +98,7 @@
   padding: var(--space-1) var(--space-2);
   border-radius: var(--radius-control);
   background: var(--muted);
-  color: var(--foreground-secondary);
+  color: var(--muted-foreground);
 }
 
 .maka-mcp-stderr {
diff --git a/apps/desktop/src/renderer/styles/module-pages/module-shell.css b/apps/desktop/src/renderer/styles/module-pages/module-shell.css
index 479b80f677..5832c497a2 100644
--- a/apps/desktop/src/renderer/styles/module-pages/module-shell.css
+++ b/apps/desktop/src/renderer/styles/module-pages/module-shell.css
@@ -200,7 +200,7 @@
   /* A square icon plate is a proportional mark (DESIGN.md §6) — ratio radius. */
   border-radius: var(--radius-plate);
   background: var(--muted);
-  color: var(--foreground-secondary);
+  color: var(--muted-foreground);
 }
 
 .maka-module-market-icon span {
diff --git a/apps/desktop/src/renderer/styles/module-pages/skills.css b/apps/desktop/src/renderer/styles/module-pages/skills.css
index 0048461d3d..7bf4f54356 100644
--- a/apps/desktop/src/renderer/styles/module-pages/skills.css
+++ b/apps/desktop/src/renderer/styles/module-pages/skills.css
@@ -66,7 +66,7 @@
   white-space: nowrap;
   border-radius: var(--radius-pill);
   background: oklch(from var(--foreground) l c h / 0.05);
-  color: var(--foreground-secondary);
+  color: var(--muted-foreground);
   padding: var(--space-0-5) var(--space-2);
 }
 
diff --git a/apps/desktop/src/renderer/styles/plan-mode.css b/apps/desktop/src/renderer/styles/plan-mode.css
index 5c7079f9b9..98de47379c 100644
--- a/apps/desktop/src/renderer/styles/plan-mode.css
+++ b/apps/desktop/src/renderer/styles/plan-mode.css
@@ -88,7 +88,7 @@
 .plan-proposal-overview {
   font: var(--maka-text-body);
   margin: 0;
-  color: var(--foreground-secondary);
+  color: var(--muted-foreground);
 }
 
 .plan-proposal-section {
@@ -99,7 +99,7 @@
 .plan-proposal-section h3 {
   font: var(--maka-text-heading-5);
   margin: 0;
-  color: var(--foreground-secondary);
+  color: var(--muted-foreground);
 }
 
 .plan-proposal-steps,
@@ -152,7 +152,7 @@
 .plan-proposal-step-content p {
   margin: 0;
   overflow-wrap: anywhere;
-  color: var(--foreground-secondary);
+  color: var(--muted-foreground);
 }
 
 .plan-proposal-risks ul {
@@ -262,7 +262,11 @@
   gap: var(--space-2);
   min-height: var(--h-control-lg);
   padding: var(--space-1-5) var(--space-2);
-  color: var(--foreground-secondary);
+  /* Full ink: a pending step is live work, and the two states below (completed
+     and skipped) are the ones that recede to muted. The retired middle grey
+     used to hold this apart from them; collapsing it onto muted instead would
+     make every step in the list read as already dealt with. */
+  color: var(--foreground);
 }
 
 .plan-execution-steps li + li {
diff --git a/apps/desktop/src/renderer/styles/search-modal.css b/apps/desktop/src/renderer/styles/search-modal.css
index b8119502fc..08ae47f341 100644
--- a/apps/desktop/src/renderer/styles/search-modal.css
+++ b/apps/desktop/src/renderer/styles/search-modal.css
@@ -49,7 +49,7 @@
   font: var(--maka-text-supporting);
   display: -webkit-box;
   overflow: hidden;
-  color: var(--foreground-secondary);
+  color: var(--muted-foreground);
   -webkit-box-orient: vertical;
   -webkit-line-clamp: 3;
 }
diff --git a/apps/desktop/src/renderer/styles/session-todo-panel.css b/apps/desktop/src/renderer/styles/session-todo-panel.css
index a0e53afc6d..f4df8be5ac 100644
--- a/apps/desktop/src/renderer/styles/session-todo-panel.css
+++ b/apps/desktop/src/renderer/styles/session-todo-panel.css
@@ -55,7 +55,7 @@
   min-height: 32px;
   padding: var(--space-1) var(--space-2);
   border-radius: var(--radius-control);
-  color: var(--foreground-secondary);
+  color: var(--muted-foreground);
 }
 
 .maka-session-todo-row > span {
@@ -72,7 +72,7 @@
   justify-content: space-between;
   min-height: 32px;
   padding: var(--space-1) var(--space-2);
-  color: var(--foreground-secondary);
+  color: var(--muted-foreground);
 }
 
 .maka-session-todo-retry {
diff --git a/apps/desktop/src/renderer/styles/settings/about.css b/apps/desktop/src/renderer/styles/settings/about.css
index ae994a8079..f7774c76fa 100644
--- a/apps/desktop/src/renderer/styles/settings/about.css
+++ b/apps/desktop/src/renderer/styles/settings/about.css
@@ -79,7 +79,7 @@
 .settingsAboutTagline {
   font: var(--maka-text-body);
   margin: 0;
-  color: var(--foreground-secondary);
+  color: var(--muted-foreground);
 }
 
 
diff --git a/apps/desktop/src/renderer/styles/settings/bot.css b/apps/desktop/src/renderer/styles/settings/bot.css
index 027c1823ba..ddf7b5765f 100644
--- a/apps/desktop/src/renderer/styles/settings/bot.css
+++ b/apps/desktop/src/renderer/styles/settings/bot.css
@@ -123,14 +123,14 @@
 .settingsBotDetailHeader p {
   font: var(--maka-text-body);
   margin: 0;
-  color: var(--foreground-secondary);
+  color: var(--muted-foreground);
 }
 
 .settingsBotEnableHint {
   font: var(--maka-text-supporting);
   display: block;
   margin-top: var(--space-1);
-  color: var(--warning-text, var(--foreground-secondary));
+  color: var(--warning-text, var(--muted-foreground));
 }
 
 .settingsBotConfigDocLink {
@@ -226,7 +226,7 @@
 
 .settingsBotOnboardingQrFrame[data-state="starting"],
 .settingsBotOnboardingQrFrame[data-state="connecting"] {
-  color: var(--foreground-secondary);
+  color: var(--muted-foreground);
   background: var(--foreground-3);
 }
 
diff --git a/apps/desktop/src/renderer/styles/settings/form.css b/apps/desktop/src/renderer/styles/settings/form.css
index a1d481a5c8..b84d3310e8 100644
--- a/apps/desktop/src/renderer/styles/settings/form.css
+++ b/apps/desktop/src/renderer/styles/settings/form.css
@@ -106,7 +106,7 @@
    explanatory wash). */
 .settingsQuietStatus {
   margin: 0;
-  color: var(--foreground-secondary);
+  color: var(--muted-foreground);
   font: var(--maka-text-body);
 }
 
@@ -122,6 +122,6 @@
 
 .settingsQuietCallout p {
   margin: 0;
-  color: var(--foreground-secondary);
+  color: var(--muted-foreground);
   font: var(--maka-text-body);
 }
diff --git a/apps/desktop/src/renderer/styles/settings/health.css b/apps/desktop/src/renderer/styles/settings/health.css
index f86ee7d7b4..e295557d1d 100644
--- a/apps/desktop/src/renderer/styles/settings/health.css
+++ b/apps/desktop/src/renderer/styles/settings/health.css
@@ -41,7 +41,7 @@
     gap: var(--space-1) var(--space-4);
     font: var(--maka-text-body);
     font-variant-numeric: tabular-nums;
-    color: var(--foreground-secondary);
+    color: var(--muted-foreground);
   }
 
 .settingsHealthSummaryLine [data-tone="success"] {
@@ -109,7 +109,7 @@
 
 .settingsHealthMetaLabel {
   font: var(--maka-text-supporting);
- color: var(--foreground-secondary); white-space: nowrap; }
+ color: var(--muted-foreground); white-space: nowrap; }
 .settingsHealthSignalLabel {
   /* A row label, not a heading — the role table's label weight carries it
      (detail audit: heading-4 here was the only heading-as-row-label in
diff --git a/apps/desktop/src/renderer/styles/settings/memory.css b/apps/desktop/src/renderer/styles/settings/memory.css
index ac6811d52e..f5767d8a83 100644
--- a/apps/desktop/src/renderer/styles/settings/memory.css
+++ b/apps/desktop/src/renderer/styles/settings/memory.css
@@ -31,8 +31,12 @@
     color: var(--muted-foreground);
     font: var(--maka-text-supporting);
   }
+/* Active is the state this line exists to announce, and the inactive line is
+   already muted — so active takes full ink rather than the retired middle
+   grey. Collapsing both onto muted would erase the distinction the attribute
+   is for. */
 .settingsMemoryInjectState[data-active="true"] {
-    color: var(--foreground-secondary);
+    color: var(--foreground);
   }
 .settingsMemoryPromptPreview small {
     color: var(--muted-foreground);
@@ -40,7 +44,7 @@
   }
 .settingsMemoryPromptPreviewBudget {
     font: var(--maka-text-code);
-    color: var(--foreground-secondary);
+    color: var(--muted-foreground);
   }
 .settingsMemoryPromptPreview pre {
     max-height: 220px;
@@ -50,14 +54,14 @@
     border: var(--border-width-hairline) solid var(--foreground-10);
     border-radius: var(--radius-control);
     background: var(--background);
-    color: var(--foreground-secondary);
+    color: var(--muted-foreground);
     font: var(--maka-text-code);
     white-space: pre-wrap;
     word-break: break-word;
   }
 .settingsMemoryPromptPreview p {
     margin: 0;
-    color: var(--foreground-secondary);
+    color: var(--muted-foreground);
     font: var(--maka-text-body);
   }
 .settingsMemorySaveSummary {
@@ -73,7 +77,7 @@
     font: var(--maka-text-body);
   }
 .settingsMemorySaveSummary small {
-    color: var(--foreground-secondary);
+    color: var(--muted-foreground);
     font: var(--maka-text-body);
   }
 .settingsMemorySaveSummaryTime {
@@ -123,7 +127,7 @@
     justify-content: space-between;
     flex-wrap: wrap;
     gap: var(--space-1-5);
-    color: var(--foreground-secondary);
+    color: var(--muted-foreground);
     font: var(--maka-text-body);
   }
 .settingsMemoryBackupCandidate > span {
@@ -152,7 +156,7 @@
     justify-content: space-between;
     gap: var(--space-3);
     font: var(--maka-text-body);
-    color: var(--foreground-secondary);
+    color: var(--muted-foreground);
   }
 
 .settingsMemoryEntryGroupHeader strong {
@@ -225,7 +229,7 @@
 
 .settingsMemoryEntryRow p {
     margin: 0;
-    color: var(--foreground-secondary);
+    color: var(--muted-foreground);
     font: var(--maka-text-body);
     white-space: pre-wrap;
     word-break: break-word;
diff --git a/apps/desktop/src/renderer/styles/settings/permission.css b/apps/desktop/src/renderer/styles/settings/permission.css
index 80cf464c1f..4756dca7cd 100644
--- a/apps/desktop/src/renderer/styles/settings/permission.css
+++ b/apps/desktop/src/renderer/styles/settings/permission.css
@@ -56,7 +56,7 @@
      .settingsFeatureStatusIcon, same 36px box. */
   border-radius: var(--radius-plate);
   background: oklch(from var(--foreground) l c h / 0.05);
-  color: var(--foreground-secondary);
+  color: var(--muted-foreground);
 }
 
 /* Denied is the only state that re-tints the plate: granted is the expected
diff --git a/apps/desktop/src/renderer/styles/settings/rows.css b/apps/desktop/src/renderer/styles/settings/rows.css
index 7fd6181704..0bea9aae27 100644
--- a/apps/desktop/src/renderer/styles/settings/rows.css
+++ b/apps/desktop/src/renderer/styles/settings/rows.css
@@ -86,7 +86,7 @@
 .settingsReadOnlyValue {
   min-width: 0;
   max-width: var(--settings-row-end-cap);
-  color: var(--foreground-secondary);
+  color: var(--muted-foreground);
   font: var(--maka-text-body);
   font-variant-numeric: tabular-nums;
   overflow-wrap: anywhere;
@@ -130,7 +130,7 @@
   display: inline-flex;
   align-items: center;
   gap: var(--space-1-5);
-  color: var(--foreground-secondary);
+  color: var(--muted-foreground);
   font: var(--maka-text-body);
   white-space: nowrap;
 }
diff --git a/apps/desktop/src/renderer/styles/settings/runtime-host.css b/apps/desktop/src/renderer/styles/settings/runtime-host.css
index d1d28b5e58..fd0634f991 100644
--- a/apps/desktop/src/renderer/styles/settings/runtime-host.css
+++ b/apps/desktop/src/renderer/styles/settings/runtime-host.css
@@ -39,7 +39,7 @@
 
 .settingsRuntimeHostTransport p {
   margin: 0 0 var(--space-1);
-  color: var(--foreground-secondary);
+  color: var(--muted-foreground);
   font: var(--maka-text-supporting);
 }
 
@@ -110,7 +110,7 @@
 }
 
 .settingsRuntimeHostManagementFacts dt {
-  color: var(--foreground-secondary);
+  color: var(--muted-foreground);
   font: var(--maka-text-supporting);
 }
 
@@ -198,7 +198,7 @@
 
 .settingsRuntimeHostDirectPeerAdvanced summary {
   width: fit-content;
-  color: var(--foreground-secondary);
+  color: var(--muted-foreground);
   cursor: pointer;
   font: var(--maka-text-supporting);
 }
@@ -214,7 +214,7 @@
 
 .settingsRuntimeHostManagementLogs {
   overflow-wrap: anywhere;
-  color: var(--foreground-secondary);
+  color: var(--muted-foreground);
   font: var(--maka-text-code);
 }
 
@@ -266,7 +266,7 @@
 
 .settingsRuntimeHostAccessIdentity span,
 .settingsRuntimeHostAccessMeta > span {
-  color: var(--foreground-secondary);
+  color: var(--muted-foreground);
   font: var(--maka-text-supporting);
 }
 
@@ -338,7 +338,7 @@
 
 .settingsPeerMeshConnectivityChevron {
   flex: 0 0 auto;
-  color: var(--foreground-secondary);
+  color: var(--muted-foreground);
   transition: transform 120ms ease;
 }
 
@@ -348,7 +348,7 @@
 
 .settingsPeerMeshConnectivityHelp {
   display: inline-flex;
-  color: var(--foreground-secondary);
+  color: var(--muted-foreground);
 }
 
 .settingsPeerMeshConnectivityBody {
@@ -593,13 +593,13 @@
 }
 
 .settingsPeerMeshCardSubtitle {
-  color: var(--foreground-secondary);
+  color: var(--muted-foreground);
   font: var(--maka-text-supporting);
 }
 
 .settingsPeerMeshChevron {
   flex: 0 0 auto;
-  color: var(--foreground-secondary);
+  color: var(--muted-foreground);
   transition: transform 120ms ease;
 }
 
@@ -670,7 +670,7 @@
 }
 
 .settingsPeerMeshTransitHelp svg {
-  color: var(--foreground-secondary);
+  color: var(--muted-foreground);
 }
 
 .settingsPeerMeshTransitMetrics {
@@ -801,7 +801,7 @@
 
 .settingsPeerMeshInvitationNote svg {
   margin-top: 2px;
-  color: var(--foreground-secondary);
+  color: var(--muted-foreground);
 }
 
 @media (max-width: 620px) {
diff --git a/apps/desktop/src/renderer/styles/settings/web-search.css b/apps/desktop/src/renderer/styles/settings/web-search.css
index b1a54a3e2e..b78f1450f6 100644
--- a/apps/desktop/src/renderer/styles/settings/web-search.css
+++ b/apps/desktop/src/renderer/styles/settings/web-search.css
@@ -100,7 +100,7 @@
 .settingsWebSearchResult p {
     font: var(--maka-text-body);
     margin: 0;
-    color: var(--foreground-secondary);
+    color: var(--muted-foreground);
     white-space: pre-wrap;
     word-break: break-word;
   }
diff --git a/apps/desktop/src/renderer/styles/shell-layout.css b/apps/desktop/src/renderer/styles/shell-layout.css
index ca810d0622..025089ccee 100644
--- a/apps/desktop/src/renderer/styles/shell-layout.css
+++ b/apps/desktop/src/renderer/styles/shell-layout.css
@@ -351,7 +351,7 @@ dialog:modal {
     transform var(--duration-base) var(--ease-out-strong);
   -webkit-app-region: no-drag;
   /* Match SideNav unselected icon ink (secondary), not ghost Button primary. */
-  color: var(--foreground-secondary);
+  color: var(--muted-foreground);
 }
 
 /* The session's identity: left-aligned at the content column's left edge.
@@ -553,7 +553,7 @@ dialog:modal {
   display: inline-flex;
   align-items: center;
   gap: var(--space-1-5);
-  color: var(--foreground-secondary);
+  color: var(--muted-foreground);
 }
 
 /* Titlebar chrome glyph ink is set where the glyph is: `ChromeIcon`
@@ -590,7 +590,7 @@ dialog:modal {
 
 .sessionCollaborationReadOnly {
   padding: var(--space-3);
-  color: var(--foreground-secondary);
+  color: var(--muted-foreground);
   text-align: center;
 }
 
diff --git a/apps/desktop/src/renderer/styles/sidebar.css b/apps/desktop/src/renderer/styles/sidebar.css
index e5a039a327..f261b23875 100644
--- a/apps/desktop/src/renderer/styles/sidebar.css
+++ b/apps/desktop/src/renderer/styles/sidebar.css
@@ -300,7 +300,7 @@
 }
 
 .maka-sidebar-hover-card-preview {
-  color: var(--foreground-secondary);
+  color: var(--muted-foreground);
   line-height: var(--leading-relaxed);
   display: -webkit-box;
   -webkit-line-clamp: 3;
@@ -320,7 +320,7 @@
   flex-wrap: wrap;
   align-items: center;
   gap: var(--space-0-5) var(--space-1);
-  color: var(--foreground-secondary);
+  color: var(--muted-foreground);
 }
 
 .maka-sidebar-hover-card-path {
diff --git a/apps/desktop/src/renderer/styles/workbar/artifacts.css b/apps/desktop/src/renderer/styles/workbar/artifacts.css
index b7b7fa9df1..d5cd5c8029 100644
--- a/apps/desktop/src/renderer/styles/workbar/artifacts.css
+++ b/apps/desktop/src/renderer/styles/workbar/artifacts.css
@@ -218,7 +218,7 @@
   min-width: max-content;
   margin: 0;
   padding: var(--space-1-5) var(--space-2);
-  color: var(--foreground-secondary);
+  color: var(--muted-foreground);
   white-space: pre;
 }
 
@@ -304,13 +304,13 @@
      dashed exception). */
   border: var(--border-width-hairline) solid var(--border);
   background: var(--color-accent-muted);
-  color: var(--foreground-secondary);
+  color: var(--muted-foreground);
 }
 
 .maka-artifact-preview-unsupported-body {
   font: var(--maka-text-body);
   margin: 0;
-  color: var(--foreground-secondary);
+  color: var(--muted-foreground);
 }
 
 .maka-artifact-preview-unsupported-meta {
@@ -320,14 +320,14 @@
   gap: var(--space-1) var(--space-4);
   margin: 0;
   padding: 0;
-  color: var(--foreground-secondary);
+  color: var(--muted-foreground);
 }
 .maka-artifact-preview-unsupported-meta > div {
   display: contents;
 }
 .maka-artifact-preview-unsupported-meta dt {
   font: var(--maka-text-label);
-  color: var(--foreground-secondary);
+  color: var(--muted-foreground);
 }
 .maka-artifact-preview-unsupported-meta dd {
   margin: 0;
diff --git a/apps/desktop/src/renderer/styles/workbar/inspector.css b/apps/desktop/src/renderer/styles/workbar/inspector.css
index 47d7880f5f..1114105134 100644
--- a/apps/desktop/src/renderer/styles/workbar/inspector.css
+++ b/apps/desktop/src/renderer/styles/workbar/inspector.css
@@ -107,7 +107,7 @@
   width: 7px;
   height: 7px;
   border-radius: var(--radius-pill);
-  background: var(--foreground-secondary);
+  background: var(--muted-foreground);
 }
 
 .maka-inspector-turn[data-failed='true'] .maka-inspector-turn-head::before {
@@ -139,7 +139,7 @@
 }
 
 .maka-inspector-turn-cost {
-  color: var(--foreground-secondary);
+  color: var(--muted-foreground);
 }
 
 /* The default 480px workbar does not leave enough room for the timestamp,
@@ -203,7 +203,7 @@
    half of the row is the fact. A 12px tier used to carry six different roles
    here; six roles at one size is not a rank, it is small print. */
 .maka-inspector-step-label {
-  color: var(--foreground-secondary);
+  color: var(--muted-foreground);
 }
 
 .maka-inspector-step-detail {
@@ -295,7 +295,7 @@
 /* One tier under a section title, for the per-tool list inside the estimate —
    it belongs to the section above it rather than opening a new one. */
 .maka-inspector-section-subtitle {
-  color: var(--foreground-secondary);
+  color: var(--muted-foreground);
   margin-top: var(--space-1);
 }
 
@@ -444,7 +444,7 @@
   display: flex;
   align-items: center;
   gap: var(--space-2);
-  color: var(--foreground-secondary);
+  color: var(--muted-foreground);
   white-space: nowrap;
 }
 
diff --git a/apps/desktop/src/renderer/styles/workbar/shell.css b/apps/desktop/src/renderer/styles/workbar/shell.css
index 411e3b9edf..20eb7b78c2 100644
--- a/apps/desktop/src/renderer/styles/workbar/shell.css
+++ b/apps/desktop/src/renderer/styles/workbar/shell.css
@@ -192,7 +192,7 @@
   min-width: 0;
   height: var(--size-element-sm);
   border-radius: var(--radius-element);
-  color: var(--foreground-secondary);
+  color: var(--muted-foreground);
   touch-action: none;
   user-select: none;
   transition:
diff --git a/packages/ui/src/styles.css b/packages/ui/src/styles.css
index 38e382d08b..1c133af7eb 100644
--- a/packages/ui/src/styles.css
+++ b/packages/ui/src/styles.css
@@ -123,7 +123,7 @@
 .maka-stat-tile-value-destructive { color: var(--destructive-text); }
 .maka-stat-tile-label {
   font: var(--maka-text-supporting);
- color: var(--foreground-secondary); letter-spacing: 0.025em; }
+ color: var(--muted-foreground); letter-spacing: 0.025em; }
 .maka-stat-tile-detail {
   font: var(--maka-text-supporting);
  color: var(--muted-foreground); }
@@ -149,7 +149,7 @@
   height: auto;
   border: 0;
   padding: 0;
-  color: var(--foreground-secondary);
+  color: var(--muted-foreground);
   background: transparent;
   text-align: left;
   box-shadow: none;
@@ -260,7 +260,7 @@
 }
 .maka-deep-thinking [data-maka-contract="markdown"] .astryx-markdown-heading,
 .maka-deep-thinking [data-maka-contract="markdown"] strong {
-  color: color-mix(in oklch, var(--link) 28%, var(--foreground-secondary));
+  color: color-mix(in oklch, var(--link) 28%, var(--muted-foreground));
 }
 .maka-deep-thinking [data-maka-contract="markdown"] .astryx-markdown-heading {
   font: var(--maka-text-label);
@@ -461,7 +461,7 @@
    point (13px h4-h6 dropped to muted); it is why a turn can carry four levels
    of structure inside two type sizes. */
 [data-maka-contract="markdown"] .astryx-markdown[data-density="compact"] .astryx-markdown-heading:not([data-level="1"], [data-level="2"], [data-level="3"]) {
-  color: var(--foreground-secondary);
+  color: var(--muted-foreground);
 }
 
 /* Astryx hands custom images no inline/block placement or spacing wrapper.
@@ -657,7 +657,7 @@
 .maka-pty-shell-header { display: flex; min-width: 0; align-items: center; padding: 10px 12px 4px; }
 .maka-pty-shell-title {
   font: var(--maka-text-label);
- color: var(--foreground-secondary); }
+ color: var(--muted-foreground); }
 .maka-pty-shell-body { display: grid; min-width: 0; gap: 8px; padding: 6px 12px; }
 .maka-pty-shell-footer {
   font: var(--maka-text-body);
@@ -680,7 +680,7 @@
  color: var(--link); }
 .maka-web-result-list p {
   font: var(--maka-text-body);
- margin: 0; color: var(--foreground-secondary); }
+ margin: 0; color: var(--muted-foreground); }
 .maka-web-result-error { gap: 6px; }
 .maka-web-result-error-message,
 .maka-web-result-repair {
@@ -879,7 +879,7 @@
   background: var(--foreground-2);
 }
 .maka-tool-terminal-head { align-items: center; font-variant-ligatures: none; }
-.maka-tool-diff-paths code { color: var(--foreground-secondary); background: transparent; }
+.maka-tool-diff-paths code { color: var(--muted-foreground); background: transparent; }
 .maka-tool-diff-body {
   font: var(--maka-text-supporting);
   max-height: 320px;
@@ -935,20 +935,22 @@
    flat single-tint reading the tokens exist to break up; the row's background
    tint and its marker carry add/del on their own.
 
-   This is also what brings context lines up from `--foreground-secondary` to
-   full foreground, which is deliberate rather than collateral: once a context
-   line's keywords and strings are syntax-coloured, dimming only the text
-   between them reads as damage, not as de-emphasis. The row's flat background
-   is what says "unchanged" now. */
+   Context lines read at full foreground, which is deliberate rather than
+   collateral: once a context line's keywords and strings are syntax-coloured,
+   dimming only the text between them reads as damage, not as de-emphasis. The
+   row's flat background is what says "unchanged" now. */
 .maka-tool-diff-code { color: var(--foreground); }
 .maka-tool-diff-line[data-line="meta"] .maka-tool-diff-code { color: inherit; }
 .maka-tool-diff-line[data-line="add"] { background: var(--color-success-muted); color: var(--success-text); }
 .maka-tool-diff-line[data-line="del"] { background: var(--color-error-muted); color: var(--destructive); }
 .maka-tool-diff-line[data-line="hunk"] {
   font: var(--maka-text-heading-5);
- background: oklch(from var(--accent) l c h / 0.08); color: var(--foreground-secondary); }
+ background: oklch(from var(--accent) l c h / 0.08); color: var(--muted-foreground); }
 .maka-tool-diff-line[data-line="meta"] { color: var(--muted-foreground); }
-.maka-tool-diff-line[data-line="ctx"] { color: var(--foreground-secondary); }
+/* Full ink, matching `.maka-tool-diff-code` above: a context line is the code
+   the change is read against, and the marker column is the only part this rule
+   still paints on its own. Meta and hunk rows are the ones that recede. */
+.maka-tool-diff-line[data-line="ctx"] { color: var(--foreground); }
 .maka-tool-terminal-cwd { color: var(--muted-foreground); background: transparent; }
 .maka-tool-terminal-cmd {
   font: var(--maka-text-heading-5);
@@ -960,7 +962,7 @@
    instead. Deleting the family is its own change. */
 .maka-tool-terminal-exit {
   font: var(--maka-text-heading-5);
- height: var(--h-control-sm); display: inline-flex; align-items: center; white-space: nowrap; padding: 1px var(--space-1-5); border-radius: var(--radius-pill); background: var(--foreground-5); color: var(--foreground-secondary); letter-spacing: var(--tracking-wide); }
+ height: var(--h-control-sm); display: inline-flex; align-items: center; white-space: nowrap; padding: 1px var(--space-1-5); border-radius: var(--radius-pill); background: var(--foreground-5); color: var(--muted-foreground); letter-spacing: var(--tracking-wide); }
 .maka-tool-terminal-exit[data-ok="true"] { background: var(--color-success-muted); color: var(--success); }
 .maka-tool-terminal-exit[data-ok="false"] { background: var(--color-error-muted); color: var(--destructive); }
 .maka-tool-terminal-empty {
@@ -973,7 +975,7 @@
 .maka-tool-terminal-stream[data-stream="stderr"] { border-top: 1px solid var(--border); background: var(--color-error-muted); color: var(--destructive); }
 .maka-tool-terminal-truncated-note {
   font: var(--maka-text-supporting);
- display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); padding: var(--space-1-5) var(--space-2); border-top: 1px solid var(--border); background: var(--color-warning-muted); color: var(--foreground-secondary); }
+ display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); padding: var(--space-1-5) var(--space-2); border-top: 1px solid var(--border); background: var(--color-warning-muted); color: var(--muted-foreground); }
 .maka-tool-terminal-truncated-note > span { min-width: 0; }
 .maka-tool-terminal-copy { flex: 0 0 auto; }
 .maka-tool-terminal-copy[data-pending="true"] { cursor: progress; }
@@ -998,14 +1000,14 @@
 .maka-web-search-preview a:focus-visible { text-decoration: underline; }
 .maka-web-search-preview li p {
   font: var(--maka-text-supporting);
- margin: 0; color: var(--foreground-secondary); white-space: pre-wrap; word-break: break-word; }
+ margin: 0; color: var(--muted-foreground); white-space: pre-wrap; word-break: break-word; }
 .maka-web-search-error { border-color: color-mix(in oklab, var(--destructive-text) 32%, var(--foreground-10)); background: color-mix(in oklab, var(--destructive-text) 8%, var(--foreground-3)); }
 .maka-web-search-error-message,
 .maka-web-search-error-repair {
   font: var(--maka-text-supporting);
  margin: 0; white-space: pre-wrap; word-break: break-word; }
 .maka-web-search-error-message { color: var(--destructive-text); }
-.maka-web-search-error-repair { color: var(--foreground-secondary); }
+.maka-web-search-error-repair { color: var(--muted-foreground); }
 
 .maka-load-tool-preview {
   display: grid;
@@ -1022,7 +1024,7 @@
   height: var(--space-8);
   place-items: center;
   border-radius: var(--radius-element);
-  color: var(--foreground-secondary);
+  color: var(--muted-foreground);
   background: var(--foreground-5);
 }
 .maka-load-tool-summary {
@@ -1035,7 +1037,7 @@
 .maka-load-tool-description {
   font: var(--maka-text-supporting);
   margin-top: var(--space-0-5) !important;
-  color: var(--foreground-secondary);
+  color: var(--muted-foreground);
   overflow-wrap: anywhere;
 }
 .maka-load-tool-count {
@@ -1088,7 +1090,7 @@
 .maka-load-tool-technical dd {
   min-width: 0;
   margin: 0;
-  color: var(--foreground-secondary);
+  color: var(--muted-foreground);
 }
 .maka-load-tool-technical code {
   font: var(--maka-text-code);
diff --git a/packages/ui/stories/composite-row-states.stories.tsx b/packages/ui/stories/composite-row-states.stories.tsx
index 13128149ee..7f49316634 100644
--- a/packages/ui/stories/composite-row-states.stories.tsx
+++ b/packages/ui/stories/composite-row-states.stories.tsx
@@ -75,7 +75,7 @@ function StoryFrame(props: { children: React.ReactNode; description: string; tit
     <section style={{ display: 'grid', gap: 16, maxWidth: 900 }}>
       <div>
         <h2 style={{ fontSize: 16, margin: 0 }}>{props.title}</h2>
-        <p style={{ color: 'var(--foreground-secondary)', fontSize: 12, margin: '4px 0 0' }}>
+        <p style={{ color: 'var(--muted-foreground)', fontSize: 12, margin: '4px 0 0' }}>
           {props.description}
         </p>
       </div>
diff --git a/packages/ui/stories/functional-motion.stories.tsx b/packages/ui/stories/functional-motion.stories.tsx
index d41a6d2380..4b3b15ddac 100644
--- a/packages/ui/stories/functional-motion.stories.tsx
+++ b/packages/ui/stories/functional-motion.stories.tsx
@@ -42,7 +42,7 @@ export const RetainedFunctionalMotion: Story = {
       <style>{'@keyframes maka-shimmer { 0% { background-position: -200% 0; } 100% { background-position: 200% 0; } }'}</style>
       <div style={{ alignItems: 'center', display: 'grid', gap: 8, justifyItems: 'center' }}>
         <Spinner style={{ height: 20, width: 20 }} />
-        <span style={{ color: 'var(--foreground-secondary)', fontSize: 12, fontWeight: 600 }}>Spinner</span>
+        <span style={{ color: 'var(--muted-foreground)', fontSize: 12, fontWeight: 600 }}>Spinner</span>
       </div>
       <div style={{ alignItems: 'center', display: 'grid', gap: 8, justifyItems: 'center' }}>
         <span
@@ -57,7 +57,7 @@ export const RetainedFunctionalMotion: Story = {
             width: 96,
           }}
         />
-        <span style={{ color: 'var(--foreground-secondary)', fontSize: 12, fontWeight: 600 }}>Shimmer</span>
+        <span style={{ color: 'var(--muted-foreground)', fontSize: 12, fontWeight: 600 }}>Shimmer</span>
       </div>
     </div>
   ),
diff --git a/packages/ui/stories/icons.stories.tsx b/packages/ui/stories/icons.stories.tsx
index 19ee044323..f41077d488 100644
--- a/packages/ui/stories/icons.stories.tsx
+++ b/packages/ui/stories/icons.stories.tsx
@@ -62,7 +62,7 @@ export const LucideIcons: Story = {
     <section style={{ display: 'grid', gap: 20, maxWidth: 920 }}>
       <div style={{ display: 'grid', gap: 4 }}>
         <h2 style={{ fontSize: 16, fontWeight: 600, margin: 0 }}>Lucide Icons</h2>
-        <p style={{ color: 'var(--foreground-secondary)', fontSize: 12, margin: 0, lineHeight: 1.5 }}>
+        <p style={{ color: 'var(--muted-foreground)', fontSize: 12, margin: 0, lineHeight: 1.5 }}>
           {LUCIDE_ICONS.length} 个通用 UI 图标,通过 icons.tsx 的 lucide-react re-export 自动追踪。业务代码仍只从 @maka/ui/icons 取图标。
         </p>
       </div>
@@ -87,7 +87,7 @@ export const LucideIcons: Story = {
             }}
           >
             <Comp size={20} />
-            <code style={{ color: 'var(--foreground-secondary)', fontSize: 10, wordBreak: 'break-word' }}>{name}</code>
+            <code style={{ color: 'var(--muted-foreground)', fontSize: 10, wordBreak: 'break-word' }}>{name}</code>
           </div>
         ))}
       </div>
@@ -100,7 +100,7 @@ export const BotBrandIcons: Story = {
     <section style={{ display: 'grid', gap: 20, maxWidth: 760 }}>
       <div style={{ display: 'grid', gap: 4 }}>
         <h2 style={{ fontSize: 16, fontWeight: 600, margin: 0 }}>Bot Brand Icons</h2>
-        <p style={{ color: 'var(--foreground-secondary)', fontSize: 12, margin: 0, lineHeight: 1.5 }}>
+        <p style={{ color: 'var(--muted-foreground)', fontSize: 12, margin: 0, lineHeight: 1.5 }}>
           {BOT_BRAND_PROVIDERS.length} 个 IM 渠道品牌图标,本地 React SVG,零运行时 CDN 依赖。
         </p>
       </div>
@@ -119,7 +119,7 @@ export const BotBrandIcons: Story = {
             }}
           >
             <BotBrandLogo provider={provider} width={32} height={32} />
-            <code style={{ color: 'var(--foreground-secondary)', fontSize: 10 }}>{provider}</code>
+            <code style={{ color: 'var(--muted-foreground)', fontSize: 10 }}>{provider}</code>
           </div>
         ))}
       </div>
diff --git a/packages/ui/stories/palette-matrix.stories.tsx b/packages/ui/stories/palette-matrix.stories.tsx
index 8a1ba1fa0d..ed2de0e777 100644
--- a/packages/ui/stories/palette-matrix.stories.tsx
+++ b/packages/ui/stories/palette-matrix.stories.tsx
@@ -83,7 +83,7 @@ export const AllPalettes: Story = {
       <section style={{ display: 'grid', gap: 20, maxWidth: 920 }}>
         <div style={{ display: 'grid', gap: 4 }}>
           <h2 style={{ fontSize: 16, fontWeight: 600, margin: 0 }}>Palette Matrix</h2>
-          <p style={{ color: 'var(--foreground-secondary)', fontSize: 12, margin: 0, lineHeight: 1.5 }}>
+          <p style={{ color: 'var(--muted-foreground)', fontSize: 12, margin: 0, lineHeight: 1.5 }}>
             {THEME_PALETTES.length} 个 palette,用工具栏切 light/dark 查看另一组。每个块独立应用 data-maka-theme。
           </p>
         </div>


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org