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:

SemanticSurfaceForeground
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.

  • --warning is 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-background is the FILL: badges, the rail notification dot, anything with --ink on 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.

  • --destructive is the INK: text-destructive, bg-destructive/10, border-destructive/30.
  • --destructive-foreground is 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.

SurfaceSemanticPrevious renderingResolutionStatus
Shared Badge successokraw Tailwind greens--color-status-ok; annotated okFixed
Shared Badge warningwarningno semantic variant--color-status-warn; annotated warningFixed
Shared OutcomeCard successokraw Tailwind green text--color-status-ok; annotated okFixed
Shared and web-v1 stat-card positive deltaokhardcoded green fallback--status-ok; annotated okFixed
Shared and web-v1 stat-card negative deltaerrorhardcoded red fallback--status-error; annotated errorFixed
web-v1 Home connected/event/live dotsoksuccess token or dangling info fallbackstatus dots use --status-ok; info uses defined --infoFixed
web-v1 account connection, authorization, SDK, and eligibility indicatorsok / warningraw green and amber Tailwind palettesmatching status utilities and annotationsFixed
web-v1 social account availability dotokundefined success token with hardcoded green fallback--status-ok; annotated okFixed
Console explore live motif, glyphs, borders, and backgroundsokdestructive-red live aliaslive alias resolves to --status-ok; live DOM is annotated okFixed
Console build-job heartbeatok / warning when staleselector-local success overridehealthy uses --status-ok; stale uses --status-warnFixed
Console consensus outcome and connector health linesok / warning / errorneutral, generic warning, or destructive utilitiesmatching status utilities and annotationsFixed
Console status badgesok / warning / errorshared variants without a status contractinherited semantic Badge variants and annotationsFixed
Mobile chip tone mapok / warning / error / infolegacy tone names backed by app-local palette valuesshared STATUS_COLORS via semantic tonesFixed
web-v2 Badge and stat-card consumersok / warning / errorinherited shared defectsinherit the shared component fixesFixed

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).

SurfaceIndicatorSemanticRenderedDisposition
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-* framelive / running (a working state)--t-red (red) across all register themesOpen — founder decision (see below)
apps/cora-console and apps/cora-mobile INBOX_KIND.delivery_attention”delivery” inbox badgeblocking delivery dispositiondestructive / 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:

  1. It is a public marketing microsite, not an operator surface — an outward-facing change.
  2. 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.
  3. “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.

Raw markdown for agents: /docs/status-colors.md