fix(theme): replace no-op --mat-sys-* tokens with --app-* design tokens

The Material theme setup in apps/web/src/m3-theme.scss uses the older M3
API (mat.define-theme + mat.all-component-themes), which emits component
tokens but never emits the --mat-sys-* system tokens. As a result ~540
var(--mat-sys-*) references across the app were silent no-ops: the
declarations were invalid at computed-value time and fell back to
inherited/initial values, so many backgrounds, borders and dividers never
rendered and several text/icon colors sat at the wrong contrast.

Rather than migrate to mat.theme() (which would flip every no-op at once
with M3 azure-palette values foreign to the hand-tuned design), remap the
high-visibility usages to the existing --app-* design tokens defined for
both themes in m3-theme.scss, verified screenshot-driven in light and dark
via Electron CDP.

Changes:
- Add --app-error-color and --app-accent-color tokens (light + dark).
- Fix high-visibility surfaces: settings area, workspace shell header,
  playlist switcher, downloads, channel-details dialog, account-info,
  group/category management dialogs, sources context/filters panels,
  nav-list, and the .dark-theme app background.
- Point the Tailwind color palette at --app-* tokens instead of --mat-sys-*.
- Strip ~150 dead var(--app-x, var(--mat-sys-y)) fallbacks repo-wide (the
  fallback can never be taken).
- UI polish surfaced by the remap: suppress Material's circular icon-button
  state layer in the header (keep the rounded-rectangle hover), remove the
  orphan divider before the download button, remove the redundant right-edge
  accent bar on active rail items (the filled chip is enough), and raise the
  header/switcher control icons from the muted tier to --app-body-color so
  they no longer read as disabled.

Guard against regressions:
- tools/styles/check-mat-sys-usage.mjs fails on any new var(--mat-sys-*)
  reference; legacy usages are grandfathered in a shrink-only baseline
  (tools/styles/mat-sys-baseline.mjs). Wired into the CI Lint job and
  exposed as `pnpm run lint:styles`.

Docs:
- docs/architecture/theme-design-tokens.md documents why the tokens are
  no-ops, the --app-* mapping table, and the screenshot-driven migration
  protocol. CLAUDE.md updated with the token policy and the new lint check.

Scrollbar-related --mat-sys-* usages are intentionally left untouched here;
they belong to the separate branch claude/iptvnator-dark-scrollbar-2600aa.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Fable 5 committed 2026-07-12 22:31:46 +02:00
1 parent f856226dc3
commit eac1dfa09e
45 files changed
+801 -488

No files matched your search

