Skip to content

Color

A near-monochrome cool-neutral palette with one quiet indigo, four semantic colors, ink steps and ten option hues.

The palette by role#

A near-monochrome cool-neutral set on hue 265, one indigo on hue 272, four semantic colors, ink steps for data, a chart set and ten option hues. Every swatch shows light on the left and dark on the right.

Neutral#

Planes, fills, ink and lines. Most of every screen is drawn from these seventeen.

White Plane--background

The content plane, table cells, the app header and grid footers.

Light
oklch(1 0 0)
Dark
oklch(0.178 0.005 265)
Card White--card

Cards and panels. Equal to the plane in light, a step lighter in dark.

Light
oklch(1 0 0)
Dark
oklch(0.205 0.006 265)
Popover White--popover

Menus, popovers, selects and dialogs. Another step lighter in dark.

Light
oklch(1 0 0)
Dark
oklch(0.225 0.007 265)
Row Mist--surface

The data-grid row hover.

Light
oklch(0.985 0.0015 265)
Dark
oklch(0.198 0.006 265)
Cool Rail--sidebar

The sidebar layer.

Light
oklch(0.978 0.0025 265)
Dark
oklch(0.158 0.005 265)
Rail Ink--sidebar-foreground

Nav text in the sidebar.

Light
oklch(0.34 0.01 265)
Dark
oklch(0.78 0.008 265)
Rail Highlight--sidebar-accent

The hover and active nav item fill.

Light
oklch(0.936 0.005 265)
Dark
oklch(0.23 0.006 265)
Well Gray--muted

Tint wells at 70%, kanban columns at 60%, tab tracks, meter tracks, keys.

Light
oklch(0.965 0.0035 265)
Dark
oklch(0.235 0.006 265)
Quiet Fill--secondary

Secondary buttons.

Light
oklch(0.962 0.004 265)
Dark
oklch(0.25 0.007 265)
Quiet Fill Ink--secondary-foreground

Text on Quiet Fill.

Light
oklch(0.26 0.008 265)
Dark
oklch(0.93 0.004 265)
Menu Hover--accent

The highlighted item in menus, selects and comboboxes.

Light
oklch(0.955 0.005 265)
Dark
oklch(0.255 0.007 265)
Graphite Ink--foreground

All primary text, the tooltip fill, the base of every ink step.

Light
oklch(0.21 0.006 265)
Dark
oklch(0.955 0.003 265)
Slate Meta--muted-foreground

Meta text, column headers, inactive nav and tabs, resting icons.

Light
oklch(0.5 0.014 265)
Dark
oklch(0.7 0.012 265)
Faint Slate--subtle-foreground

Empty-cell dashes, column-header icons, the neutral status dot.

Light
oklch(0.62 0.012 265)
Dark
oklch(0.58 0.012 265)
Hairline--border

Structural dividers only: table rules, header bottoms, section splits.

Light
oklch(0.918 0.005 265)
Dark
oklch(1 0 0 / 0.075)
Firm Hairline--border-strong

Scrollbar thumbs, flow edges and handles, the dotted canvas grid.

Light
oklch(0.86 0.007 265)
Dark
oklch(1 0 0 / 0.13)
Field Stroke--input

Input, select and textarea strokes, the unchecked switch track.

Light
oklch(0.885 0.006 265)
Dark
oklch(1 0 0 / 0.12)

Primary#

Quiet Indigo and the two roles that travel with it. See where indigo is allowed.

Quiet Indigo--primary

The one filled button, selection, checked controls, labelled live state.

Light
oklch(0.52 0.19 272)
Dark
oklch(0.585 0.18 272)
Focus Indigo--ring

Every focus indicator. Matches Quiet Indigo in light and lifts in dark.

Light
oklch(0.52 0.19 272)
Dark
oklch(0.65 0.16 272)
Indigo Paper--primary-foreground

Text and icons on the indigo fill.

Light
oklch(0.99 0.004 272)
Dark
oklch(0.99 0.004 272)

Semantic#

Verdicts. Each one appears as a dot, an icon, a tint or a ring beside a text label, never as color alone.

Signal Red--destructive

Errors, failed runs, Stalled. Destructive buttons are a 10% tint, never a solid fill.

Light
oklch(0.56 0.2 25)
Dark
oklch(0.68 0.18 22)
Ledger Green--success

Passed runs, On track, Connected, a strong ICP fit.

