Status-color contract
Status color is semantic across every Layers operator surface:
- Green means healthy, ok, live, working, complete, or otherwise operating normally.
- Amber means warning, stale, degraded, or needs attention.
- Red means error, destructive, blocked, or failed only.
A healthy state rendered in red is a defect, even when red was an intentional visual motif.
Information blue is a separate, non-status semantic exposed as --info; it does not replace any
of the three status meanings.
Tokens
CSS status UI uses these pairs:
| Semantic | Surface | Foreground |
|---|---|---|
| ok | --status-ok | --status-ok-foreground |
| warning | --status-warn | --status-warn-foreground |
| error | --status-error | --status-error-foreground |
Tailwind exposes the same values as --color-status-ok, --color-status-warn, and
--color-status-error, including each -foreground counterpart. React Native consumers import
STATUS_COLORS from @layers/ui/tokens/status-colors; they do not hand-copy hex values.
Warn splits ink from surface (founder ruling 2026-07-31)
--status-warn aliases --warning-background (#ff9f66, constant across themes), not
--warning. Warn was the only semantic without its own fill: ok already aliased
--success-background and error --destructive-background, while warn pointed at --warning — a
value tuned to be legible as text on white (L44%). Using a text color as a badge fill and
setting near-black on top is what made every “Needs attention” chip read muddy.
--warningis the INK:text-warning,bg-warning/10, stale-age labels, inline notices. Still tuned per theme, because text has to hold against two different page colors.--warning-backgroundis the FILL: badges, the rail notification dot, anything with--inkon top. One value for both themes, exactly like--success-background.
The fill sits at L70% against ok’s L63% so the three status chips read as one family, and it is
deliberately not the literal brand peach #ffc0a2 — that is --chart-series-4, and status
colors are never series colors.
The status pairs alias the existing success, warning, and destructive theme families. The shared
ok surface uses --ink as its foreground because white does not provide sufficient contrast on
the bright #59e67e ok green.
Error finished the same split (ruling 2026-08-01)
Warn’s split fixed warn and left error half-done. --status-error aliased --destructive-background
and --status-error-foreground aliased --destructive-foreground — and those two tokens held the
identical value, so the documented pair was red on red at 1:1. Nothing rendered it (every
consumer used --status-error as ink, never as a fill with its foreground on top), so nothing was
broken. It was a loaded gun in the token layer: the first person to follow the table above would
have shipped an invisible chip.
--destructiveis the INK:text-destructive,bg-destructive/10,border-destructive/30.--destructive-foregroundis the ink that sits ON the fill, and it is now genuinely that.
Unlike ok and warn, this foreground is not --ink and not one value across themes. Their
fills are bright pastels; --destructive is a deep red in light (white reads 4.88:1 there, near-black
only 4.30:1) and a light salmon in dark (--ink reads 7.26:1, white only 2.89:1). The fill flips
lightness between themes, so its ink has to flip with it.
Six call sites were using text-/bg-destructive-foreground as destructive ink or tint — they were
migrated to --destructive first, because redefining the token under them would have turned red
error text white on a white card.
Annotation and enforcement
Every element whose color communicates status declares data-status="ok",
data-status="warn", or data-status="error". The shared Badge uses
data-status="warning" for its public warning variant; the guard treats that spelling as the
warn semantic.
CSS selectors that apply status color include the attribute selector. Inside that selector:
- use only the matching
--status-*or--color-status-*family for the status signal; - do not use raw hex,
rgb(...), raw palette utilities, or fallback colors; - do not use legacy
--success-*,--warning*,--destructive*, or--info*as a status color; - do not map one status to another status token.
The blocking Stylelint rule is layers/status-color-contract. It validates explicit status
selectors and follows file-local aliases such as the console’s live motif back to their underlying
status semantic — including alias chains that resolve through the legacy families
(--success* → ok, --warning* → warn, --destructive* → error, --info* → info), so the
original console defect (--cora-explore-live aliasing the destructive family under a live
surface) is caught if reintroduced. Neutral colors used only to compose a tint remain allowed;
the matching status token still carries the meaning. Unit fixtures and the top-level negative
control cover raw green, a dangling hardcoded fallback, an ok-to-error semantic mismatch, and a
file-local alias resolving to the destructive family.
Shipped inventory
The implementation inventory is code-grounded against the 2026-07-19 worktree. Every defect row below is fixed in the shared contract or its consuming surface.
| Surface | Semantic | Previous rendering | Resolution | Status |
|---|---|---|---|---|
Shared Badge success | ok | raw Tailwind greens | --color-status-ok; annotated ok | Fixed |
Shared Badge warning | warning | no semantic variant | --color-status-warn; annotated warning | Fixed |
Shared OutcomeCard success | ok | raw Tailwind green text | --color-status-ok; annotated ok | Fixed |
| Shared and web-v1 stat-card positive delta | ok | hardcoded green fallback | --status-ok; annotated ok | Fixed |
| Shared and web-v1 stat-card negative delta | error | hardcoded red fallback | --status-error; annotated error | Fixed |
| web-v1 Home connected/event/live dots | ok | success token or dangling info fallback | status dots use --status-ok; info uses defined --info | Fixed |
| web-v1 account connection, authorization, SDK, and eligibility indicators | ok / warning | raw green and amber Tailwind palettes | matching status utilities and annotations | Fixed |
| web-v1 social account availability dot | ok | undefined success token with hardcoded green fallback | --status-ok; annotated ok | Fixed |
| Console explore live motif, glyphs, borders, and backgrounds | ok | destructive-red live alias | live alias resolves to --status-ok; live DOM is annotated ok | Fixed |
| Console build-job heartbeat | ok / warning when stale | selector-local success override | healthy uses --status-ok; stale uses --status-warn | Fixed |
| Console consensus outcome and connector health lines | ok / warning / error | neutral, generic warning, or destructive utilities | matching status utilities and annotations | Fixed |
| Console status badges | ok / warning / error | shared variants without a status contract | inherited semantic Badge variants and annotations | Fixed |
| Mobile chip tone map | ok / warning / error / info | legacy tone names backed by app-local palette values | shared STATUS_COLORS via semantic tones | Fixed |
| web-v2 Badge and stat-card consumers | ok / warning / error | inherited shared defects | inherit the shared component fixes | Fixed |
The mobile code/typecheck is part of the automated vehicle. Color perception on an Expo device or simulator remains an operator visual check, as does the final light/dark founder walk.
Cross-surface follow-up (post-#2973)
A later cross-surface audit under the same initiative (48d6a97a) swept the Cora-owned surfaces the
original research index missed. Both rows below sit outside the Stylelint rule’s reach — it only
inspects .css files carrying a [data-status] selector, so React/RN component variants and Astro
theme tokens are structurally invisible to it (see tools/stylelint/status-color-rule.cjs:102).
| Surface | Indicator | Semantic | Rendered | Disposition |
|---|---|---|---|---|
apps/cora-marketing/src/pages/live.astro | .st-run “RUNNING”, .nb-name “IN PROGRESS · N RUNNING”, the .tok-live live-burn counter, and the .now-* frame | live / running (a working state) | --t-red (red) across all register themes | Open — founder decision (see below) |
apps/cora-console and apps/cora-mobile INBOX_KIND.delivery_attention | ”delivery” inbox badge | blocking delivery disposition | destructive / error (red) | Kept red. A delivery awaiting disposition is a blocking item, not mere attention, and red is permitted for blocking. Consistent across console and mobile; #2973 deliberately left it destructive when it reclassified the stale-squawk and dropped-hunch badges. |
live.astro — why this is a founder call, not a mechanical swap
live.astro is a deliberate iterm/ANSI register: nine palette tokens per theme across ~9 themes,
with a documented editorial metaphor (live.astro:476) — red = the live burn, green = shipped,
yellow = comparisons, magenta = cora. “Running” is red because red = tokens burning now; green
already means shipped / done.
The literal ruling (“a live/working state styled red is a defect, even as a deliberate motif”) is the same shape as the console explore “on-air” motif this initiative migrated. Three things still make the marketing page a distinct call rather than an implementer swap:
- It is a public marketing microsite, not an operator surface — an outward-facing change.
- Red is a woven identity color (“the burn”) across nine register themes, not a status token; migrating it is a palette redesign, not a token remap.
- “Running” and “shipped” are the page’s central visual contrast (burning-now vs. done). Making both green collapses that distinction and the page’s editorial concept.
Recommendation: route to the founder for a ruling on whether the sweep governs the public marketing aesthetic; do not recolor unilaterally. No inverse defects (an error/failed state styled green) were found on any surface.
Research-index process finding
apps/cora-console, apps/cora-mobile, and apps/cora-marketing were absent from the research
index used by earlier planning passes — cora-marketing surfaced only in the later cross-surface
audit above. This implementation resolved the first two surfaces by direct working-tree inspection.
The codebase-knowledge-graph initiative should rebuild coverage for all three apps before a future
cross-surface inventory relies on that index alone.