@@ -16,13 +16,13 @@
border-radius: 12px;
background: color-mix(
in srgb,
var(--app-selection-surface, var(--mat-sys-surface-container)) 72%,
var(--app-selection-surface) 72%,
transparent
);
border: 1px solid
color-mix(
in srgb,
var(--app-selection-border, var(--mat-sys-outline-variant)) 70%,
var(--app-selection-border) 70%,
transparent
);
color: var(--mat-sys-on-surface-variant);
@@ -52,8 +52,8 @@
border-radius: 999px;
background: color-mix(
in srgb,
var(--app-widget-header-bg, var(--mat-sys-surface-container-high)) 52%,
var(--app-body-color, var(--mat-sys-on-surface-variant)) 48%
var(--app-widget-header-bg) 52%,
var(--app-body-color) 48%
);
&::after {
@@ -65,7 +65,7 @@
transparent 8%,
color-mix(
in srgb,
var(--app-heading-color, var(--mat-sys-on-surface)) 18%,
var(--app-heading-color) 18%,
transparent
)
44%,
@@ -34,7 +34,7 @@
gap: 14px;
text-align: center;
box-sizing: border-box;
color: var(--app-body-color, var(--mat-sys-on-surface-variant));
color: var(--app-body-color);
img {
width: min(100%, var(--error-view-illustration-size));
@@ -68,7 +68,7 @@
font-size: var(--error-view-title-size);
font-weight: var(--error-view-title-weight);
line-height: 1.12;
color: var(--app-heading-color, var(--mat-sys-on-surface));
color: var(--app-heading-color);
letter-spacing: var(--error-view-title-spacing);
text-wrap: balance;
}
@@ -31,7 +31,7 @@
font-size: 0.94rem;
font-weight: 600;
letter-spacing: -0.01em;
color: var(--mat-sys-on-surface);
color: var(--app-heading-color);
}
}
@@ -54,14 +54,14 @@
}
&--active mat-icon {
color: var(--mat-sys-primary);
color: var(--app-selection-color);
}
mat-icon {
font-size: 18px;
width: 18px;
height: 18px;
color: var(--mat-sys-on-surface-variant);
color: var(--app-muted-color);
}
}
@@ -72,8 +72,8 @@
margin-top: 5px;
padding: 2px 0;
border-radius: 999px;
background: var(--mat-sys-surface-container);
color: var(--mat-sys-on-surface-variant);
background: var(--app-hover-overlay);
color: var(--app-muted-color);
font-size: 0.72rem;
font-weight: 500;
letter-spacing: 0.04em;
@@ -87,7 +87,7 @@
.context-header p {
margin: 4px 0 0;
font-size: 0.78rem;
color: var(--mat-sys-on-surface-variant);
color: var(--app-muted-color);
}
// ─── Search Bar ──────────────────────────────────────────────────────────────
@@ -99,8 +99,8 @@
padding: 0 10px;
min-height: 36px;
border-radius: 10px;
border: 1px solid var(--app-search-border, var(--mat-sys-outline-variant));
background: var(--app-search-bg, var(--mat-sys-surface-container));
border: 1px solid var(--app-search-border);
background: var(--app-search-bg);
flex-shrink: 0;
cursor: text;
transition:
@@ -108,9 +108,9 @@
box-shadow 0.15s ease;
&:focus-within {
border-color: var(--mat-sys-primary);
border-color: var(--app-selection-color);
box-shadow: 0 0 0 3px
color-mix(in srgb, var(--mat-sys-primary) 12%, transparent);
color-mix(in srgb, var(--app-selection-color) 12%, transparent);
}
}
@@ -118,7 +118,7 @@
font-size: 18px;
width: 18px;
height: 18px;
color: var(--mat-sys-on-surface-variant);
color: var(--app-muted-color);
flex-shrink: 0;
}
@@ -127,14 +127,14 @@
border: none;
outline: none;
background: transparent;
color: var(--mat-sys-on-surface);
color: var(--app-heading-color);
font: inherit;
font-size: 0.84rem;
width: 100%;
min-width: 0;
&::placeholder {
color: var(--mat-sys-on-surface-variant);
color: var(--app-muted-color);
opacity: 0.6;
}
@@ -161,7 +161,7 @@
// ─── Divider ──────────────────────────────────────────────────────────────────
.context-divider {
height: 1px;
background: var(--mat-sys-outline-variant);
background: var(--app-separator);
margin: 0 0 6px;
opacity: 0.6;
flex-shrink: 0;
@@ -197,15 +197,11 @@ app-workspace-context-category-view {
border-radius: 12px;
background: color-mix(
in srgb,
var(--app-widget-header-bg, var(--mat-sys-surface-container-high)) 88%,
var(--app-widget-bg, var(--mat-sys-surface-container)) 12%
var(--app-widget-header-bg) 88%,
var(--app-widget-bg) 12%
);
border: 1px solid
color-mix(
in srgb,
var(--app-widget-header-border, var(--mat-sys-outline-variant)) 96%,
transparent
);
color-mix(in srgb, var(--app-widget-header-border) 96%, transparent);
}
.context-skeleton__dot,
@@ -215,8 +211,8 @@ app-workspace-context-category-view {
overflow: hidden;
background: color-mix(
in srgb,
var(--app-widget-header-bg, var(--mat-sys-surface-container-high)) 52%,
var(--app-body-color, var(--mat-sys-on-surface-variant)) 48%
var(--app-widget-header-bg) 52%,
var(--app-body-color) 48%
);
&::after {
@@ -226,12 +222,7 @@ app-workspace-context-category-view {
background: linear-gradient(
100deg,
transparent 8%,
color-mix(
in srgb,
var(--app-heading-color, var(--mat-sys-on-surface)) 18%,
transparent
)
44%,
color-mix(in srgb, var(--app-heading-color) 18%, transparent) 44%,
transparent 74%
);
transform: translateX(-120%);
@@ -6,7 +6,7 @@
.context-panel {
border-right: 1px solid var(--mat-sys-outline-variant);
background: var(--app-rail-bg, var(--mat-sys-surface-container-low));
background: var(--app-rail-bg);
display: flex;
flex-direction: column;
gap: 6px;
@@ -9,8 +9,8 @@
gap: 12px;
height: 56px;
padding: 0 24px;
border-bottom: 1px solid var(--mat-sys-outline-variant);
background: var(--app-header-bg, var(--mat-sys-surface-container-low));
border-bottom: 1px solid var(--app-widget-header-border);
background: var(--app-header-bg);
app-region: drag;
}
@@ -26,8 +26,8 @@
max-width: 560px;
height: 36px;
border-radius: 10px;
border: 1px solid var(--app-search-border, var(--mat-sys-outline-variant));
background: var(--app-search-bg, var(--mat-sys-surface-container));
border: 1px solid var(--app-search-border);
background: var(--app-search-bg);
display: flex;
align-items: center;
gap: 8px;
@@ -39,16 +39,16 @@
app-region: no-drag;
&:focus-within {
border-color: var(--mat-sys-primary);
border-color: var(--app-selection-color);
box-shadow: 0 0 0 3px
color-mix(in srgb, var(--mat-sys-primary) 12%, transparent);
color-mix(in srgb, var(--app-selection-color) 12%, transparent);
}
mat-icon {
font-size: 18px;
width: 18px;
height: 18px;
color: var(--mat-sys-on-surface-variant);
color: var(--app-muted-color);
flex-shrink: 0;
}
@@ -60,14 +60,10 @@
display: inline-flex;
align-items: center;
justify-content: center;
color: var(--mat-sys-on-surface-variant);
background: color-mix(
in srgb,
var(--mat-sys-surface-container-high) 76%,
transparent
);
color: var(--app-muted-color);
background: var(--app-hover-overlay);
border: 1px solid
color-mix(in srgb, var(--mat-sys-outline-variant) 82%, transparent);
color-mix(in srgb, var(--app-separator) 82%, transparent);
font-size: 0.75rem;
font-weight: 600;
line-height: 1;
@@ -79,19 +75,19 @@
}
.search-chip--scope {
color: var(--mat-sys-on-surface);
color: var(--app-heading-color);
}
.search-chip--status {
color: var(--app-selection-color, var(--mat-sys-primary));
color: var(--app-selection-color);
background: color-mix(
in srgb,
var(--app-selection-surface, var(--mat-sys-primary-container)) 70%,
var(--app-selection-surface) 70%,
transparent
);
border-color: color-mix(
in srgb,
var(--app-selection-border, var(--mat-sys-primary)) 52%,
var(--app-selection-border) 52%,
transparent
);
}
@@ -101,7 +97,7 @@
border: none;
outline: none;
background: transparent;
color: var(--mat-sys-on-surface);
color: var(--app-heading-color);
font: inherit;
font-size: 0.875rem;
@@ -111,7 +107,7 @@
}
&::placeholder {
color: var(--mat-sys-on-surface-variant);
color: var(--app-muted-color);
opacity: 0.7;
}
}
@@ -124,14 +120,10 @@
min-width: 36px;
padding: 0 6px;
margin-right: -2px;
color: var(--mat-sys-on-surface-variant);
background: color-mix(
in srgb,
var(--mat-sys-surface-container-high) 76%,
transparent
);
color: var(--app-muted-color);
background: var(--app-hover-overlay);
border: 1px solid
color-mix(in srgb, var(--mat-sys-outline-variant) 70%, transparent);
color-mix(in srgb, var(--app-separator) 70%, transparent);
border-radius: 5px;
font-family: inherit;
font-size: 0.7rem;
@@ -146,17 +138,17 @@
border-color 0.15s ease;
&:hover {
background: var(--mat-sys-surface-container-highest);
color: var(--mat-sys-on-surface);
background: var(--app-card-hover-bg);
color: var(--app-heading-color);
border-color: color-mix(
in srgb,
var(--mat-sys-outline-variant) 100%,
var(--app-separator) 100%,
transparent
);
}
&:focus-visible {
outline: 2px solid var(--mat-sys-primary);
outline: 2px solid var(--app-selection-color);
outline-offset: 1px;
}
}
@@ -170,15 +162,26 @@
button,
a {
color: var(--mat-sys-on-surface-variant);
// Body-tier (not muted) so these read as active controls, not
// disabled. Hover brightens to --app-heading-color below.
color: var(--app-body-color);
border-radius: 8px;
transition:
color 0.15s ease,
background 0.15s ease;
// Suppress Material's circular state-layer overlay so only the
// rounded-rectangle hover/active states below render. Keeps the
// header consistent with the rail, nav items, and the .is-active
// state (all rounded rectangles). The click ripple is left intact —
// it is clipped to the button's 8px radius.
::ng-deep .mat-mdc-button-persistent-ripple {
display: none;
}
&:hover {
color: var(--mat-sys-on-surface);
background: var(--mat-sys-surface-container);
color: var(--app-heading-color);
background: var(--app-hover-overlay);
}
&.is-active {
@@ -202,7 +205,6 @@
.header-actions--trailing {
margin-left: auto;
padding-left: 8px;
border-left: 1px solid var(--mat-sys-outline-variant);
app-region: no-drag;
}
@@ -45,7 +45,7 @@
font-size: 1.55rem;
font-weight: 650;
letter-spacing: -0.03em;
color: var(--app-heading-color, var(--mat-sys-on-surface));
color: var(--app-heading-color);
}
}
@@ -85,13 +85,13 @@
.workspace-loading-overlay__phase {
font-size: 1rem;
font-weight: 560;
color: var(--app-body-color, var(--mat-sys-on-surface));
color: var(--app-body-color);
}
.workspace-loading-overlay__detail {
max-width: 32ch;
justify-self: center;
color: var(--app-muted-color, var(--mat-sys-on-surface-variant));
color: var(--app-muted-color);
line-height: 1.45;
}
@@ -100,7 +100,7 @@
justify-self: center;
line-height: 1.45;
font-variant-numeric: tabular-nums;
color: var(--app-muted-color, var(--mat-sys-on-surface-variant));
color: var(--app-muted-color);
text-wrap: balance;
}
@@ -83,17 +83,3 @@
color: var(--rail-link-active-icon, var(--app-selection-color));
}
}
.portal-rail-link.is-active::after,
.portal-rail-link.active::after {
content: '';
position: absolute;
right: -4px;
top: 50%;
transform: translateY(-50%);
width: 4px;
height: 20px;
border-radius: 999px;
background: var(--rail-link-active-icon, var(--app-selection-color));
box-shadow: 0 0 10px var(--rail-link-active-glow, var(--app-selection-glow));
}
@@ -13,8 +13,8 @@
box-sizing: border-box;
padding: 10px 8px;
border-right: 1px solid
var(--app-rail-border, var(--mat-sys-outline-variant));
background: var(--app-rail-bg, var(--mat-sys-surface-container-low));
var(--app-rail-border);
background: var(--app-rail-bg);
app-region: drag;
overflow-y: auto;
overflow-x: hidden;
@@ -83,19 +83,6 @@
mat-icon {
color: var(--rail-link-active-icon);
}
&::after {
content: '';
position: absolute;
right: -4px;
top: 50%;
transform: translateY(-50%);
width: 3px;
height: 18px;
border-radius: 999px;
background: var(--rail-link-active-icon);
box-shadow: 0 0 10px var(--rail-link-active-glow);
}
}
.brand {
@@ -208,19 +195,6 @@
background: var(--app-hover-overlay);
color: var(--app-on-surface);
}
&.is-active::after {
content: '';
position: absolute;
right: -4px;
top: 50%;
transform: translateY(-50%);
width: 4px;
height: 20px;
border-radius: 999px;
background: var(--app-selection-color);
box-shadow: 0 0 10px var(--app-selection-glow);
}
}
}
@@ -13,7 +13,7 @@
height: 100%;
min-height: 0;
overflow: hidden;
background: var(--app-shell-bg, var(--mat-sys-surface));
background: var(--app-shell-bg);
color: var(--mat-sys-on-surface);
}
@@ -37,7 +37,7 @@ app-workspace-shell-header {
min-height: 0;
min-width: 0;
overflow: hidden;
background: var(--app-rail-bg, var(--mat-sys-surface-container-low));
background: var(--app-rail-bg);
&.single-pane {
grid-template-columns: 1fr;
@@ -53,14 +53,14 @@ app-workspace-shell-context-sidebar {
grid-column: 2;
grid-row: 3;
padding: 0;
background: var(--app-content-bg, var(--mat-sys-surface));
background: var(--app-content-bg);
}
.workspace-content {
min-width: 0;
min-height: 0;
overflow: auto;
background: var(--app-content-bg, var(--mat-sys-surface));
background: var(--app-content-bg);
border-radius: 16px 0 0 0;
box-shadow: -4px 8px 20px var(--app-content-shadow, rgba(0, 0, 0, 0.18));
}
@@ -32,20 +32,20 @@
font-size: 0.94rem;
font-weight: 600;
letter-spacing: -0.01em;
color: var(--mat-sys-on-surface);
color: var(--app-heading-color);
}
}
.context-header p {
margin: 4px 0 0;
font-size: 0.78rem;
color: var(--mat-sys-on-surface-variant);
color: var(--app-muted-color);
}
// ─── Divider ──────────────────────────────────────────────────────────────────
.context-divider {
height: 1px;
background: var(--mat-sys-outline-variant);
background: var(--app-separator);
margin: 0 0 6px;
opacity: 0.6;
flex-shrink: 0;
@@ -97,6 +97,6 @@
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--mat-sys-on-surface-variant);
color: var(--app-muted-color);
opacity: 0.85;
}