Light
oklch(0.56 0.13 155)
Dark
oklch(0.7 0.14 155)
Caution Amber--warning

At risk, Invited, warnings.

Light
oklch(0.7 0.15 68)
Dark
oklch(0.8 0.14 75)
Amber Ink--warning-foreground

Text on Caution Amber.

Light
oklch(0.27 0.06 60)
Dark
oklch(0.22 0.05 70)
Note Blue--info

Informational notices and toasts.

Light
oklch(0.58 0.13 240)
Dark
oklch(0.7 0.12 240)

Ink steps#

Graphite Ink at fixed alphas, so they invert with the theme. They draw magnitudes. See ink steps in use.

Ink 80--ink-80

The first part of a stack, the current stage segment.

Light
oklch(0.21 0.006 265 / 0.8)
Dark
oklch(0.955 0.003 265 / 0.8)
Ink 65--ink-65

The default single fill: meters, progress, talk ratio.

Light
oklch(0.21 0.006 265 / 0.65)
Dark
oklch(0.955 0.003 265 / 0.65)
Ink 45--ink-45

The middle of a three-part stack.

Light
oklch(0.21 0.006 265 / 0.45)
Dark
oklch(0.955 0.003 265 / 0.45)
Ink 25--ink-25

Completed stage segments.

Light
oklch(0.21 0.006 265 / 0.25)
Dark
oklch(0.955 0.003 265 / 0.25)
Ink 15--ink-15

The last part of a three-part stack.

Light
oklch(0.21 0.006 265 / 0.15)
Dark
oklch(0.955 0.003 265 / 0.15)

Chart set#

For charts inside the chart container only, in this series order. Most charts lead with Chart Ink.

Chart Ink--chart-1

The first series. Most charts lead with it.

Light
oklch(0.4 0.02 265)
Dark
oklch(0.82 0.015 265)
Chart Teal--chart-2

The second series.

Light
oklch(0.66 0.11 190)
Dark
oklch(0.7 0.11 190)
Chart Amber--chart-3

The third series.

Light
oklch(0.76 0.14 75)
Dark
oklch(0.8 0.13 75)
Chart Magenta--chart-4

The fourth series.

Light
oklch(0.6 0.19 330)
Dark
oklch(0.68 0.17 330)
Chart Soft Ink--chart-5

The fifth series.

Light
oklch(0.72 0.05 265)
Dark
oklch(0.55 0.03 265)
Invoices by channelLight
  • Chart Ink
  • Chart Teal
  • Chart Amber
  • Chart Magenta
  • Chart Soft Ink
Invoices by channelDark
  • Chart Ink
  • Chart Teal
  • Chart Amber
  • Chart Magenta
  • Chart Soft Ink

Categorical tags#

Ten pale fills with deep same-hue ink, for select-option values and identity tints. Tag Indigo is a categorical hue, paler than Quiet Indigo and never a stand-in for it.

Net 30
Net 30
gray--tag-gray
Discovery
Discovery
blue--tag-blue
Evaluation
Evaluation
indigo--tag-indigo
Enterprise
Enterprise
violet--tag-violet
Freight
Freight
pink--tag-pink
Churned
Churned
red--tag-red
Net 60
Net 60
orange--tag-orange
Pilot
Pilot
amber--tag-amber
Customer
Customer
green--tag-green
Logistics
Logistics
teal--tag-teal

Contrast#

Ratios computed on every build from the OKLCH values in globals.css: OKLCH to OKLab to linear sRGB to relative luminance. Translucent layers are composited over what they sit on.

Canon checks against WCAG 2.2 AA: 4.5:1 for text, 3:1 for large text and for non-text UI such as icons, status dots, meter fills and focus rings. AAA is 7:1. Most of the system's text is 12 to 14px, so the large-text allowance rarely applies.

