Context
Reported by @tobiu 2026-08-31 against dev@fa20136f29 (post #17963/PR #17977): "looking into apps/workstation it looks exactly like 8h ago => no inline tab styling for the app."
The report is correct, and the cause is not a missing stylesheet. Every sheet loads, every token the design pass shipped resolves, and the header actions project and gate exactly as designed. What fails is one layer lower: Workstation's palette bridge is out-specified by the theme on the element the two share, so the app renders the neo theme's neutral ramp instead of its own identity.
Measured live at dev@fa20136f29 (dev server, neo-theme-neo-dark, 6 inline dock tab containers mounted). 21 of the bridge's 23 contested tokens lose. Zero win.
The Problem
resources/scss/src/apps/workstation/Viewport.scss:16 opens the bridge with .workstation-viewport { … } — 23 custom properties mapping generic --grid-* / --tab-* primitives onto the --workstation-* palette. Its own header states the intent: "generic grid/tab primitives inherit its own palette in both skins instead of leaking their default blue/purple values."
The purple is leaking. The rendered viewport element is:
div#neo-viewport-1.workstation-viewport.neo-viewport.neo-theme-neo-dark.neo-flex-container
It carries the app class and the theme class. So both of these match the same element:
| declaration |
selector |
specificity |
outcome |
| bridge |
.workstation-viewport |
(0,1,0) |
loses |
| theme |
:root .neo-theme-neo-dark |
(0,2,0) |
wins |
Measured losses (bridge intent → what actually computes):
| token |
bridge declares |
computes to |
theme source |
--tab-indicator-background-color-active |
--workstation-signal #5eead4 |
#FFFFFF |
--cmp-tab-strip-selected |
--tab-button-text-color |
--workstation-ink-dim #8b97a8 |
#99A295 |
--sem-color-fg-neutral-subdued |
--tab-button-glyph-color |
--workstation-ink-dim #8b97a8 |
#99A295 |
--sem-color-fg-neutral-subdued |
--tab-button-background-color-hover |
color-mix(… signal 8% …) |
rgba(255,255,255,.05) |
--cmp-tab-unselected-bg-hover |
--tab-button-background-color-active |
color-mix(… signal 12% …) |
rgba(255,255,255,.1) |
--cmp-tab-unselected-bg-active |
--grid-container-cell-background-color |
--workstation-panel #141a23 |
#0E0F0D |
--sem-color-surface-neutral-default |
--grid-container-cell-background-color-even |
--workstation-panel-2 #1a212c |
#11141C |
--purple-950 |
--grid-container-border-color |
--workstation-line #262f3d |
#1D2E62 |
--purple-800 |
--grid-cell-background-color-hover |
color-mix(… signal 10% …) |
#182449 |
--purple-900 |
--grid-container-color |
--workstation-ink-dim #8b97a8 |
#F0F2F0 |
--sem-color-text-neutral-default |
--grid-header-button-justify-content |
flex-end |
start |
theme default |
…and 10 more, full list reproducible by the probe in the Evidence row below. The active-tab indicator is white where the app's accent is mint, the grid renders on the theme's olive-black over purple borders where the app is blue-slate, and one loss is layout, not colour (justify-content).
A positive control proves the bridge is well-formed and only fails where contested. The two tokens no theme declares — --grid-cell-progress-active-color and --grid-cell-progress-track-color — resolve to #5eead4 and #262f3d, the workstation values, exactly as authored. The bridge is not broken; it is out-ranked.
Two consequences are already compensated and are NOT part of this ticket's defect surface. Viewport.scss:172 re-declares the --grid-header-button-* family directly on .neo-grid-header-button, and that block wins — measured #1a212c / #8b97a8 / flex-end, all correct. Those upstream declarations at line 27-36 are therefore dead, not wrong. The file already documents why that second block exists (:168 — "Theme classes live on the grid primitives themselves"), which shows the collision was understood one element down and missed one element up.
How it got here
dda4aa04b4 (#15923 / PR #15928, 2026-07-25) moved the bridge from .workstation-workspace to .workstation-viewport so the ?popout= vessel — a viewport with no workspace — would reach it. That fix was correct for reachability and is not being reverted.
It also silently changed which side of the collision the bridge sits on, and nothing in the tree measured that:
.workstation-workspace is not matched by :root .neo-theme-neo-dark (verified live: the workspace div carries workstation-workspace neo-dock-workspace neo-flex-* and no theme class). At that scope the bridge was the only matching declaration and won by default.
.workstation-viewport is matched. At the new scope the bridge became the loser of a (0,1,0)-vs-(0,2,0) contest.
So the hoist traded a reachability defect for a specificity defect, and the app has been rendering theme paint ever since. #17963's design pass did not cause this — it landed 8 new --tab-header-* tokens onto an app whose bridge was already inert, which is why the result reads as "nothing changed".
The Architectural Reality
- The bridge's mapping is skin-independent (
--tab-button-glyph-color → --workstation-ink-dim, whatever that resolves to per skin), so it correctly lives in the structural sheet resources/scss/src/apps/workstation/Viewport.scss, not duplicated per theme. Only its selector is wrong. The skin-dependent half — the --workstation-* values themselves — already lives correctly in resources/scss/theme-neo-{dark,light}/apps/workstation/Viewport.scss.
- Moving the bridge into the theme layer is the wrong repair. It would be emitted at
:root .neo-theme-neo-dark, tying the theme's own (0,2,0) and resolving on load order. #17241 names that exact hazard in this tree — "Equal-specificity load-order ties are not a contract" — and CSS is emitted per source file, so no readable source order decides it. A tie is not a fix.
- The deterministic repair is a strictly-higher app selector that stays skin-agnostic.
:root .workstation-viewport.neo-viewport is (0,3,0): :root (0,1,0) + two classes. It beats the theme's (0,2,0) outright, names a real element (neo-viewport is the engine's viewport class, present in both boot modes), and needs no per-skin duplication.
- Descendants that carry the theme class themselves are a separate case and are already handled. A declaration matching an element directly beats any inherited value, so grid primitives — which do carry
neo-theme-* — need the element-level block that :172 already provides. The tab header toolbar and its buttons do not carry the theme class (nearest theme-classed ancestor is the viewport itself), so raising the viewport selector is sufficient for the whole tab surface.
- The
--tab-header-* family from #17963 was never bridged at all — it is 8 tokens newer than the bridge block and no one added rows for it. Even after the specificity repair those 8 stay theme-valued unless projections are authored.
The Fix
resources/scss/src/apps/workstation/Viewport.scss only. No engine change, no theme change, no JS.
- Raise the bridge selector from
.workstation-viewport to :root .workstation-viewport.neo-viewport, with an in-file comment recording the collision, the two specificities, and why the theme layer is not the home (the tie).
- Add the missing
--tab-header-* projections so #17963's chrome speaks the instrument's language: glyph rest → --workstation-ink-dim, glyph hover/active → --workstation-signal, hover/active surfaces → the app's established color-mix(… --workstation-signal …) convention, and --tab-header-inline-background-image → a --workstation-panel-2-based band. The size and radius the theme ships are density decisions, not identity — leave them unless measurement says otherwise.
- Delete the now-dead
--grid-header-button-* rows at :27-36 that :172 already supersedes, or keep them and say in-file why both exist. One or the other — not silence.
- A computed-style spec asserting the bridged tokens resolve to
--workstation-* values, with a mutation control: removing the bridge block must red it. Grace's own sharpening on #15923 applies directly — "a class check would have passed throughout this entire bug" — so the assertion reads computed values, never class presence.
Contract Ledger
| Target Surface |
Source of Authority |
Behavior |
Fallback |
Docs |
Evidence |
.workstation-viewport bridge block (resources/scss/src/apps/workstation/Viewport.scss:16) |
app-owned projection of --workstation-* onto engine token names |
deterministically out-ranks :root .neo-theme-neo-* at (0,3,0) |
none — a losing bridge is the defect |
in-file collision comment naming both specificities |
computed-style assertion per token + mutation control |
--tab-header-action-{size,border-radius,glyph-color,glyph-color-hover,glyph-color-active,background-color-hover,background-color-active} |
#17963 / merged PR #17977 theme surface |
projected onto the workstation palette in the app bridge |
theme values (today's behavior) |
bridge row comments |
computed glyph/hover colours on a live dock action |
--tab-header-inline-background-image |
#17950 hook, valued by #17963 |
app band reading --workstation-panel-2 |
theme gradient |
bridge row comment |
computed background-image on .neo-tab-container-inline > .neo-tab-header-toolbar |
--grid-header-button-* rows at :27-36 |
superseded by the element-level block at :172 |
removed, or retained with a written reason |
n/a |
in-file |
grep + unchanged computed values on .neo-grid-header-button |
No engine or theme surface changes. The engine's token names and the themes' values are consumed exactly as published.
Decision Record impact
aligned-with ADR 0029. This is presentation-layer consumption inside one app; it neither challenges nor amends the docking design.
Acceptance Criteria
Out of Scope
- The general "should a theme out-specify its consumers" question. That is the layering principle
#17241 owns; this ticket repairs one app deterministically and does not settle the engine-wide rule. If #17241 later lands a general mechanism, this app's raise is the first thing it should delete.
- Any engine or theme SCSS change. The themes' values are correct for a theme; they are simply not this instrument's identity.
- The
--tab-header-action-{size,border-radius} density values — theme-owned density, not app identity, unless a measurement in this lane says otherwise.
- Dock behaviour, action semantics, gating, ordering — all shipped and frozen by the
#13158 family leaves.
apps/agentos/fleet/FleetCockpit.scss, whose own dock overrides belong to #17241.
Avoided Traps
- Reverting the
#15923 hoist. It cured a real defect — the ?popout= vessel rendering unskinned — and the workspace mode's regression is a second bug the hoist exposed, not a reason to restore the first. Both boot modes must resolve the bridge.
- Moving the bridge into
theme-neo-*/apps/workstation/. It reads like the tidy home and it is a load-order coin flip against the theme's own (0,2,0). #17241 already named this hazard in this tree.
- Reaching for
!important. It would work, it would hide the specificity fact, and it would be copied by the next app. #17241's splitter-promotion discipline is explicit that a needed !important is a specificity defect to fix, not a tool.
- Asserting the fix with a class or selector check.
#15923's post-mortem is the anchor: the theme class was never missing, so a class check passed for the entire lifetime of that bug. Only a computed-style assertion moves on the axis the defect moves.
- Treating this as
#17963 shipping wrong. #17963's ACs were theme-layer and witness-layer, and PR #17977 met them. The gap is one my own ticket did not name — it called Workstation a "living witness" and never asked whether the witness would speak the app's palette.
Related
Parent: #13158. Exposed by #17963 (merged PR #17977) landing 8 new tokens onto an already-inert bridge. Root cause introduced by dda4aa04b4 for #15923 (PR #15928); the sibling pane-skin hoist is #15945 (PR #15946). Adjacent layering principle: #17241 (dock visual language lives in apps/, not src/dashboard) — that ticket's Out of Scope explicitly excludes Viewport.scss's non-dock content, which is exactly this surface. Token hook: #17950.
Live latest-open sweep: latest 20 open issues checked 2026-08-31T21:33:49Z; no equivalent — the nearest, #17241, owns the engine/app layering for dock chrome and excludes this file's non-dock content. A2A in-flight claim sweep: 30 most recent messages, newest 2026-08-31T21:28:46Z; no claim on this scope. Filed inside the operator's 21:28Z topic freeze (docklayout / tab header actions / styling).
Origin Session ID: a4fa24e6-b983-4ecd-81a2-8b98baef9cfc
Retrieval Hint: "workstation palette bridge out-specified by theme on shared viewport element grid tab tokens leak purple"
— Vega (Opus 5, Claude Code) 🌿
Context
Reported by @tobiu 2026-08-31 against
dev@fa20136f29(post#17963/PR#17977): "looking into apps/workstation it looks exactly like 8h ago => no inline tab styling for the app."The report is correct, and the cause is not a missing stylesheet. Every sheet loads, every token the design pass shipped resolves, and the header actions project and gate exactly as designed. What fails is one layer lower: Workstation's palette bridge is out-specified by the theme on the element the two share, so the app renders the neo theme's neutral ramp instead of its own identity.
Measured live at
dev@fa20136f29(dev server,neo-theme-neo-dark, 6 inline dock tab containers mounted). 21 of the bridge's 23 contested tokens lose. Zero win.The Problem
resources/scss/src/apps/workstation/Viewport.scss:16opens the bridge with.workstation-viewport { … }— 23 custom properties mapping generic--grid-*/--tab-*primitives onto the--workstation-*palette. Its own header states the intent: "generic grid/tab primitives inherit its own palette in both skins instead of leaking their default blue/purple values."The purple is leaking. The rendered viewport element is:
It carries the app class and the theme class. So both of these match the same element:
.workstation-viewport:root .neo-theme-neo-darkMeasured losses (bridge intent → what actually computes):
--tab-indicator-background-color-active--workstation-signal#5eead4#FFFFFF--cmp-tab-strip-selected--tab-button-text-color--workstation-ink-dim#8b97a8#99A295--sem-color-fg-neutral-subdued--tab-button-glyph-color--workstation-ink-dim#8b97a8#99A295--sem-color-fg-neutral-subdued--tab-button-background-color-hovercolor-mix(… signal 8% …)rgba(255,255,255,.05)--cmp-tab-unselected-bg-hover--tab-button-background-color-activecolor-mix(… signal 12% …)rgba(255,255,255,.1)--cmp-tab-unselected-bg-active--grid-container-cell-background-color--workstation-panel#141a23#0E0F0D--sem-color-surface-neutral-default--grid-container-cell-background-color-even--workstation-panel-2#1a212c#11141C--purple-950--grid-container-border-color--workstation-line#262f3d#1D2E62--purple-800--grid-cell-background-color-hovercolor-mix(… signal 10% …)#182449--purple-900--grid-container-color--workstation-ink-dim#8b97a8#F0F2F0--sem-color-text-neutral-default--grid-header-button-justify-contentflex-endstart…and 10 more, full list reproducible by the probe in the Evidence row below. The active-tab indicator is white where the app's accent is mint, the grid renders on the theme's olive-black over purple borders where the app is blue-slate, and one loss is layout, not colour (
justify-content).A positive control proves the bridge is well-formed and only fails where contested. The two tokens no theme declares —
--grid-cell-progress-active-colorand--grid-cell-progress-track-color— resolve to#5eead4and#262f3d, the workstation values, exactly as authored. The bridge is not broken; it is out-ranked.Two consequences are already compensated and are NOT part of this ticket's defect surface.
Viewport.scss:172re-declares the--grid-header-button-*family directly on.neo-grid-header-button, and that block wins — measured#1a212c/#8b97a8/flex-end, all correct. Those upstream declarations at line 27-36 are therefore dead, not wrong. The file already documents why that second block exists (:168— "Theme classes live on the grid primitives themselves"), which shows the collision was understood one element down and missed one element up.How it got here
dda4aa04b4(#15923/ PR#15928, 2026-07-25) moved the bridge from.workstation-workspaceto.workstation-viewportso the?popout=vessel — a viewport with no workspace — would reach it. That fix was correct for reachability and is not being reverted.It also silently changed which side of the collision the bridge sits on, and nothing in the tree measured that:
.workstation-workspaceis not matched by:root .neo-theme-neo-dark(verified live: the workspace div carriesworkstation-workspace neo-dock-workspace neo-flex-*and no theme class). At that scope the bridge was the only matching declaration and won by default..workstation-viewportis matched. At the new scope the bridge became the loser of a (0,1,0)-vs-(0,2,0) contest.So the hoist traded a reachability defect for a specificity defect, and the app has been rendering theme paint ever since.
#17963's design pass did not cause this — it landed 8 new--tab-header-*tokens onto an app whose bridge was already inert, which is why the result reads as "nothing changed".The Architectural Reality
--tab-button-glyph-color→--workstation-ink-dim, whatever that resolves to per skin), so it correctly lives in the structural sheetresources/scss/src/apps/workstation/Viewport.scss, not duplicated per theme. Only its selector is wrong. The skin-dependent half — the--workstation-*values themselves — already lives correctly inresources/scss/theme-neo-{dark,light}/apps/workstation/Viewport.scss.:root .neo-theme-neo-dark, tying the theme's own (0,2,0) and resolving on load order.#17241names that exact hazard in this tree — "Equal-specificity load-order ties are not a contract" — and CSS is emitted per source file, so no readable source order decides it. A tie is not a fix.:root .workstation-viewport.neo-viewportis (0,3,0)::root(0,1,0) + two classes. It beats the theme's (0,2,0) outright, names a real element (neo-viewportis the engine's viewport class, present in both boot modes), and needs no per-skin duplication.neo-theme-*— need the element-level block that:172already provides. The tab header toolbar and its buttons do not carry the theme class (nearest theme-classed ancestor is the viewport itself), so raising the viewport selector is sufficient for the whole tab surface.--tab-header-*family from#17963was never bridged at all — it is 8 tokens newer than the bridge block and no one added rows for it. Even after the specificity repair those 8 stay theme-valued unless projections are authored.The Fix
resources/scss/src/apps/workstation/Viewport.scssonly. No engine change, no theme change, no JS..workstation-viewportto:root .workstation-viewport.neo-viewport, with an in-file comment recording the collision, the two specificities, and why the theme layer is not the home (the tie).--tab-header-*projections so#17963's chrome speaks the instrument's language: glyph rest →--workstation-ink-dim, glyph hover/active →--workstation-signal, hover/active surfaces → the app's establishedcolor-mix(… --workstation-signal …)convention, and--tab-header-inline-background-image→ a--workstation-panel-2-based band. The size and radius the theme ships are density decisions, not identity — leave them unless measurement says otherwise.--grid-header-button-*rows at:27-36that:172already supersedes, or keep them and say in-file why both exist. One or the other — not silence.--workstation-*values, with a mutation control: removing the bridge block must red it. Grace's own sharpening on#15923applies directly — "a class check would have passed throughout this entire bug" — so the assertion reads computed values, never class presence.Contract Ledger
.workstation-viewportbridge block (resources/scss/src/apps/workstation/Viewport.scss:16)--workstation-*onto engine token names:root .neo-theme-neo-*at (0,3,0)--tab-header-action-{size,border-radius,glyph-color,glyph-color-hover,glyph-color-active,background-color-hover,background-color-active}#17963/ merged PR#17977theme surface--tab-header-inline-background-image#17950hook, valued by#17963--workstation-panel-2background-imageon.neo-tab-container-inline > .neo-tab-header-toolbar--grid-header-button-*rows at:27-36:172.neo-grid-header-buttonNo engine or theme surface changes. The engine's token names and the themes' values are consumed exactly as published.
Decision Record impact
aligned-with ADR 0029. This is presentation-layer consumption inside one app; it neither challenges nor amends the docking design.Acceptance Criteria
--workstation-*intent on the live viewport element in both neo skins — asserted on computed values, not on class presence, and not on SCSS source.--workstation-signal, and unselected tab label/glyph render--workstation-ink-dim, in both neo skins.--tab-header-*family from#17963is projected onto the workstation palette; dock header action glyphs read the app's ink and signal, not#99A295/#FFFFFF.--grid-header-button-*computed values on.neo-grid-header-buttonare unchanged by this ticket — the element-level block at:172already wins, and the repair must not move it.?popout=vessel keeps the reachability#15923bought: the bare-pane boot mode resolves the same bridged values as the workspace mode. This is the regression#15928was filed for and the raise must not undo it.!importantanywhere in the repair. If one seems required, the specificity cause is fixed instead (the discipline#17241recorded for the splitter promotion).Out of Scope
#17241owns; this ticket repairs one app deterministically and does not settle the engine-wide rule. If#17241later lands a general mechanism, this app's raise is the first thing it should delete.--tab-header-action-{size,border-radius}density values — theme-owned density, not app identity, unless a measurement in this lane says otherwise.#13158family leaves.apps/agentos/fleet/FleetCockpit.scss, whose own dock overrides belong to#17241.Avoided Traps
#15923hoist. It cured a real defect — the?popout=vessel rendering unskinned — and the workspace mode's regression is a second bug the hoist exposed, not a reason to restore the first. Both boot modes must resolve the bridge.theme-neo-*/apps/workstation/. It reads like the tidy home and it is a load-order coin flip against the theme's own (0,2,0).#17241already named this hazard in this tree.!important. It would work, it would hide the specificity fact, and it would be copied by the next app.#17241's splitter-promotion discipline is explicit that a needed!importantis a specificity defect to fix, not a tool.#15923's post-mortem is the anchor: the theme class was never missing, so a class check passed for the entire lifetime of that bug. Only a computed-style assertion moves on the axis the defect moves.#17963shipping wrong.#17963's ACs were theme-layer and witness-layer, and PR#17977met them. The gap is one my own ticket did not name — it called Workstation a "living witness" and never asked whether the witness would speak the app's palette.Related
Parent: #13158. Exposed by #17963 (merged PR #17977) landing 8 new tokens onto an already-inert bridge. Root cause introduced by
dda4aa04b4for #15923 (PR #15928); the sibling pane-skin hoist is #15945 (PR #15946). Adjacent layering principle: #17241 (dock visual language lives in apps/, notsrc/dashboard) — that ticket's Out of Scope explicitly excludesViewport.scss's non-dock content, which is exactly this surface. Token hook: #17950.Live latest-open sweep: latest 20 open issues checked 2026-08-31T21:33:49Z; no equivalent — the nearest, #17241, owns the engine/app layering for dock chrome and excludes this file's non-dock content. A2A in-flight claim sweep: 30 most recent messages, newest 2026-08-31T21:28:46Z; no claim on this scope. Filed inside the operator's 21:28Z topic freeze (docklayout / tab header actions / styling).
Origin Session ID: a4fa24e6-b983-4ecd-81a2-8b98baef9cfc
Retrieval Hint: "workstation palette bridge out-specified by theme on shared viewport element grid tab tokens leak purple"
— Vega (Opus 5, Claude Code) 🌿