This issue challenges a settled decision and carries both sides. Nothing changes
until Armando ratifies here. No milestone — this is not queued work.
The observation
Two dogfooding sessions, 2026-08-06. The near-black/white filled buttons read as
inert — as chrome, or as disabled controls — including where they are the
screen's one primary CTA. The reaction is not "that is the thing to press"; it is
a scan for the thing to press, followed by finding it.
That is one person's reaction on one display, and it is recorded as such. It is
also the exact property the current rule is a bet on, which is why it is worth
re-opening rather than absorbing.
Exploration — where the near-black primary is the main CTA
variant="primary" renders bg-primary (#1e2130) with white ink. Project-level
screens:
| Screen |
Site |
Line |
| Projects |
"New project" (header) |
screens/ProjectsScreen.tsx:64-67 |
| Projects |
"Create" (dialog confirm) |
screens/ProjectsScreen.tsx:199-207 |
| Project header |
"Annotate" — the page's answer to what now? |
screens/ProjectScreen.tsx:603-612 |
| Project header |
"Ingest" — primary when there is no batch to annotate |
screens/ProjectScreen.tsx:614-624 |
| Project header |
"Rename" (dialog confirm) |
screens/ProjectScreen.tsx:935-943 |
| Schema tab |
"Save version" — the header defers so this can be the loudest thing on the page |
screens/SchemaEditor.tsx:301-311, and ProjectScreen.tsx:540-549 for the deferral |
| Batches |
"Approve" / lifecycle advance |
screens/BatchLifecycle.tsx |
| Dataset |
"Publish release", "Export" (dialog confirms) |
screens/DatasetScreen.tsx |
| Ingest |
the active step's action |
screens/IngestScreen.tsx |
| Annotator |
"Save and next" — the frame's flow verb (#383/#384) |
annotator/AnnotationPage.tsx |
Adjacent, and part of the same question: the checklist's active step marker is
bg-primary text-primary-foreground (patterns/Checklist.tsx:60-67), the
selected class row is border-primary + bg-primary/10, the active tab rule is a
2px primary underline, and the active tool in the annotator's palette is
primary. The near-black is doing five jobs: primary action, active tab,
active tool, selection tint, and rail chrome.
The original rationale — quoted in full
From DESIGN.md (Overview, lines 95-104):
VisionSet's interface is neutral-first, content-first, and accessible. The
screen is almost monochrome — cool greys with a faint indigo undertone — and the
colour a person can act on is a near-black (#1e2130): filled buttons, the
active tab's rule, the active tool. A dark rail in that same near-black frames a
bright content area.
Robomous coral (#e85d44) is not the interface's colour. It appears in
exactly two places (see Where the brand is, below), and everything else that
used to wear it now wears the near-black. The reasoning is in the token file and
worth repeating here: an interface whose every button is brand-coloured spends
the brand on "Cancel". Making colour scarce is what lets the two places it
survives actually mean something.
From DESIGN.md, One filled button per view (lines 208-221):
Principle 8 says a screen answers "what do I do next?" with exactly one primary
CTA. With a near-black primary the rule is visual as well as structural: a
filled button is now the loudest thing on a grey page, so two of them compete
far more than two coral ones did. Sibling actions are the outlined secondary
variant — card fill, input border, foreground ink. A row action inside a
table is per-row, never the view's forward action, so those are secondary too.
The known exceptions, all legitimate: a modal Dialog's confirm button (it
overlays the page, so it is its own view), and the steps of the ingest stepper
(only the active step renders).
From DESIGN.md, Where the brand is (lines 189-206) — the invariant this
palette buys, currently at exactly three hits:
Coral appears in the product in exactly two places, and adding a third is a
design decision rather than a styling one — raise it in review, do not put it
in a diff:
- The wordmark in the rail. 2. The ingest progress bar's fill.
A progress bar is the one piece of chrome a person watches rather than reads,
which is where the coral buys attention instead of spending it.
And principle 3, Content over chrome: "the brand is not a surface fill and is
not an interaction colour at all."
The change landed in #323 (neutral-first palette), whose own recorded finding
was that two tokens converging made the rail's active item invisible with every
suite green — i.e. this palette's failure mode is one that automated checks
cannot see, which is an argument for taking a subjective report like this one
seriously in either direction.
The tension, stated fairly
The rule's claim is that on a near-monochrome page a filled near-black button is
the loudest thing there is — scarcity of colour is what makes it loud. The
dogfooding observation is that near-black is not read as actionable; it is
read as structural, because near-black is also the rail, the tab rule, the
tool state and the selection tint. Loud and actionable are not the same property,
and the rule may have bought the first while assuming the second.
Both can be true at once, which is why this is a real question rather than a bug.
Three options — no recommendation
1. Keep the near-black; raise hierarchy through size, weight and placement.
Costs nothing in tokens, preserves the whole two-site brand invariant and every
argument above. Concretely: larger primary buttons, heavier label weight, more
isolation around the CTA, possibly a subtle elevation. Risk: if the problem is
that near-black is categorically read as chrome, geometry will not fix it, and
the work is spent.
2. Introduce a dedicated action colour, distinct from the brand. The palette
already has "a faint indigo undertone" in its greys (#1e2130 is a blue-black);
the same hue at real saturation would be an action colour that is unmistakably
interactive and unmistakably not Robomous coral. Coral stays at its two sites.
Cost: a new token, a real re-styling of every primary site, and a new question —
does the action colour also carry active tab / active tool / selection, or do
those stay near-black? (If they do stay, this is arguably the cleanest outcome:
one colour means "you can press this", another means "this is where you are.")
3. Extend brand coral to primary actions. This inverts the documented
rationale — principle 3 says the brand is not an interaction colour at all, and
the whole Where the brand is invariant, its git grep check and its "raise it
in review, do not put it in a diff" clause would be retired. Recorded as the
radical option because it is the one #323 deliberately walked away from, and
because it reintroduces the failure the rationale names: an interface whose every
button is brand-coloured spends the brand on "Cancel".
Next step when this is picked up
Token-level mockups on real screens before any decision — at minimum the
project header (two buttons, one primary), the Schema tab (a primary that commits
typed work), and the annotator top bar (a primary next to an active-tool
near-black, which is where options 1 and 2 diverge most). Side by side, same
screen, same content.
Nothing in frontend/ changes until Armando ratifies an option in this thread.
cf. #391, which spends the success/warning/destructive
tokens and is independent of this one — it does not touch primary and
should not wait on this decision.
This issue challenges a settled decision and carries both sides. Nothing changes
until Armando ratifies here. No milestone — this is not queued work.
The observation
Two dogfooding sessions, 2026-08-06. The near-black/white filled buttons read as
inert — as chrome, or as disabled controls — including where they are the
screen's one primary CTA. The reaction is not "that is the thing to press"; it is
a scan for the thing to press, followed by finding it.
That is one person's reaction on one display, and it is recorded as such. It is
also the exact property the current rule is a bet on, which is why it is worth
re-opening rather than absorbing.
Exploration — where the near-black primary is the main CTA
variant="primary"rendersbg-primary(#1e2130) with white ink. Project-levelscreens:
screens/ProjectsScreen.tsx:64-67screens/ProjectsScreen.tsx:199-207screens/ProjectScreen.tsx:603-612screens/ProjectScreen.tsx:614-624screens/ProjectScreen.tsx:935-943screens/SchemaEditor.tsx:301-311, andProjectScreen.tsx:540-549for the deferralscreens/BatchLifecycle.tsxscreens/DatasetScreen.tsxscreens/IngestScreen.tsxannotator/AnnotationPage.tsxAdjacent, and part of the same question: the checklist's active step marker is
bg-primary text-primary-foreground(patterns/Checklist.tsx:60-67), theselected class row is
border-primary+bg-primary/10, the active tab rule is a2px
primaryunderline, and the active tool in the annotator's palette isprimary. The near-black is doing five jobs: primary action, active tab,active tool, selection tint, and rail chrome.
The original rationale — quoted in full
From
DESIGN.md(Overview, lines 95-104):From
DESIGN.md, One filled button per view (lines 208-221):From
DESIGN.md, Where the brand is (lines 189-206) — the invariant thispalette buys, currently at exactly three hits:
And principle 3, Content over chrome: "the brand is not a surface fill and is
not an interaction colour at all."
The change landed in #323 (neutral-first palette), whose own recorded finding
was that two tokens converging made the rail's active item invisible with every
suite green — i.e. this palette's failure mode is one that automated checks
cannot see, which is an argument for taking a subjective report like this one
seriously in either direction.
The tension, stated fairly
The rule's claim is that on a near-monochrome page a filled near-black button is
the loudest thing there is — scarcity of colour is what makes it loud. The
dogfooding observation is that near-black is not read as actionable; it is
read as structural, because near-black is also the rail, the tab rule, the
tool state and the selection tint. Loud and actionable are not the same property,
and the rule may have bought the first while assuming the second.
Both can be true at once, which is why this is a real question rather than a bug.
Three options — no recommendation
1. Keep the near-black; raise hierarchy through size, weight and placement.
Costs nothing in tokens, preserves the whole two-site brand invariant and every
argument above. Concretely: larger primary buttons, heavier label weight, more
isolation around the CTA, possibly a subtle elevation. Risk: if the problem is
that near-black is categorically read as chrome, geometry will not fix it, and
the work is spent.
2. Introduce a dedicated action colour, distinct from the brand. The palette
already has "a faint indigo undertone" in its greys (
#1e2130is a blue-black);the same hue at real saturation would be an action colour that is unmistakably
interactive and unmistakably not Robomous coral. Coral stays at its two sites.
Cost: a new token, a real re-styling of every
primarysite, and a new question —does the action colour also carry active tab / active tool / selection, or do
those stay near-black? (If they do stay, this is arguably the cleanest outcome:
one colour means "you can press this", another means "this is where you are.")
3. Extend brand coral to primary actions. This inverts the documented
rationale — principle 3 says the brand is not an interaction colour at all, and
the whole Where the brand is invariant, its
git grepcheck and its "raise itin review, do not put it in a diff" clause would be retired. Recorded as the
radical option because it is the one #323 deliberately walked away from, and
because it reintroduces the failure the rationale names: an interface whose every
button is brand-coloured spends the brand on "Cancel".
Next step when this is picked up
Token-level mockups on real screens before any decision — at minimum the
project header (two buttons, one primary), the Schema tab (a primary that commits
typed work), and the annotator top bar (a primary next to an active-tool
near-black, which is where options 1 and 2 diverge most). Side by side, same
screen, same content.
Nothing in
frontend/changes until Armando ratifies an option in this thread.cf.#391, which spends thesuccess/warning/destructivetokens and is independent of this one — it does not touch
primaryandshould not wait on this decision.