Text contrast
PairLightDarkWhere
Body textforeground on background17.72:1AAA16.55:1AAAReading text and table cells on the plane.
Card copyforeground on card17.72:1AAA15.71:1AAAText on cards and panels.
Popover copyforeground on popover17.72:1AAA15.00:1AAAMenus, popovers and dialogs.
Metamuted-foreground on background6.00:1AA7.06:1AAATimestamps, counts, column headers, helper text.
Meta in a wellmuted-foreground on muted/70 over card5.59:1AA6.38:1AALabels inside a tint well on a card.
Nav itemsidebar-foreground on sidebar11.04:1AAA9.72:1AAASidebar items at rest.
Active nav itemforeground on sidebar-accent over sidebar14.68:1AAA14.82:1AAAThe current page in the sidebar.
Selected rowforeground on primary/6 over background16.30:1AAA15.72:1AAAGrid rows and list items that are selected.
Secondary buttonsecondary-foreground on secondary13.92:1AAA13.02:1AAAQuiet Fill Ink on Quiet Fill.
Filled buttonprimary-foreground on primary5.66:1AA4.26:1AA large onlyIndigo Paper on Quiet Indigo.
Indigo textprimary on background5.82:1AA4.30:1AA large onlyLink buttons.
Destructive buttondestructive on destructive/10 over background (destructive/20 over background in dark)4.42:1AA large only4.64:1AAThe red tint button, 10% in light and 20% in dark.
Error textdestructive on background5.15:1AA6.03:1AAField errors and failed-run messages.
Green textsuccess on background4.37:1AA large only7.50:1AAALedger Green set as words.
Blue textinfo on background4.21:1AA large only7.18:1AAANote Blue set as words.
Amber Inkwarning-foreground on warning5.54:1AA9.15:1AAAText on a Caution Amber fill.
Tooltipbackground on foreground17.72:1AAA16.55:1AAAWhite Plane on Graphite Ink.
Non-text contrast
ElementLightDarkWhere
Faint Slatesubtle-foreground on background3.64:1Passes 3:14.40:1Passes 3:1Empty-cell dashes, column-header icons, the neutral dot. Not for words.
Focus ringring on background5.82:1Passes 3:15.66:1Passes 3:1The 2px outline and the indigo border on focus.
Meter fillink-65 on muted5.28:1Passes 3:16.89:1Passes 3:1Ink 65 against its Well Gray track.
Green dotsuccess on background4.37:1Passes 3:17.50:1Passes 3:1The success status dot, beside its label.
Amber dotwarning on background2.76:1Below 3:19.92:1Passes 3:1The warning status dot, beside its label.
Red dotdestructive on background5.15:1Passes 3:16.03:1Passes 3:1The danger status dot, beside its label.

Pairs that fall short

Indigo Paper on Quiet Indigo is 4.26:1 in dark, and indigo link text 4.30:1, both under 4.5:1 for 14px labels. Signal Red on its 10% tint is 4.42:1 in light. Ledger Green (4.37:1) and Note Blue (4.21:1) miss AA as text in light, and the amber dot is 2.76:1 on white. DESIGN.md sets no contrast target, so these are recorded here rather than fixed. Until the tokens move, set status words in Graphite Ink beside the colored dot, as Status label does, and don't set paragraphs in semantic colors.

Tag ink on tag fill#

Every option hue clears AA in both themes, which is why tags can carry 12px Label type.

Tag contrast
HueLightDarkSample value
Graytag-gray-fg on tag-gray7.42:1AAA8.06:1AAANet 30
Bluetag-blue-fg on tag-blue6.43:1AA7.61:1AAADiscovery
Indigotag-indigo-fg on tag-indigo6.36:1AA7.47:1AAAEvaluation
Violettag-violet-fg on tag-violet6.29:1AA7.74:1AAAEnterprise
Pinktag-pink-fg on tag-pink5.67:1AA7.94:1AAAFreight
Redtag-red-fg on tag-red5.63:1AA7.41:1AAAChurned
Orangetag-orange-fg on tag-orange5.52:1AA7.75:1AAANet 60
Ambertag-amber-fg on tag-amber5.98:1AA8.33:1AAAPilot
Greentag-green-fg on tag-green5.99:1AA7.74:1AAACustomer
Tealtag-teal-fg on tag-teal6.06:1AA7.68:1AAALogistics

Four rules for color#

They keep indigo legible, data neutral and status readable in grayscale. Each is drawn below the way review catches it.

The Quiet Indigo Rule

Indigo is spent on the view's one filled primary action, on selection, on focus, and on live state that carries a text label (running, active, upcoming), plus the viewer's own markers: the unread dot, an @mention of you, the unsaved-changes dot. It never fills data, tints an icon tile, colors a heading or decorates.

The Ink Fill Rule

Magnitudes are drawn in ink steps (80, 65, 45, 25, 15) on a Well Gray track: meters, progress, the stage track, ICP bars and the forecast bar. A fill turns semantic only when the value is itself a verdict, such as a strong ICP fit in Ledger Green. It is never indigo.

The Option Hue Rule

The ten categorical hues belong to select-option values (stage, lifecycle, tier, list and attribute options) and to identity tints on avatars, monogram tiles and the four app marks. An app hue appears in two places: the glyph tile, and the 3px pill beside the active sidebar item. Categorical hues never color-code sections, navigation text, status or charts.

The Label-Beside-Color Rule

Status is never color alone. A dot, tint or ring always travels with a text label (On track, At risk, Running, Passed), so the state reads in grayscale.

Indigo is for the decision#

Headings, icon tiles and decoration stay in ink. Indigo goes to the action, the selection and focus.

W-9 requests
Do. Set the heading in Graphite Ink and the icon tile in Well Gray, and save indigo for the one filled button.
W-9 requests
Don't. Tint the icon tile and color the heading indigo. The eye lands on decoration instead of the action.

Magnitudes in ink#

Talk ratio, Nora62%
Do. Fill a meter in Ink 65 on Well Gray. A fill turns semantic only when the value is itself a verdict.
Talk ratio, Nora62%
Don't. Fill it indigo. It reads as selected or clickable, and it competes with the one filled button.

Hues belong to option values#

Northwind FreightEvaluationEnterprise
Orchard StreetDiscoveryMid-market
Do. Give select-option values their hue: the stage and the tier on a record.
SuppliersInvoicesInboxCalls
Don't. Color-code navigation or sections with tag hues. The hues stop meaning anything, and the page turns into a legend.

A label beside every color#

On trackAt riskStalled
Do. Pair each dot with its word, so the state reads in grayscale and to a screen reader.
HalcyonNorthwind FreightOrchard Street
Don't. Show health as dots alone. Red and green are the pair most people with color blindness confuse.

Where indigo is allowed#

A closed list. If a use isn't on it, the answer is ink, a neutral fill or a tag.

Every indigo in a view

A running payment run with two invoices selected. Five uses of indigo, each on the list below. Uncheck rows or remove the filter and watch the count fall.

Number every indigo
Payment run, FridayRunning
2 unread
DueisThis week
  • Northwind Freightinv_8F3K2QW7$18,240.00
  • Halcyoninv_2J9D4MX1$12,940.50
  • Orchard Streetinv_7Q1L8RZ3$4,310.00
2 of 3 selected
import { Button } from "@oration/canon/components/button";import { Checkbox } from "@oration/canon/components/checkbox";import { FilterChip } from "@oration/canon/components/filter-chip";import { StatusLabel } from "@oration/canon/components/status-dot";import { Switch } from "@oration/canon/components/switch";import { toast } from "@oration/canon/components/toast";import { cn } from "@oration/canon/lib/utils";import { CalendarIcon } from "lucide-react";import * as React from "react";import { AnatomyMarker } from "@/components/design/kit/lists";export function IndigoMap() {    const invoices = [        {            id: "inv_8F3K2QW7",            supplier: "Northwind Freight",            amount: "$18,240.00",        },        { id: "inv_2J9D4MX1", supplier: "Halcyon", amount: "$12,940.50" },        { id: "inv_7Q1L8RZ3", supplier: "Orchard Street", amount: "$4,310.00" },    ];    const [selected, setSelected] = React.useState<Set<string>>(        () => new Set(["inv_8F3K2QW7", "inv_2J9D4MX1"]),    );    const [mark, setMark] = React.useState(true);    const [filtered, setFiltered] = React.useState(true);    const toggle = (id: string) =>        setSelected((current) => {            const next = new Set(current);            if (next.has(id)) next.delete(id);            else next.add(id);            return next;        });    const marker = (n: number, className: string) =>        mark ? (            <AnatomyMarker n={n} className={cn("absolute z-10", className)} />        ) : null;    return (        <div className="flex w-full max-w-lg flex-col gap-3">            <Switch                label="Number every indigo"                checked={mark}                onCheckedChange={setMark}            />            <div className="flex flex-col overflow-hidden rounded-xl bg-card shadow-border">                <div className="flex h-11 items-center justify-between gap-2 border-b border-border px-3">                    <div className="relative flex items-center gap-2">                        <span className="text-13 font-medium text-foreground">                            Payment run, Friday                        </span>                        <StatusLabel tone="primary" className="text-[13px]">                            Running                        </StatusLabel>                        {marker(1, "-top-3 right-0")}                    </div>                    <span className="relative flex items-center gap-1.5 text-xs text-muted-foreground">                        <span                            className="size-1.5 rounded-full bg-primary"                            aria-hidden="true"                        />                        2 unread                        {marker(2, "-top-3 -left-3")}                    </span>                </div>                <div className="relative flex min-h-11 items-center px-3 py-2">                    {filtered ? (                        <FilterChip                            icon={                                <CalendarIcon                                    aria-hidden="true"                                    className="size-3.5 text-muted-foreground"                                />                            }                            field="Due"                            valuesLabel="This week"                            multiple={false}                            onRemove={() => setFiltered(false)}                        />                    ) : (                        <Button                            type="button"                            variant="ghost"                            size="sm"                            onClick={() => setFiltered(true)}                        >                            Add filter                        </Button>                    )}                    {filtered ? marker(3, "top-0 left-1.5") : null}                </div>                <ul className="border-t border-border">                    {invoices.map((invoice, index) => {                        const isSelected = selected.has(invoice.id);                        return (                            <li                                key={invoice.id}                                className={cn(                                    "relative flex h-9 items-center gap-3 border-b border-border px-3 text-13 last:border-b-0",                                    isSelected                                        ? "bg-primary/6"                                        : "hover:bg-surface",                                )}                            >                                <Checkbox                                    checked={isSelected}                                    onCheckedChange={() => toggle(invoice.id)}                                    aria-label={`Select ${invoice.supplier}`}                                />                                <span className="min-w-0 flex-1 truncate text-foreground">                                    {invoice.supplier}                                </span>                                <span className="font-mono text-xs text-muted-foreground">                                    {invoice.id}                                </span>                                <span className="w-24 text-right font-medium text-foreground tabular-nums">                                    {invoice.amount}                                </span>                                {index === 0 && isSelected                                    ? marker(4, "top-0.5 -left-2.5")                                    : null}                            </li>                        );                    })}                </ul>                <div className="relative flex items-center justify-between gap-2 border-t border-border px-3 py-2.5">                    <span className="text-xs text-muted-foreground tabular-nums">                        {selected.size} of {invoices.length} selected                    </span>                    <div className="flex gap-2">                        <Button type="button" variant="outline" size="sm">                            Export CSV                        </Button>                        <Button                            type="button"                            size="sm"                            disabled={selected.size === 0}                            onClick={() =>                                toast.add({                                    title: `${selected.size} invoices approved`,                                    description:                                        "They join Friday's payment run on Oct 2.",                                })                            }                        >                            Approve {selected.size}                        </Button>                    </div>                    {marker(5, "-top-2.5 right-1")}                </div>            </div>        </div>    );}
Where indigo is allowed
UseTreatmentSee
The one filled buttonQuiet Indigo fill and Indigo Paper text. Hover mixes in 9% black, 8% white in dark.Button
Checked controlsCheckbox, radio and switch fills when on.Checkbox, Switch
SelectionA 6% tint on selected rows; a 6% tint and a 22% inset ring on active filter chips; a 1.5 to 2px ring on a selected choice card.Data grid, Filter chip, Choice card
FocusA 3px ring at 40 to 50% with an indigo border on controls; a 2px outline at a 2px offset on bare links.Accessibility
Text selection and the caretSelection at 22%, and the caret in every input and textarea.Styles
Workflow edgesSelected and running edges. A running edge is a 1.75px dashed line that marches.Flow
Live state with a labelA running node or run, an active sequence, an upcoming meeting, with its word beside it.Status label
The viewer's own markersThe unread dot, an @mention of you, the unsaved-changes dot.Save bar
Link buttonsIndigo text that underlines on hover.Button
  • Never on data: meters, bars, chart series, sparklines and stage tracks are ink.
  • Never on icon tiles, headings, section labels, illustrations or borders for decoration.
  • Never two filled indigo buttons in one view. See The One Filled Button Rule on Button.
  • Tag Indigo (--tag-indigo) is an option hue. Don't use it to fake a softer primary.

Ink steps in use#

Magnitudes are drawn in the same ink as the text, at five fixed alphas, on a Well Gray track. On most screens that leaves indigo as the only saturated color.

Which step to use
StepUtilityUse
Ink 80bg-ink-80The first part of a stack (closed won), the current stage segment.
Ink 65bg-ink-65The default single fill: meters, progress, talk ratio, the credits meter.
Ink 45bg-ink-45The middle of a three-part stack (commit).
Ink 25bg-ink-25Completed stage segments.
Ink 15bg-ink-15The last part of a three-part stack (best case).

A three-part stack

Closed won in Ink 80, commit in Ink 45 and best case in Ink 15, against a target marker. Switch quarters and the segments animate over 500ms.

ForecastMaya Okafor, VP of Revenue
  • Closed won$0.64M
  • Commit$0.58M
  • Best case$0.39M
  • Target$2.00M
import { Meter } from "@oration/canon/components/meter";import { SegmentedControl } from "@oration/canon/components/segmented-control";import { cn } from "@oration/canon/lib/utils";import * as React from "react";export function ForecastStack() {    const [quarter, setQuarter] = React.useState<keyof typeof quarters>("q4");    const data = quarters[quarter];    const max = 2.4;    const parts = [        { label: "Closed won", value: data.won, className: "bg-ink-80" },        { label: "Commit", value: data.commit, className: "bg-ink-45" },        { label: "Best case", value: data.best, className: "bg-ink-15" },    ];    const money = (n: number) => `$${n.toFixed(2)}M`;    return (        <div className="flex w-full max-w-md flex-col gap-4 rounded-xl bg-card p-4 shadow-border">            <div className="flex items-center justify-between gap-3">                <div className="flex flex-col">                    <span className="text-sm font-semibold text-foreground">                        Forecast                    </span>                    <span className="text-xs text-muted-foreground">                        Maya Okafor, VP of Revenue                    </span>                </div>                <SegmentedControl<keyof typeof quarters>                    label="Quarter"                    value={quarter}                    onValueChange={setQuarter}                    options={[                        { value: "q3", label: "Q3" },                        { value: "q4", label: "Q4" },                    ]}                />            </div>            <Meter                label={`Forecast against a ${money(data.target)} target`}                max={max}                target={data.target}                size="lg"                segments={parts}            />            <ul className="flex flex-col gap-1.5">                {parts.map((part) => (                    <li                        key={part.label}                        className="flex items-center gap-2 text-13"                    >                        <span                            aria-hidden="true"                            className={cn(                                "size-2 shrink-0 rounded-[2px]",                                part.className,                            )}                        />                        <span className="flex-1 text-foreground">                            {part.label}                        </span>                        <span className="font-medium text-foreground tabular-nums">                            {money(part.value)}                        </span>                    </li>                ))}                <li className="flex items-center gap-2 border-t border-border pt-1.5 text-13">                    <span                        aria-hidden="true"                        className="h-2.5 w-0.5 shrink-0 rounded-full bg-foreground"                    />                    <span className="flex-1 text-muted-foreground">Target</span>                    <span className="text-muted-foreground tabular-nums">                        {money(data.target)}                    </span>                </li>            </ul>        </div>    );}

Stage track and a single fill

Completed stages in Ink 25 with a check, the current one in Ink 80, upcoming ones in Well Gray. The credits meter is one Ink 65 fill.

Halcyon renewalProposal
Enrichment credits6,200 of 10,000
Resets Oct 1
import { Button } from "@oration/canon/components/button";import { Meter } from "@oration/canon/components/meter";import { cn } from "@oration/canon/lib/utils";import { CheckIcon } from "lucide-react";import * as React from "react";export function StageTrack() {    const [current, setCurrent] = React.useState(2);    const [credits, setCredits] = React.useState(6200);    return (        <div className="flex w-full max-w-md flex-col gap-5 rounded-xl bg-card p-4 shadow-border">            <div className="flex flex-col gap-2">                <div className="flex items-baseline justify-between gap-3">                    <span className="text-sm font-semibold text-foreground">                        Halcyon renewal                    </span>                    <span className="text-xs text-muted-foreground">                        {stages[current]}                    </span>                </div>                <div className="flex gap-1">                    {stages.map((stage, index) => (                        <button                            key={stage}                            type="button"                            aria-label={`Move to ${stage}`}                            aria-pressed={index === current}                            onClick={() => setCurrent(index)}                            className="group flex min-w-0 flex-1 flex-col gap-1.5 rounded-sm text-left outline-none"                        >                            <span                                className={cn(                                    "h-1 w-full rounded-full transition-colors duration-150 ease-out",                                    index < current && "bg-ink-25",                                    index === current && "bg-ink-80",                                    index > current &&                                        "bg-muted group-hover:bg-ink-15",                                )}                            />                            <span                                className={cn(                                    "items-center gap-1 truncate text-xs sm:flex",                                    index === current                                        ? "flex font-medium text-foreground"                                        : "hidden text-muted-foreground",                                )}                            >                                {index < current ? (                                    <CheckIcon                                        aria-hidden="true"                                        className="size-3 shrink-0"                                    />                                ) : null}                                <span className="truncate">{stage}</span>                            </span>                        </button>                    ))}                </div>            </div>            <div className="flex flex-col gap-2">                <div className="flex items-baseline justify-between gap-3 text-13">                    <span className="text-foreground">Enrichment credits</span>                    <span className="text-muted-foreground tabular-nums">                        {credits.toLocaleString("en-US")} of 10,000                    </span>                </div>                <Meter                    label="Enrichment credits used"                    value={credits}                    max={10000}                />                <div className="flex items-center justify-between">                    <span className="text-2xs text-muted-foreground">                        Resets Oct 1                    </span>                    <Button                        type="button"                        variant="outline"                        size="sm"                        onClick={() =>                            setCredits((value) => Math.min(10000, value + 900))                        }                    >                        Enrich 900 rows                    </Button>                </div>            </div>        </div>    );}

Legends pair an 8px square swatch with a 13px label and a tabular value. The Meter component draws the track, the fills and the target; pass ink utilities as segment classes.

App marks#

Each app in the suite has an identity hue, shown as the short bar beside the active sidebar item and on its app tile. They are identity tints under The Option Hue Rule, never indigo.

The active mark in both themes

Pick an app. The 3px mark takes that app's hue, with a soft glow at 55%.

Light
Dark
import { cn } from "@oration/canon/lib/utils";import * as React from "react";export function AppMarks() {    const [active, setActive] =        React.useState<(typeof apps)[number]["id"]>("crm");    return (        <div className="grid w-full max-w-xl gap-3 sm:grid-cols-2">            {(["light", "dark"] as const).map((mode) => (                <div                    key={mode}                    className={cn(                        mode,                        "flex flex-col gap-0.5 rounded-xl bg-sidebar p-2 shadow-border",                    )}                >                    <span className="px-2 pt-1 pb-1.5 text-xs font-medium text-muted-foreground">                        {mode === "light" ? "Light" : "Dark"}                    </span>                    {apps.map((app) => {                        const isActive = app.id === active;                        const Icon = app.icon;                        return (                            <button                                key={app.id}                                type="button"                                aria-pressed={isActive}                                onClick={() => setActive(app.id)}                                className={cn(                                    "relative flex h-7 items-center gap-2.5 rounded-md px-2 text-left text-13 text-sidebar-foreground outline-none transition-colors duration-150 ease-out hover:bg-sidebar-accent focus-visible:ring-3 focus-visible:ring-ring/40",                                    isActive &&                                        "bg-sidebar-accent font-medium text-foreground",                                )}                            >                                {isActive ? (                                    <span                                        aria-hidden="true"                                        className="absolute top-1.5 -left-2 h-4 w-[3px] rounded-full"                                        style={{                                            background: app.mark,                                            boxShadow: `0 0 8px -1px color-mix(in oklch, ${app.mark} 55%, transparent)`,                                        }}                                    />                                ) : null}                                <Icon                                    aria-hidden="true"                                    className={cn(                                        "size-4",                                        isActive                                            ? "text-foreground"                                            : "text-muted-foreground",                                    )}                                />                                {app.label}                            </button>                        );                    })}                </div>            ))}        </div>    );}
App mark tokens
TokenAppLightDark
--app-crmCRMoklch(0.62 0.16 250)var(--tag-blue-fg)
--app-agentsAgents Platformoklch(0.6 0.18 300)var(--tag-violet-fg)
--app-contactContact Centeroklch(0.62 0.15 155)var(--tag-green-fg)
--app-ticketsTicketingoklch(0.72 0.15 75)var(--tag-amber-fg)

In light the marks are lighter than the tag inks, which are tuned for text on pale chips. In dark they alias the matching tag ink directly. App tiles pair the tag fill with the tag ink. Read the marks with bg-(--app-crm) or an inline var(); they have no Tailwind key.

The mark's glow is the only colored shadow in the system. Spend each hue only where DESIGN.md allows: the app tile and the active sidebar item.