aiRA Design System First Edition

The First Edition is organised around atomic design — Atoms → Molecules → Organisms → Templates — so every element maps cleanly onto a Figma library. The product has one assistant voice (aiRA), one set of avatars, and custom-rendered form controls throughout.

Open this file in a browser. No build step. Toggle Light/Dark and LTR/RTL via the top bar.

About this file

This single HTML file contains every part of the aiRA Design System First Edition — markup, tokens, behaviour, README, changelog, the Figma bundle (tokens.json in DTCG 2025.10, component map, Code Connect), and the system-wide Component status table. Open it in any browser; no build step, no external assets.

README

aiRA Design System · First Edition

A standalone, browser-openable design system for the aiRA conversational product. Atomic IA, one assistant voice, no agent-identity fiction, and custom form controls throughout.

Open it

open aira-design-system/index.html

No build step. Toggle Light/Dark and LTR/RTL from the top nav.

Files

aira-design-system/

index.html # full design system page tokens.css # all design tokens (light + dark) CHANGELOG.md # First Edition release notes README.md # this file assets/ # logo + fonts figma/ # tokens.json, figma-components.md, code-connect.json

Atomic IA

Atoms       Button · Icon button · Input · Textarea · Chip · Tag · Badge · Pill

Avatar · Citation chip · Tooltip · Kbd · Divider · Spinner · Skeleton

Molecules Field · Dropdown (single · multi · searchable × sm · md · lg) Checkbox · Radio (incl. disabled) · Toggle · Stepper · Progress Feedback control · Action row · Tabs · Breadcrumb · Popover Toast · Banner · Nudge card · Selection card

Organisms Chat message · Composer · Approval card (with Type-to-confirm) Code block · Inline diff · Agent activity (animated single-step + todo-style multi-step) · Conversation state machine · Modal · Sheet List row · Data table (column filters) · Card · Form · Change-log feed

Templates Empty · Loading · Errors · My Configs · Mobile · Email × 4

Cross-cutting rules

1. One aiRA. No named agents. No agent-config / qa / analytics / support / coder tokens, classes, badges, or avatars anywhere. 2. Custom-rendered form controls. Never the native <select>. 3. Stop, not disable. Agents are stopped via the composer Stop icon; they're never disabled.

Source of truth

Until the React component library lands:

  • Markup — index.html. Copy demo HTML verbatim into your product; replace inline values with your data binding.
  • Tokens — tokens.css. Import once at the app root. Never redefine a token locally.
  • Behaviour — the data-ds-* hooks at the bottom of index.html (Tabs, Dropdown, sidebar search, Type-to-confirm).

Color

Two-tier system: raw primitives at the base, semantic tokens on top. Product code references semantic tokens only.

Slate · neutrals

slate-25#FBFCFD
slate-50#F6F8FA
slate-100#EEF1F4
slate-200#E2E6EB
slate-300#C9D0D9
slate-400#9AA4B2
slate-500#6B7785
slate-600#4D5867
slate-700#364152
slate-800#1F2937
slate-900#111827
slate-950#0A0F1A

Indigo · primary brand

indigo-600 (#3438C9) is the primary action colour — the fill used on every primary button across the system. Flagged below.

indigo-50#EEF0FF
indigo-100#DDE0FF
indigo-200#BAC1FF
indigo-300#8E97FF
indigo-400#6268FF
indigo-500#4A50EF
indigo-600#3438C9Primary button
indigo-700#252AA0
indigo-800#1B2078
indigo-900#121757

Sky · gradient end-stop

sky-50#E6F1FF
sky-100#C7E1FF
sky-200#8EC1FF
sky-300#5EA8FF
sky-400#3BA7FF
sky-500#1564CC

Status accents

green-500#1F7A42
amber-400#B97300
red-400#D92D2D
info-400#1F6AAA
violet-500#6E40D9

aiRA gradient

indigo-400 → sky-400. The single gradient used for the aiRA mark, send button, avatars, and any aiRA-anchored surface.

Semantic chips

Default Brand Success Warning Danger Info

Do / don't

Do

Reference semantic tokens (--text-primary, --bg-surface) in product code.

Don't

Reference primitives (--slate-700) directly. They change between modes.

Color fidelity

Colors are authored in sRGB. Reference rendering is on a display calibrated to sRGB IEC61966-2.1, white point D65, gamma 2.2, at roughly 120 cd/m².

Why colors vary across machines

On uncalibrated panels — most Windows laptops, many external monitors — variation is expected in both light and dark mode. In light mode, panel white points drift cooler than D65, saturated mid-tones lose punch on sRGB-only panels, and hairline borders either vanish or harden. In dark mode, backlight bleed lifts near-black values into muddy grey-blue on IPS LCD while OLED renders them as nearly-off pixels. Four factors stack: display gamut (Mac displays cover P3; most Windows panels cover only sRGB), gamma curves, backlight characteristics, and browser color management (Safari respects the system ICC profile; Chrome and Edge on Windows are inconsistent).

What the system does

  • Brand gradients use OKLCH interpolation so the midpoint stays vibrant across display gamuts.
  • The dark canvas sits at #0E1424 — lifted slightly from true black so panels with backlight bleed render it as clearly dark rather than muddy grey-blue.
  • Brand surfaces opt out of Windows forced-color overrides so the indigo primary stays indigo in High Contrast Mode.
  • Light-mode rendering can't be hardware-corrected via CSS. Uncalibrated Windows panels with cool white points will render light surfaces with a faint blue cast that no code change resolves.

What product teams can do

  • Review on a calibrated display when possible. A software calibration via OS settings gets most of the way.
  • Spot-check on the cheapest Windows laptop common in your deployment — that's the floor.
  • Trust the tokens. Don't tweak hex values to compensate for what you see on your own machine.

What can't be fixed in code

Ambient lighting, blue-light filters, panel hardware, OS calibration, and browser color-management quirks are outside the system's reach. Aim for consistency within the variance envelope each panel allows.

Typography

Roboto, Regular (400) + Medium (500) + Semibold (600). One ramp, used consistently.

Display lg · 40/48
Display md · 32/40
Heading 1 · 28/36
Heading 2 · 22/30
Heading 3 · 18/26
Heading 4 · 16/24
Body lg · 16/26 — long-form prose, marketing.
Body md · 14/22 — the default body size used everywhere.
Body sm · 13/20 — helper, captions, metadata.
Label lg · 15/22
Label md · 14/20
Label sm · 12/16
Overline · 11/16

Spacing

4 px base grid. Use the scale; no 10 px, 13 px, 22 px.

sp-14px
sp-28px
sp-312px
sp-416px
sp-520px
sp-624px
sp-832px
sp-1248px

Radius & elevation

Radii

Elevation

Motion

Three durations. prefers-reduced-motion halts animation — does not speed it up.

TokenValueUse
--motion-fast120ms · ease-outHover, focus, micro-toggles.
--motion-base200ms · ease-outDefault for most state changes.
--motion-slow320ms · ease-outLarge surfaces (sheets, modals).
--motion-reduced0.001msDetectable by JS when reduced-motion is on.

Iconography

Every icon used in the system lives here. 20 px nominal · stroke 1.5 · slate-600 default. Inherit colour from currentColor; never bake a colour into the SVG.

plusAdd
micVoice input
sendSubmit / send
stopHalt generation · circle + square
copyCopy to clipboard
regenerateRe-run prompt
thumbs-upHelpful feedback
thumbs-downNot helpful — opens reason picker
chevron-upDisclose / expand caret
chevron-downDropdown caret · collapse
chevron-leftBack / previous
chevron-rightForward / next
pencilEdit / "Something else"
closeDismiss / X
checkConfirm / done
searchFind
chevrons-up-downSortable column · unsorted
sort-ascSort ascending
sort-descSort descending
more-horizontalRow menu
more-verticalToolbar menu
infoInfo badge
warningCaution
errorFailure
successConfirmed · circle + check
loaderAsync work
sparkleaiRA mark
downloadSave out
external-linkOpen elsewhere
linkCitation chip
eyeShow password / view
eye-offHide password
lockLocked / RBAC
unlockUnlocked
trashDelete
archiveArchive
pausePause a live promo
playResume / start
life-buoySupport · action row
uploadUpload file
serverFTP push · export marker
workbookWorkbook download · export marker
file-codeCSS download · export marker
paperclipAttach file
lightbulbInfo tip · how-to note

Interactive states

Every icon, when wrapped in an Icon button (or any interactive target), supports four states. The matrix below shows the contract on a representative icon — the same treatment applies to every icon in the grid above.

StatePlusSendStopTrash
Default
Hover
Pressed
Disabled

Default · hover · pressed · disabled apply to every interactive icon. Disabled is reserved for "this action is not currently possible". For aiRA generation, use the Stop icon instead of disabling Send.

Do / don't

Do

Render icons inline as SVG with stroke set to the current text colour, so the theme tints them.

Don't

Embed PNGs or bake hex colours into icons. Breaks dark mode.

Do

Use the Stop icon (circle with a square inside) only when interrupting an in-flight aiRA generation.

Don't

Disable an agent. Agents stop generating; they're never disabled.

Data viz

Five guidelines, the right encoding for every data relationship, and a gallery of every chart format aiRA produces. Based on Cleveland & McGill's perceptual rankings and Schwabish's Better Data Visualizations.

Five guidelines

  1. Show the data. Strip ornament. If the reader can't see the pattern you're communicating, simplify until they can.
  2. Reduce the clutter. No 3D, no gradients on chart marks, no heavy gridlines. Every non-data element earns its place.
  3. Integrate graphics and text. The title is the takeaway, not the topic. Annotate moments directly on the chart.
  4. Avoid the spaghetti chart. Past four-to-six series, highlight-and-gray or break into small multiples.
  5. Start with gray. Add colour only where it carries meaning. Forces intentional choices and creates visual hierarchy.

Perceptual ranking

Encodings ranked by how accurately the eye reads them (Cleveland & McGill). Use higher-ranked encodings when precision matters.

RankEncodingUsed in
1Position on a common scaleBar · Dot plot · Line
2Position on non-aligned scalesSmall multiples
3Length without shared baselineStacked bar segments
4Direction · slopeSlope chart
5AnglePie chart (use sparingly)
6AreaTreemap · Bubble
7Volume3D — avoid
8Colour saturation · hueHeatmap · Choropleth

Choosing the chart — by data relationship

You're showingGo-to chart types
Comparison across categoriesBar · Dot plot · Heatmap · Waffle
Change over timeLine · Area · Slope · Sparkline
DistributionHistogram · Box plot
Relationship between variablesScatterplot
Part-to-wholeStacked bar · Treemap · Waffle
Precise value lookupData table (see below)

Bar chart · vertical

The workhorse. Length on a common baseline reads accurately. Always start the value axis at zero. Sort by value unless a natural order exists.

HSR Layout leads on monthly sales, by 28%
Loyalty engine · sales in lakhs · May 2026
15 8 0 HSR Ind Kor Whi Jay MGR

Bar chart · horizontal

Use horizontal when labels are long. Sort descending by default.

Top redeeming channels this quarter
Share of total redemptions
WhatsApp Push Email In-app SMS Web 34% 28% 22% 16% 11% 7%

Paired bar chart

Two values per category, side by side. Limit to two groups; beyond that, switch to a slope chart or dot plot.

This quarter vs. last · top stores
Q2 2026    Q1 2026
HSR Ind Kor Whi

Stacked bar chart

Total + composition. Only the bottom segment shares a baseline — put the most important there. Limit to three to five segments.

Redemptions by channel · last 30 days
WhatsApp 34% Push 28% Email 22% In-app SMS

Diverging bar chart

Bars extend in two directions from a central baseline. Use for data with a natural midpoint — satisfaction, sentiment, percent change.

Δ vs. prev 30 days · top stores
+8.2% HSR +2.1% Ind −4.6% Kor −11.8% Whi +4.6% Jay

Dot plot · dumbbell

Two dots per row, connected by a line. Shows the gap between two values across many categories — clearer than paired bars when the values are similar.

Open rate · email vs. push, by tier
0% 50% 100% Platinum Gold Silver Base Email Push

Heatmap

Grid of cells coloured by value. Two categorical axes, one quantitative measure. Sequential palette for a single measure; diverging when there's a midpoint.

Redemption rate by day & hour
9a 12p 3p 6p 9p 12a 3a 6a Mon Tue Wed Thu Fri Sat Sun

Waffle chart

10×10 grid of squares. Each square is one percent. More tangible than a pie for a single statistic — "23 of every 100 members".

38% redemption rate · 38 of every 100 active members

Line chart

The default for time series. Limit to four-to-six lines; beyond that, highlight-and-gray or small multiples.

Active members grew 14% over six months
14k 12k 10k Dec Jan Feb Mar Apr May

Line chart · highlight-and-gray

When showing many series, color only the lines that carry the story. Everything else stays neutral.

HSR Layout pulled away from the pack in April
HSR Dec May

Area chart

Line chart with filled area below. Emphasises magnitude. Don't stack more than four-to-five series.

Redemptions, last six months
Dec May

Slope chart

Two vertical axes connected by lines. Shows direction and magnitude of change across many categories at once.

Tier movement, Q1 → Q2
Q1 Q2 Platinum 18% 22% Gold 34% 38% Silver 28% 25% Base 20% 15%

Sparkline

Tiny inline chart — no axes, no labels. Used in tables and KPI tiles to add trend context to a single number.

Active members
12,481
Redemption rate
38%
Tier upgrades
219

Histogram

Bars showing frequency per bin. Bars touch — gaps are reserved for categorical bar charts.

Days since last purchase · active members
0 30 60 90+ days

Box-and-whisker plot

Median, IQR, and outliers in compact form. Requires statistical literacy — annotate parts for general audiences.

Basket size distribution by tier (₹)
0 5000 10000 Platinum Gold Silver

Scatterplot

Dots on two continuous axes — the go-to for showing correlation. Use transparency when dots overlap; label outliers directly.

Recency vs. basket size
Each dot is a member · sample of 80
0 5k 10k days since last purchase → linear fit

Treemap

Nested rectangles, area encodes value. For hierarchical data. Area is imprecise — use for broad patterns, not lookup.

Spend by category
Apparel 32% Beauty 18% Home 12% Tech 8% Grocery 14% Pharma 8% Other 8%

Pie chart · use sparingly

Only for two-to-three slices when the approximate proportion is the point. Never 3D, never exploded. With more slices, switch to a stacked bar or waffle.

Approved vs. rejected proposals
  • Approved · 72%
  • Rejected · 28%

Donut chart · use sparingly

Same constraints as a pie — two to three segments, single insight. The hole in the centre is a natural place to surface the headline number. Preferred over a pie when the dataset has a clear total worth calling out.

Redemption mix · last 30 days
42.1K redemptions
  • Points · 54%
  • Voucher · 28%
  • Tier-upgrade · 18%

Categorical · 6 series

Unordered groups. Distinct, equally vivid hues. Limit to six.

Channel · locked taxonomy

WhatsApp is always green; Push is always indigo. Users learn the taxonomy once.

WhatsApp
Push
Email
In-app
SMS
Web

Sequential · single hue

For ordered data (low → high). Single hue varying in lightness.

Diverging · with midpoint

For data with a natural neutral midpoint — sentiment, percent change. Two hues diverging from neutral.

Delta semantics

Green for positive change · orange for negative change. Red is reserved for errors and destructive actions — never the default for "down" deltas in a chart.

+8.2%
vs. prev 30d
−4.6%
vs. prev 30d

Data table · with sparklines

Tables are visualisations too. Right-align numbers, light horizontal rules only where needed, sparklines for trend context.

Store performance · 6-month trend
Loyalty engine · May 2026
StoreSalesΔ vs prev6-mo trend
HSR Layout₹14,21,540+8.2%
Indiranagar₹11,03,210+2.1%
Koramangala₹9,84,720−4.6%
Whitefield₹7,21,310−11.8%

Table design — ten rules

  1. Remove or minimise borders and fills — let whitespace organise.
  2. Light, thin horizontal rules only where needed.
  3. Remove vertical rules entirely.
  4. Right-align numbers, left-align text.
  5. Consistent precision within a column.
  6. Guide the eye with alignment, not lines.
  7. Highlight key values with weight, colour, or background.
  8. Add sparklines or inline bars for trend context.
  9. Limit columns — past fifteen, split or chart.
  10. The title states the finding, not "Table 1".

Numeric formatting

  • Use tabular numerals so digits line up vertically in tables.
  • Match precision to what the reader needs — "₹14.2L" beats "₹1,421,540" in a dashboard.
  • Symbols on the value, not the axis — "₹" prefix on each tile, not on every gridline.
  • Round consistently. Don't show 8.234% next to 12% in the same column.

Active titles

The title is the takeaway. "Active members grew 14% over six months" beats "Active members over time".

Accessibility

  • Never rely on colour alone — pair with shape, pattern, label, or direct annotation.
  • About 8% of men have a colour vision deficiency. Avoid red-green as the only distinction.
  • Charts must remain readable in grayscale.
  • Provide an accessible name on every chart SVG.

Implementation checklist

  • Title states the insight, not just the topic.
  • Non-data elements (gridlines, borders, fills) are minimal.
  • Colour is intentional, not decorative.
  • Chart type matches the data relationship.
  • Bar charts start at zero.
  • No 3D, no gradient fills on chart marks.
  • Series labelled directly where practical — not just a legend.
  • Chart works in grayscale.
  • Key findings called out by annotation.
  • Data source is credited in a subtitle or footnote.

Do / don't

Do

Start every bar chart's value axis at zero.

Don't

Truncate a bar axis to amplify the difference — it misleads.

Do

Use orange for negative deltas. Red is reserved for errors.

Don't

Use a pie chart with more than three slices.

Do

Highlight-and-gray when there are more than four to six series.

Don't

Use 3D charts or gradient fills on chart marks.

Approval status tokens

Six lifecycle states with paired surfaces.

Draft Awaiting approval Upcoming Approved Live Rejected
  • Draft — not yet submitted.
  • Awaiting approval — submitted, with a reviewer.
  • Upcoming — approved and scheduled, not yet live.
  • Approved — signed off, no schedule required.
  • Live — currently active in production.
  • Rejected — declined; reason persisted on the card.

Code & syntax tokens

Single-tone palette; no carnival-coloured highlighting. Tokens used in Code block.

TokenUsed for
--syntax-keywordSELECT, function, const
--syntax-string'literal'
--syntax-number42, 3.14
--syntax-comment// remark
--syntax-operator=, +, AND
--syntax-functionCOUNT(), SUM()
--syntax-punctuation(), ;, ,

Safe area

Honour the device notch and home indicator on mobile surfaces (sheets, app bars).

Safe area content lives here
--safe-top --safe-bottom
Portrait — notch top, home indicator bottom
Safe area content lives here
--safe-left --safe-right
Landscape (iOS) — insets shift to the orientation edges
Safe area content lives here
status bar gesture nav
Android — status bar top, gesture nav bottom

The hatched red zones map to env(safe-area-inset-*) on iOS and to WindowInsets (system bars + display cutouts) on Android. On the web, the same --safe-* tokens cover both platforms — the browser resolves them per OS. Apply the tokens as padding on any container that touches the viewport edge — sheets, app bars, full-screen modals — so content clears the notch, punch-hole, home indicator, and gesture-nav pill on every orientation.

Tokens

TokenSource (web)iOS maps toAndroid maps to
--safe-topenv(safe-area-inset-top, 0px)Notch / Dynamic IslandStatus bar inset (incl. punch-hole)
--safe-bottomenv(safe-area-inset-bottom, 0px)Home indicatorGesture nav pill (or 3-button nav)
--safe-leftenv(safe-area-inset-left, 0px)Landscape notch sideDisplay cutout (landscape)
--safe-rightenv(safe-area-inset-right, 0px)Landscape home-indicator sideGesture nav (landscape)

On Android WebView (Chrome 88+), env(safe-area-inset-*) resolves once the app sets WindowCompat.setDecorFitsSystemWindows(window, false) and the manifest declares android:windowLayoutInDisplayCutoutMode="shortEdges". The CSS contract is identical to iOS — write tokens, not platform branches.

Do / don't

Do

Pad sheets, app bars, and full-screen modals with padding-top: var(--safe-top) and padding-bottom: var(--safe-bottom) so content clears the notch, punch-hole, home indicator, and gesture-nav pill.

Don't

Hard-code 44 px or 34 px insets. They differ per device, OS, and orientation — use the env-backed tokens.

Do

Test on iOS (Dynamic Island + home indicator) and Android (status bar + gesture nav). The tokens resolve correctly only when the Android host turns off "fit system windows".

Don't

Branch CSS by user-agent. The same --safe-* tokens cover both platforms — branching means a missed device.

Responsive

Same tokens, retuned for touch. Scale up the hit targets, drop the density, and keep the visual rhythm consistent across Windows, macOS, iPadOS, and Android.

Breakpoints

Mobile-first. Layouts collapse top-down.

TokenRangeColumns
sm0 – 599 px4
md600 – 1023 px8
lg1024 – 1439 px12
xl1440 px +12

Hit targets

Minimum tappable area per surface. Always pass the largest constraint that applies.

28
Dense table
36
Desktop default
44
Mobile / tablet
48
Material AAA

Type-scale tweaks

Headings step down one notch on sm; body stays.

RoleWebTabletMobile
Display40 / 4832 / 4028 / 36
H128 / 3624 / 3222 / 30
H222 / 3020 / 2818 / 26
Body14 / 2014 / 2015 / 22
Caption12 / 1612 / 1612 / 16

Platform consistency · Windows ↔ macOS ↔ iOS ↔ Android

Tokens are platform-agnostic. The only platform-aware pieces are scrollbar width, native focus-ring colour, and font-smoothing — all handled by the design system shell.

ConcernWeb (Win / Mac)iOS / iPadOSAndroid
Tap target36 px44 × 44 pt48 × 48 dp
Body text14 / 2015 / 22 (Dynamic Type)14 / 20 (sp)
Corner radius--radius-md 8 px10 px (continuous)12 px
Elevation--shadow-sm / md / lgsame shadow tokenssame + state-layer overlay
Focus ring2 px indigo + 3 px halosystem tint2 dp indigo
Iconography1.5 px stroke @ 20 px1.5 pt stroke @ 24 pt1.5 dp stroke @ 24 dp
Safe area—env(safe-area-inset-*)system bars · 56 dp top

Layout adaptation

How the aiRA chat surface restructures across breakpoints.

Mobile · sm
aiRA
Full-bleed chat. Side-nav becomes a drawer. Canvas stacks vertically.
Tablet · md
aiRA · Acme
Rail nav + single panel. Split panel collapses to tabs.
Desktop · lg / xl
Chat thread
Artifact preview

Do / don't

Do

Use the same semantic tokens at every breakpoint. Only the hit-target size and the type-scale notch change.

Don't

Re-skin a component per breakpoint. The tokens already handle theme; breakpoints only resize.

Do

Bump every tappable target to ≥ 44 px on mobile and ≥ 48 dp on Android.

Don't

Ship a 28 px target outside a dense table. 28 px is the floor for opt-in density only.

Voice rules

Voice is part of the design. Nine hard rules.

  1. Sentence case. Titles, buttons, labels — all sentence case. Avoid Title Case unless it's a proper noun.
  2. Active voice. "aiRA created the promotion" not "The promotion was created."
  3. One exclamation per first session. No exclamation marks elsewhere.
  4. No marketing language. "Set up the campaign" not "Unleash your campaign magic."
  5. Don't apologise reflexively. If aiRA can't do something, say what it can, then what's next.
  6. Don't anthropomorphise. aiRA does not "think", "feel", or "hope". It does work.
  7. Numbers in numerals, even for one-digit. "5 items left." Easier to scan.
  8. Plain verbs in activity rows. "Querying analytics" not "Executing query_engine_v3".
  9. Refusal grammar follows the can't / can / next template. See below.

Do / don't

Do

"Couldn't run that query — the table isn't connected yet. Connect it under Settings → Data."

Don't

"Sorry, sorry — I'll try again." Reflexive apologies clutter the surface.

Do

"aiRA created 1 promotion." Numerals scan faster than spelled-out one.

Don't

"aiRA thought you might like this!" Reads as artificial enthusiasm.

Refusal grammar — can't / can / next

When aiRA can't fulfil a request, the response has three parts: what it can't do, what it can, and what to do next.

I can't pull transaction data for the Acme brand — RBAC restriction.
I can show you aggregate spend by tier across the brands you have access to.
Ask your admin to add Analytics for Acme, or pick a different brand to query.

Button

StableUpdated Owner · Punya Shetty

Four variants, three sizes. Primary indigo-600 fill. The single canonical button atom — no per-context one-offs.

Variants

Sizes

Properties

PropValues
variantprimary · secondary · tertiary · danger
sizesm · md · lg
disabledboolean

Do / don't

Do

Use primary for the single confirming action; secondary for the alternative; tertiary for low-frequency.

Don't

Stack two primary buttons next to each other. There can only be one.

Do

Match button size to the surrounding density.

Don't

Use a danger button for routine destructive actions like Remove — use secondary.

Icon button

StableUpdated Owner · Punya Shetty

Single-icon affordances. 32 px default; 44 px touch. Use the Stop icon only for halting an in-flight aiRA generation.

Sizes & states

Stop icon (Generating state)

Filled square inside a coloured circle. Indicates active generation; clicking it stops aiRA.

Used inside the Composer when aiRA is streaming. See Composer.

Do / don't

Do

Pair every icon button with an accessible name describing the action.

Don't

Pack more than three icon buttons in a row without visual separation.

Input

StableUpdated Owner · Punya Shetty

36 px default. Ten states.

Enter a valid email.
High multiplier — review.

Do / don't

Do

Show a label above the input. Placeholder is never a substitute for a label.

Don't

Trigger validation on every keystroke. Validate on blur or submit.

Do

Pre-fill the most likely value when one exists.

Don't

Use a generic Save error — name the field that failed.

Textarea

StableUpdated Owner · Punya Shetty

Multi-line input. Resizable on the Y axis only.

Markdown is supported.

Do / don't

Do

Resize vertically only.

Don't

Auto-grow past six lines without showing a scroll affordance.

Chip

StableUpdated Owner · Punya Shetty

Neutral metadata tag, removable. Carries an optional dot for status semantics.

Default Brand Active Pending Blocked Scheduled

Do / don't

Do

Reserve colour semantics — green for success, amber for pending, red for danger.

Don't

Stack chips, tags, and badges in the same row — pick one primitive per context.

Tag

StableUpdated Owner · Punya Shetty

Categorical label. Larger surface than a chip; no dot.

Loyalty Promotions Analytics

Do / don't

Do

Use Tag for categorical grouping (channel, type, owner).

Don't

Use Tag for status — that's a Pill.

Badge

StableUpdated Owner · Punya Shetty

Count or numeric indicator. Sits on top of a parent surface.

3 12 99+

Do / don't

Do

Cap counts at 99+. Beyond that, the precise number is noise.

Don't

Use a badge for a state name — that's a Pill.

Pill (status)

StableUpdated Owner · Punya Shetty

Lifecycle states across approvals, scheduled jobs, and content. Six variants.

Draft Awaiting approval Upcoming Approved Live Rejected

Properties

VariantToken pair
draft--approval-draft-bg · --approval-draft-text
awaiting--approval-awaiting-bg · --approval-awaiting-text
upcoming--approval-upcoming-bg · --approval-upcoming-text
approved--approval-approved-bg · --approval-approved-text
live--approval-live-bg · --approval-live-text
rejected--approval-rejected-bg · --approval-rejected-text

Do / don't

Do

Use the canonical state colours so users learn the lifecycle once.

Don't

Invent new state labels — extend the existing set instead.

Avatar

StableUpdated Owner · Punya Shetty

Two variants only — User initials and the aiRA mark. Both use the canonical aiRA gradient (--avatar-grad). No per-agent avatars: the product has one assistant voice.

User avatar — initials

PS PS PS Initials = first + last name, capitalised.

aiRA avatar — sparkle mark

Wherever aiRA speaks.

Do / don't

Do

Use the aiRA avatar wherever the assistant talks. Use the User avatar for the human user; never for aiRA.

Don't

Tint avatars by "agent type". There is only one aiRA.

Do

Provide aria-label when there's no visible name.

Don't

Use random colours for user initials. The system has one avatar gradient.

Citation chip

StableUpdated Owner · Punya Shetty

RAG provenance. Real <a href> links. Hover lifts the border + text to brand.

Inline

Tier upgrades happen automatically when 90-day spend crosses the threshold 1Tier upgrade SOP. The system re-evaluates eligibility nightly 2Loyalty engine spec.

Hover & focus

Hover or keyboard-focus brightens the border (--citation-border-hover) and shifts the text to brand colour (--citation-text-hover).

Appended list

Customer 88959517 wasn't issued points because the transaction amount was below the promotion's minimum threshold.

Do / don't

Do

Number citations sequentially in reading order.

Don't

Cite more than four sources inline. Use the appended list.

Tooltip

StableUpdated Owner · Punya Shetty

Short explainer on hover/focus. Surface is --tooltip-bg (slate-700 — softer than near-black) with white text at 9.6 : 1 contrast.

Do / don't

Do

Use Tooltip for short, optional context. Trigger on hover AND focus.

Don't

Hide essential information inside a tooltip. Surface it inline.

Keyboard (kbd)

StableUpdated Owner · Punya Shetty

Visualises a key in a shortcut. Use sparingly — inline help, command palette docs, the Selection card keyboard hint.

↑↓EnterEsc⌘K

Divider

StableUpdated Owner · Punya Shetty

1 px horizontal rule using --border-default.

Section A
Section B

Spinner

StableUpdated Owner · Punya Shetty

Indeterminate progress. 14 px default.

Loading…

Skeleton

StableUpdated Owner · Punya Shetty

Placeholder for loading content. Hide from assistive tech (aria-hidden).

Field

StableUpdated Owner · Punya Shetty

Composed label + control + helper + error. The shape every form control uses.

Visible internally only. 1–60 characters.

Accessibility

  • Pair label[for] ↔ input[id].
  • Use aria-describedby to associate the helper with the input.
  • Error message: announce via aria-describedby in addition to the visual border colour.

Checkbox

StableUpdated Owner · Punya Shetty

Multi-select within a group. Wrap with <fieldset> + <legend>.

Test scenarios

Do / don't

Do

Wrap a group with a fieldset and a legend so the group has a name.

Don't

Use a single standalone checkbox for binary on/off — that's a Toggle.

Radio

StableUpdated Owner · Punya Shetty

Exclusive selection. Always wrap in <fieldset> + <legend>. The disabled state is documented explicitly.

Reward type

States

  • Default — empty circle.
  • Selected — filled brand dot inside the circle.
  • Disabled — greyed, cursor not-allowed.
  • Disabled-selected — selected and locked. Used when policy pins a value.

Do / don't

Do

Always pre-select the safest, most-common default.

Don't

Use radio for more than five options. Switch to a Dropdown.

Toggle

StableUpdated Owner · Punya Shetty

Binary on/off that takes effect immediately. For settings that need an explicit Save, use a Checkbox in a form.

Do / don't

Do

Reflect immediate effect — no Save button.

Don't

Use a Toggle when the change needs a confirmation. Use a Modal.

Stepper

StableUpdated Owner · Punya Shetty

Ordered multi-step flow.

✓Draft
✓Submit
3Review
4Approve
5Live

Progress bar

StableUpdated Owner · Punya Shetty

Determinate progress for known durations.

Feedback control

StableUpdated Owner · Punya Shetty

Two bare icon buttons — thumbs-up / thumbs-down — sitting inline with the action row. No pill, no container, no divider. The selected thumb fills with the link colour. An optional reason picker pops below on Not helpful.

Default
Thumbs-up selected
Thumbs-down selected — opens reason picker

Do / don't

Do

Render the thumbs as bare icon buttons, sitting inline with the action row. No pill, no border, no divider.

Don't

Wrap the controls in a chip, pill, or bordered container. The icons themselves are the affordance.

Do

Open a reason picker on Not helpful. The signal alone is half useful.

Don't

Block the user with a modal reason prompt. Keep it inline and dismissible.

Action row

StableUpdated Owner · Punya Shetty

Inline actions under an aiRA reply — Copy · Support · Feedback (thumbs-up / thumbs-down). Keyboard focus uses the global :focus-visible ring; no per-action override.

Tabs

StableUpdated Owner · Punya Shetty

Local navigation between views of the same data.

Top 10 stores by last-month sales.

Popover

StableUpdated Owner · Punya Shetty

Rich content with actions. Footer buttons are right-aligned; primary on the far right.

Rule

Popover footer actions are right-aligned. Primary action sits on the far right. Tertiary or destructive actions sit to its left.

Do / don't

Do

Right-align footer actions; primary on the far right.

Don't

Hide essential information in a popover. Surface it inline.

Toast

StableUpdated Owner · Punya Shetty

Transient notification. Auto-dismisses; reserves for non-critical confirmations.

Copied to clipboard
Promotion config copied as YAML.
Couldn't save draft
Network error. Retrying…

Do / don't

Do

Auto-dismiss after about four seconds.

Don't

Stack more than three toasts. Collapse the rest into a notification centre.

Nudge card

StableUpdated Owner · Punya Shetty

Suggestion or prompt presented next to the chat surface. Blue left-border accent.

Try Analytics for weekend trends
aiRA spotted that 38% of your top customers spend on Saturdays. See a breakdown by tier.
Heads up — fraud lookup may take longer today
A queued backfill is running. Results are accurate; expect 30–60 s response times.

Do / don't

Do

Render adjacent to the chat surface, never on top of it.

Don't

Stack two nudges. One per session.

Selection card

StableUpdated Owner · Punya Shetty

Disambiguation step. Keyboard-first, escape-to-skip, free-type fallback. Option rows and the "Something else" row use visually distinct hover treatments so the two never look like the same target.

Default

What kind of promotion are you creating?

1 of 2
  1. 1Points multiplier on purchases
  2. 2Bonus points (flat reward)
  3. 3Coupon or discount code
  4. 4Tiered spend reward
Something else
↑↓ to navigate · Enter to select · 1–5 jumps · Esc skips

Interaction matrix

Option rows and "Something else" use different hover surfaces so they read as separate affordances.

ElementDefaultHoverFocus (keyboard)Selected
Option rowtransparent--bg-hoverfocus-visible ring--bg-selected + filled number badge
"Something else"transparent, 1 px top divider--bg-accent-subtle (distinct violet tint)focus-visible ringn/a (escape hatch)
Skip buttontertiary chip--bg-hover backgroundfocus-visible ringn/a

Properties

PropValues
titleThe question being asked
optionsArray of 2–5 strings
step{ current, total } when chained
allowSomethingElseboolean
onSelectFires on click, Enter, or numeric key (1–5)
onSkipFires on Skip click or Esc

Do / don't

Do

Use when the prompt has 2–5 likely interpretations. Numbered options are faster than prose.

Don't

Use for more than 5 options. Switch to a Dropdown (searchable).

Do

Always include "Something else / Skip". Never force the user into a listed option.

Don't

Block the composer while a Selection card is up. Typing dismisses the card.

To-do list

BetaUpdated Owner · Punya Shetty

Interactive checklist with check-off. Items strike through and fade once complete. Used in onboarding, multi-step setup flows, and anywhere a small set of discrete tasks needs visual progress.

Default

2 of 5 done

Anatomy

  • Item — checkbox + label. Whole row is clickable. Hover lifts background to hover surface.
  • Done state — checkbox fills with brand colour; label strikes through and shifts to tertiary text.
  • Progress line — "X of N done", live-region-polite so screen readers announce updates.

Interactions

  • Mouse — click anywhere on the row to toggle.
  • Keyboard — Tab moves between items; Space toggles. Each item is a real input[type="checkbox"].
  • Transition — 200 ms fade + strike-through (reduced-motion replaces with an instant snap).

Do / don't

Do

Use for short, ordered lists (3–8 items) where each row is a discrete task.

Don't

Use for long, unstructured task lists. Past ~8 items, switch to a Data table with status.

Do

Animate the strike-through on toggle (200 ms). Builds visual confirmation.

Don't

Hide completed items by default. Crossed-out items show progress and remind the user what was done.

Chat message

StableUpdated Owner · Punya Shetty

Three message types. aiRA's reply free-flows — anchored by the aiRA avatar at the start of the message. No agent label, no badge, no chip.

Variants

Show me last month's sales by store.
Here's last month's sales by store. Top performer is HSR Layout at ₹14.2L.
aiRA reused the cached query plan from 2 minutes ago.

Anatomy

  • Thread — role="log" aria-live="polite"; new messages announce.
  • User message — grey pill, end-aligned, max 80% width.
  • aiRA reply — aiRA avatar (sm) + free-flowing body. No badge above the prose. Body wraps in role="status" aria-live="polite".
  • System — single quiet line in tertiary text. Metadata, not announced.
  • Error — inline alert block with role="alert".

Do / don't

Do

Let aiRA's reply free-flow as plain text. The avatar is enough attribution.

Don't

Wrap aiRA's reply in a bubble or attach an agent-name badge. There are no agent labels.

Composer

StableUpdated Owner · Punya Shetty

Input area with attach, voice, send. While aiRA is generating, the send icon swaps for the Stop icon — clicking it interrupts generation and keeps the partial output. Agents are never disabled; they're stopped.

Default · empty

The Send button stays disabled until the user types. Disabled uses the neutral disabled surface; the gradient never lives on this control.

Default · with text

Once the input has content, Send enables and renders in the primary action colour (indigo-600). No gradient — gradients are reserved for the aiRA mark.

Generating state

The send icon is replaced by the enabled Stop icon. Placeholder reads "Generating…". Clicking Stop transitions the conversation state to aborted; partial output is preserved.

Read-only (conversation locked)

For historical or shared sessions where the user can read but not send.

Do / don't

Do

Swap the Send icon for the Stop icon while aiRA generates.

Don't

Disable the composer because aiRA is busy. Let the user keep typing.

Approval card

BetaUpdated Owner · Punya Shetty

aiRA proposal awaiting human review. Header is the proposal title only — no agent badge. High-risk variants use the Type-to-confirm pattern.

Default

2× points on weekends · Gold tier

aiRA generated this promotion from your prompt. Review the diff and apply, edit, or reject.

3name: "Gold weekend boost"
4multiplier: 2
5applies_to: ["saturday","sunday"]
6audience: gold_tier
Awaiting approval

Status variants

Six lifecycle states. Status pill on the left of the footer; primary action shifts per state.

Weekend boost · Gold tier

Draft — not yet submitted for approval.

Draft

Weekend boost · Gold tier

Submitted 4 min ago · with Priya R. for review.

Awaiting approval

Weekend boost · Gold tier

Approved by Priya R. · scheduled to go live Sat at 00:00.

Upcoming

Weekend boost · Gold tier

Approved by Priya R. · 2 hours ago.

Approved

Weekend boost · Gold tier

Live since Sat 00:00 · 1,247 redemptions so far.

Live

Weekend boost · Gold tier

Rejected by Priya R. · 2 hours ago.

Multiplier is 2× — too aggressive for the Gold tier this quarter. Try 1.5× and resubmit.
Rejected

High-risk · Type-to-confirm

Production-affecting, irreversible, security-relevant, or financial changes require a secondary confirmation. On Accept click, the footer Accept button swaps in place for a slim input (placeholder "Type APPROVE to confirm") + a Confirm button — both disabled until the input matches APPROVE (case-insensitive). Reject and Edit remain reachable. Same compact primary-button size as routine Accept — no giant outsized button.

3× multiplier · All members

High risk · production

Applies to all 1.2 M members; irreversible once live. Confirm by typing APPROVE.

3audience: gold_tier
4multiplier: 2
4multiplier: 3
5audience: all_members # scope expanded
Awaiting approval

Diff overflow behaviour

Lines in diffTreatment
≤ 12Render in full. No scroll, no collapse.
13 – 40Max-height 320 px with internal scroll. Caption above the diff: "12 of 40 lines visible".
> 40Collapse to a summary (N added · M removed · K changed) with "View full diff" opening a Modal. Approval cannot complete until the full diff has been opened.
Binary payloadReplace diff with a before/after preview. Never paste raw bytes.

Risk flagging rules

  • Production-affecting — any change whose audience or schedule expands. Surface a risk pill in the header.
  • Irreversible — deletions, archival, schema changes. Require Type-to-confirm.
  • Security-relevant — RBAC, audience PII filters, data-source connections. Approval shows the reviewer's name + timestamp; an audit-log entry is written.
  • Financial — multipliers, coupon values, payout caps. Require Type-to-confirm + restate the financial impact in plain language.

Multi-brand override path

The status pill colours and the lifecycle states (Draft → Awaiting → Upcoming → Approved → Live → Rejected) are wired to Capillary's Connect+ workflow. White-label / reseller deployments need a --approval-state-* token layer that maps tenant lifecycle names to the system's six-state machine. Tracked under Deferred · Multi-brand theming hooks; flagged here so the override surface is designed in, not bolted on later.

Do / don't

Do

Show the diff or preview inline up to twelve lines; collapse beyond that.

Don't

Reuse the routine Accept styling on high-risk proposals. Use Type-to-confirm.

Code block

StableUpdated Owner · Punya Shetty

First-class output for SQL and scripts. Language badge, Copy / Explain actions.

JavaScript
function normaliseRow(row) {
  // Drop fields not in target schema
  const { id, amount, currency } = row;
  return { id, amount: Number(amount), currency: currency || 'INR' };
}

Inline diff

StableUpdated Owner · Punya Shetty

Add / remove / context lines. Used inside Approval cards and the Change-log feed.

1tier: gold
2benefits:
3 - points_multiplier: 1.5
3 - points_multiplier: 2.0
4 - free_shipping: true
5expiry: 90_days

Do / don't

Do

Pair sign and colour — plus for added, minus for removed.

Don't

Hide context lines. Surrounding context tells the reviewer where the change lands.

Agent activity

StableUpdated Owner · Punya Shetty

Streaming disclosure of what aiRA is doing. Use the single-step row for one task; the multi-step list when several steps run in sequence.

Single-step

One row: spinner + step verb + optional detail. Use plain verbs — no internal job IDs.

Querying analytics DB · last 30 days of sales

Multi-step · todo list

Each step starts empty; the indicator fills on activation and locks to a check mark when complete. Completed rows strike through with faded text, like ticking items off a list.

Prompt enriched with brand context
Intent interpreted · build ingestion dataflow
Generating block sequence…
Validating outputs
Preview & explain

Do / don't

Do

Use plain verbs. "Querying analytics DB" — not "Executing query_engine_v3".

Don't

Surface internal job IDs.

Do

Show duration on completed steps.

Don't

Spin forever. Past 10 s, append "Still working — taking longer than usual" inline.

Conversation state machine

BetaUpdated Owner · Punya Shetty

An AI turn has seven stops. The table below maps every state to its visible UI, user affordance, and live-region behaviour.

StateVisible UIUser affordanceLive region
idle Composer focused; send button disabled (empty input). Type to start. None.
user-sent User message renders end-aligned. Composer clears, send returns to disabled. Continue typing follow-ups. Thread role="log" announces.
thinking Activity row above the composer — spinner/sparkle + step verb. Cancel on the activity row (Esc alias). role="status" aria-live="polite". Step changes announce.
streaming aiRA message renders; tokens append in place. Composer Send is replaced by the enabled Stop icon. Click Stop to interrupt (preserves partial output). Body role="status"; tokens announced in chunks ≥ 80 chars.
done Final reply renders; Action row appears (Copy · Support · Feedback). Compose a follow-up — loops back to user-sent. Live region settles.
error Error message in the danger surface. Plain-language reason. Retry (rewinds to user-sent with original prompt). Optional Report. role="alert" — interrupts.
aborted Partial reply stays; system line: "Stopped at your request." Continue from here · Regenerate · ask a new question. System line not announced.

Transition rules

  • thinking → streaming happens on the first content token. Never an empty streaming UI.
  • If thinking exceeds 10 s without a token, append "Still working — taking longer than usual" to the activity row. Do not auto-cancel.
  • error is reachable from thinking, streaming, aborted. The retry path rewinds to user-sent with the original prompt.
  • aborted is reachable only from streaming (Stop) or thinking (Cancel) — never from done.
  • done, aborted, and error all loop back to user-sent when the user composes a follow-up.
  • During streaming, the composer Send icon swaps to the Stop icon. Clicking it triggers streaming → aborted.
  • Composer is never disabled by the assistant state. Read-only treatment is reserved for locked conversations (shared sessions, audit views).

Bottom sheet

StableUpdated Owner · Punya Shetty

Mobile escalation of Modal. Honours --safe-bottom.

Do / don't

Do

Pad the bottom with safe-bottom so the action row clears the home indicator.

Don't

Stack content beyond 95% height — route to a page instead.

List row

StableUpdated Owner · Punya Shetty

Chat history rail — a narrow sidebar pinned to the left of the chat surface listing previous sessions (ChatGPT / Gemini convention). Each row is a single click target. An export marker on the right shows how many times the session was exported (FTP push, CSS download, or workbook download).

Sessions
2× points · Gold weekends
2m ago
3
Why didn't customer 88959517 get points?
30m ago
Last month sales by store
2h ago
2
Acme tier-2 redemptions vs. prev quarter
Yesterday
1
Draft Diwali campaign brief
3d ago
Chat surface (selected session renders here)
Export marker — what each pip means
Server — session output was pushed to an FTP destination
Spreadsheet — the workbook (.xlsx) was downloaded
Code file — the CSS file was downloaded
3 Total exports across all kinds (FTP + workbook + CSS)

Anatomy

  • Width. 260 px on desktop. The rail is narrow on purpose so the chat surface owns the viewport.
  • Row. One title (max two lines, ellipsis after), one timestamp, and an optional export marker on the trailing edge.
  • Selected state. Active session uses --bg-selected with a 3 px --border-focus leading border.
  • Export marker. Up to three distinct icon pips (FTP / workbook / CSS) followed by a total count. Repeats of the same type collapse into the single pip — the count carries the multiplicity.

Do / don't

Do

Wrap the whole row in a single interactive target. The export marker is a tooltip, not a separate link.

Don't

Split actions into multiple click targets on the same row.

Do

Show one pip per distinct export type. The count carries multiplicity.

Don't

Repeat the same pip three times for three exports of the same kind. The pip is the type; the count is the volume.

Data table

StableUpdated Owner · Punya Shetty

Every column header carries the same sort icon — chevrons-up-down. A click cycles ascending → descending → unsorted. The icon never changes shape; only its colour reflects state (slate-500 idle, indigo-600 active). aria-sort on the header carries the order for assistive tech. No filter icons in headers.

Default · sortable

Store Sales Txns Δ vs prev 30d Status
HSR Layout₹14,21,5403,219+8.2%Active
Indiranagar₹11,03,2102,884+2.1%Active
Koramangala₹9,84,7202,440−4.6%Active
Whitefield₹7,21,3101,940−11.8%Upcoming

Sort states

One icon, three states. Shape stays the same on every header; colour and aria-sort carry the meaning.

StateIcon shapeIcon colouraria-sort
Unsorted (idle)chevrons-up-down--text-tertiary (slate-500)none
Ascending (active)chevrons-up-down--text-link (indigo-600)ascending
Descending (active)chevrons-up-down--text-link (indigo-600)descending

Do / don't

Do

Use the same chevrons-up-down icon on every column header. Consistency reads as one control, repeated.

Don't

Swap to a triangle, caret, or directional arrow when a column becomes active. Shape change adds visual noise.

Do

Right-align numbers; use font-variant-numeric: tabular-nums.

Don't

Use red for negative deltas. Orange (delta-negative) is the chart palette default.

Do

Wire aria-sort on the header — assistive tech reads it.

Don't

Pair a separate filter icon with the sort. The data table sorts; it doesn't filter.

Card

StableUpdated Owner · Punya Shetty

Generic container. Use only when the content needs visual elevation; otherwise prefer a plain section.

Loyalty health · Last 30 days

1,247 active members · 38% redemption rate · 12% new tier upgrades.

Do / don't

Do

Use Card only when the content needs elevation; otherwise a plain section is enough.

Don't

Nest cards inside cards.

Form

StableUpdated Owner · Punya Shetty

Standard composition — field stack + actions footer.

Do / don't

Do

Keep the action footer right-aligned; primary on the far right.

Don't

Hide the submit affordance below the fold.

Change-log feed

BetaUpdated Owner · Punya Shetty

A product UI pattern for visualising change history inside aiRA — e.g. a loyalty-program edit timeline, a Config audit trail, or "What changed since you last looked." Vertical timeline with grouped entries.

Today · 13 May 2026
Priya R. · Approved · 11:42
Approved Weekend boost · Gold tier — scheduled to go live Sat 00:00.
1Proposal #4421
aiRA · Changed · 11:30
Updated multiplier from 1.5× to 2.0×.
3 - multiplier: 1.5
3 - multiplier: 2.0
Yesterday · 12 May 2026
Punya S. · Added · 16:08
Created promotion draft Weekend boost · Gold tier.
Punya S. · Reverted · 14:22
Reverted earlier change to audience; scope narrowed back to Gold tier.
Older
Rohit M. · Removed · 10 May
Removed deprecated auto_apply flag from program settings.

Anatomy

  • Filter strip — change-type multi-select + date range.
  • Day group — header label above an entries cluster.
  • Entry — actor avatar · name · change-type tag · timestamp · summary · optional diff or citation.
  • Timeline — vertical gutter with a small dot per entry.
  • Empty state — "No changes in this period" with a "Clear filters" CTA.

Change-type tags

Added Changed Removed Fixed Approved Reverted

Do / don't

Do

Group entries by day; lead with actor and change type.

Don't

Include entries with no summary. Every entry must say what changed.

Empty states

StableUpdated Owner · Punya Shetty

Specific copy that names what's missing and offers a next step. Never a generic "No results".

No matches for "xyz"

Try a shorter query, or search by promotion ID. You can also reset filters to see everything in this brand.

Loading & streaming

StableUpdated Owner · Punya Shetty

Three loading patterns. Choose by the work being done.

Spinner — single short task

Loading…

Skeleton — content shell

Agent activity

Drafting promotion config

Error handling

StableUpdated Owner · Punya Shetty

Errors are first-class UI. Plain-language reason, then what to do next. Never a stack trace.

My Configs

StableUpdated Owner · Punya Shetty

A list of saved configurations the user can reuse — list-row organism with a leading aiRA avatar, the config name, and a status pill.

Weekend boost · Gold tier
Updated 2m ago
Upcoming
Diwali bonus · All members
Updated yesterday
Live
Birthday month · Auto
Updated 5 days ago
Draft

Mobile preview

StableUpdated Owner · Punya Shetty

Desktop is the primary surface; mobile honours safe-area tokens for sheets and bottom CTAs.

Today · 13 May
2× points for Gold this weekend.
Got it. I drafted a promotion — review and send for approval.

In context

Tokens applied to the aiRA surface. The same theme switch flips every element below — sidebar, header, tabs, metric cards, alert — with no layout shift and no contrast loss. Toggle the Dark button in the top bar and watch the cascade.

Customer insights · April 2026
Auto-refreshed at 09:14
24,810
Tier 1 active
+18%
38,210
Tier 2 active
+11%
12,430
Lapsed 30d
−22%
420
VIP top 1%
+8%

Every surface above is composed from semantic tokens — --bg-surface, --bg-surface-alt, --bg-selected, --border-default, --text-primary, --text-tertiary, --text-link, --delta-positive, --delta-negative. Toggling the theme on the top bar re-resolves every one of those — no per-component override needed.

Email templates

StableUpdated Owner · Punya Shetty

Four transactional templates aiRA sends. Shared rules apply to all four — listed once below.

Shared rules

  • Hardcoded hex values. Email clients strip CSS variables. Copy resolved values from tokens.css at build time.
  • Table-based layout for any structural layout that must survive Outlook 2016+ on Windows. Flex and grid are not safe.
  • One CTA. Never two primary buttons of equal weight.
  • Width 560 px max. Wider templates clip on mobile clients.
  • No gradients in email. Solid colours only.
  • No tracking pixels. Use Capillary's transactional pipeline only.
  • Reply-to a routable address if the email surfaces reply-based affordances.

Template · Share session

Sent when a user shares a chat session with a colleague.

Template · Approval request

Sent when a reviewer is asked to approve an aiRA proposal.

Template · Scheduled-run result

Sent when a scheduled aiRA job completes (daily / weekly digest).

Template · Daily digest

A morning summary of pending items the user owns.

Do / don't

Do

Render diffs as text in the email. Survives mobile zoom, alt-text fallback, and assistive tech.

Don't

Send a "click to view in browser" stub. Put the decision in the email.

Accessibility

WCAG 2.2 AA+ as the floor. Contrast verified token-by-token, light + dark.

Verified contrast — light mode

PairingRatioWCAG
text-primary on bg-surface15.8 : 1AAA
text-secondary on bg-surface9.6 : 1AAA
text-tertiary on bg-surface5.0 : 1AA
btn-primary-text on btn-primary-bg8.2 : 1AAA
tooltip-text on tooltip-bg (slate-700 surface, white text)9.6 : 1AAA
citation-text-hover on citation-bg-hover7.4 : 1AAA
status-upcoming-text on status-upcoming-bg5.6 : 1AA

Verified contrast — dark mode

PairingRatioWCAG
text-primary on bg-surface14.1 : 1AAA
tooltip-text on tooltip-bg (slate-100 surface, slate-900 text)14.0 : 1AAA
btn-primary-text on btn-primary-bg7.4 : 1AAA
status-upcoming-text on status-upcoming-bg7.2 : 1AAA

Hard rules

  • Hit area ≥ 44 × 44 px (touch) / ≥ 32 × 32 px (mouse-only desktop).
  • focus-visible on every interactive element.
  • Status communicated by colour AND text/icon — never colour alone.
  • prefers-reduced-motion halts animation (sparkle, todo strike-through, shimmer).
  • Form labels connected via for/id or aria-labelledby; errors via aria-describedby.
  • Avatars without a visible name carry aria-label.

Feedback routing

Five surfaces for telling the user something. Pick by urgency and persistence.

SurfaceUse when…Persistence
ToastNon-critical confirmation that the user can ignore.Auto-dismisses ~4 s.
BannerPage-level state that should persist.Stays until dismissed or condition changes.
Nudge cardSuggestion adjacent to the chat surface; user can act or ignore.Stays until dismissed.
Inline errorField-level validation.Stays until the field is valid.
ModalThe user must act before continuing.Blocks the page until resolved.

Conventions worth raising

Cmd+K · slash commands

aiRA is a B2B power-user tool. Cmd+K and slash commands are expected conventions. The system doesn't ship a palette component yet because the underlying command surface isn't designed. Until that's decided, do not ship a Cmd+K UI per team — the design will fragment. Tracked under Deferred.

Until the React library ships

The React component library is correctly deferred. Until then:

  • Markup source of truth — this index.html. Copy demo HTML verbatim; replace inline values with your data binding.
  • Tokens source of truth — tokens.css. Import once at app root. Never redefine a token locally.
  • Behaviour source of truth — the data-ds-* hooks inline at the bottom of this file (Tabs, Dropdown, sidebar search).

When the React library lands, the migration is a class-rename pass + a token resolver swap — not a rewrite.

Component status

System-wide parity. Figma means a 1:1 Figma library component exists. HTML means a live reference renders in this file. A11y means keyboard, ARIA, and contrast have been audited in light + dark for the latest spec.

ComponentFigmaHTMLA11yStatus
Button✓✓✓Stable
Icon button✓✓✓Stable
Input✓✓✓Stable
Textarea✓✓✓Stable
Chip✓✓✓Stable
Tag✓✓✓Stable
Badge✓✓✓Stable
Pill (status)✓✓✓Stable
Avatar✓✓✓Stable
Citation chip✓✓✓Stable
Tooltip✓✓✓Stable
Keyboard (kbd)✓✓✓Stable
Divider✓✓✓Stable
Spinner✓✓✓Stable
Skeleton✓✓✓Stable
Field✓✓✓Stable
Dropdown✓✓✓Stable
Checkbox✓✓✓Stable
Radio✓✓✓Stable
Toggle✓✓✓Stable
Stepper✓✓✓Stable
Progress bar✓✓✓Stable
Feedback control✓✓✓Stable
Action row✓✓✓Stable
Tabs✓✓✓Stable
Breadcrumb✓✓✓Stable
Popover✓✓✓Stable
Toast✓✓✓Stable
Banner✓✓✓Stable
Nudge card✓✓✓Stable
Selection card✓✓✓Stable
To-do list✓✓PartialBeta
Chat message✓✓✓Stable
Composer✓✓✓Stable
Approval card✓✓✓Beta
Code block✓✓✓Stable
Inline diff✓✓✓Stable
Agent activity✓✓✓Stable
Conversation state machine✓✓PartialBeta
Modal / Dialog✓✓✓Stable
Bottom sheet✓✓✓Stable
List row✓✓✓Stable
Data table✓✓✓Stable
Card✓✓✓Stable
Form✓✓✓Stable
Change-log feed✓✓PartialBeta

Reading the table

  • Stable — passed full review · safe to consume in product · changes are additive only.
  • Beta — visual and interaction spec is settled but a11y or cross-product review is still pending. Safe to consume; expect minor refinements before the next edition.
  • Experimental — exploratory · no consumer expectation · may be removed.
  • Deprecated — scheduled for removal; do not consume.
  • Partial in A11y means the audit ran but at least one note is open. Linked from the component's metadata strip.

Figma bundle

The Figma library mirrors the atomic IA. Tokens published as Figma variables from tokens.json in DTCG 2025.10 format.

tokens.json · DTCG 2025.10

{
  "$themes": [
    {
      "id": "light",
      "name": "Light",
      "selectedTokenSets": {
        "global": "enabled",
        "primitives": "source",
        "semantic-light": "enabled",
        "additions-light": "enabled"
      }
    },
    {
      "id": "dark",
      "name": "Dark",
      "selectedTokenSets": {
        "global": "enabled",
        "primitives": "source",
        "semantic-dark": "enabled",
        "additions-dark": "enabled"
      }
    }
  ],
  "$metadata": {
    "tokenSetOrder": [
      "primitives",
      "semantic-light",
      "semantic-dark",
      "additions-light",
      "additions-dark",
      "global"
    ]
  },
  "primitives": {
    "slate": {
      "25": {
        "$value": "#FBFCFD",
        "$type": "color"
      },
      "50": {
        "$value": "#F6F8FA",
        "$type": "color"
      },
      "100": {
        "$value": "#EEF1F4",
        "$type": "color"
      },
      "200": {
        "$value": "#E2E6EB",
        "$type": "color"
      },
      "300": {
        "$value": "#C9D0D9",
        "$type": "color"
      },
      "400": {
        "$value": "#9AA4B2",
        "$type": "color"
      },
      "500": {
        "$value": "#6B7785",
        "$type": "color"
      },
      "600": {
        "$value": "#4D5867",
        "$type": "color"
      },
      "700": {
        "$value": "#364152",
        "$type": "color"
      },
      "800": {
        "$value": "#1F2937",
        "$type": "color"
      },
      "900": {
        "$value": "#111827",
        "$type": "color"
      },
      "950": {
        "$value": "#0A0F1A",
        "$type": "color"
      }
    },
    "indigo": {
      "50": {
        "$value": "#EEF0FF",
        "$type": "color"
      },
      "100": {
        "$value": "#DDE0FF",
        "$type": "color"
      },
      "200": {
        "$value": "#BAC1FF",
        "$type": "color"
      },
      "300": {
        "$value": "#8E97FF",
        "$type": "color"
      },
      "400": {
        "$value": "#6268FF",
        "$type": "color"
      },
      "500": {
        "$value": "#4A50EF",
        "$type": "color"
      },
      "600": {
        "$value": "#3438C9",
        "$type": "color"
      },
      "700": {
        "$value": "#252AA0",
        "$type": "color"
      },
      "800": {
        "$value": "#1B2078",
        "$type": "color"
      },
      "900": {
        "$value": "#121757",
        "$type": "color"
      }
    },
    "violet": {
      "50": {
        "$value": "#F4EEFF",
        "$type": "color"
      },
      "100": {
        "$value": "#E6DAFF",
        "$type": "color"
      },
      "200": {
        "$value": "#CDB5FF",
        "$type": "color"
      },
      "300": {
        "$value": "#B08CFF",
        "$type": "color"
      },
      "400": {
        "$value": "#8E61F2",
        "$type": "color"
      },
      "500": {
        "$value": "#6E40D9",
        "$type": "color"
      },
      "600": {
        "$value": "#5A2FB8",
        "$type": "color"
      },
      "700": {
        "$value": "#46248F",
        "$type": "color"
      }
    },
    "sky": {
      "50": {
        "$value": "#E6F1FF",
        "$type": "color"
      },
      "100": {
        "$value": "#C7E1FF",
        "$type": "color"
      },
      "200": {
        "$value": "#8EC1FF",
        "$type": "color"
      },
      "300": {
        "$value": "#5EA8FF",
        "$type": "color"
      },
      "400": {
        "$value": "#3BA7FF",
        "$type": "color"
      },
      "500": {
        "$value": "#1564CC",
        "$type": "color"
      },
      "600": {
        "$value": "#114DA0",
        "$type": "color"
      },
      "700": {
        "$value": "#0D3C7E",
        "$type": "color"
      }
    },
    "green": {
      "50": {
        "$value": "#E7F6EC",
        "$type": "color"
      },
      "100": {
        "$value": "#C6EAD2",
        "$type": "color"
      },
      "200": {
        "$value": "#8FD7A6",
        "$type": "color"
      },
      "300": {
        "$value": "#4FBE76",
        "$type": "color"
      },
      "400": {
        "$value": "#2A9D55",
        "$type": "color"
      },
      "500": {
        "$value": "#1F7A42",
        "$type": "color"
      },
      "600": {
        "$value": "#185C32",
        "$type": "color"
      },
      "700": {
        "$value": "#103E22",
        "$type": "color"
      }
    },
    "amber": {
      "50": {
        "$value": "#FFF6E0",
        "$type": "color"
      },
      "100": {
        "$value": "#FFE5A8",
        "$type": "color"
      },
      "200": {
        "$value": "#FFCB60",
        "$type": "color"
      },
      "300": {
        "$value": "#E89B1A",
        "$type": "color"
      },
      "400": {
        "$value": "#B97300",
        "$type": "color"
      },
      "500": {
        "$value": "#8A5500",
        "$type": "color"
      },
      "600": {
        "$value": "#5C3800",
        "$type": "color"
      }
    },
    "red": {
      "50": {
        "$value": "#FFECEC",
        "$type": "color"
      },
      "100": {
        "$value": "#FFCFCF",
        "$type": "color"
      },
      "200": {
        "$value": "#FF9C9C",
        "$type": "color"
      },
      "300": {
        "$value": "#F55959",
        "$type": "color"
      },
      "400": {
        "$value": "#D92D2D",
        "$type": "color"
      },
      "500": {
        "$value": "#B11E1E",
        "$type": "color"
      },
      "600": {
        "$value": "#8A1414",
        "$type": "color"
      }
    }
  },
  "semantic-light": {
    "bg": {
      "canvas": {
        "$value": "{primitives.slate.50}",
        "$type": "color"
      },
      "surface": {
        "$value": "#FFFFFF",
        "$type": "color"
      },
      "surfaceAlt": {
        "$value": "{primitives.slate.25}",
        "$type": "color"
      },
      "sunken": {
        "$value": "{primitives.slate.100}",
        "$type": "color"
      },
      "brand": {
        "$value": "{primitives.indigo.600}",
        "$type": "color"
      },
      "brandSubtle": {
        "$value": "{primitives.indigo.50}",
        "$type": "color"
      },
      "selected": {
        "$value": "{primitives.indigo.50}",
        "$type": "color"
      }
    },
    "text": {
      "primary": {
        "$value": "{primitives.slate.900}",
        "$type": "color"
      },
      "secondary": {
        "$value": "{primitives.slate.700}",
        "$type": "color"
      },
      "tertiary": {
        "$value": "{primitives.slate.500}",
        "$type": "color"
      },
      "onBrand": {
        "$value": "#FFFFFF",
        "$type": "color"
      },
      "link": {
        "$value": "{primitives.indigo.600}",
        "$type": "color"
      }
    },
    "border": {
      "subtle": {
        "$value": "{primitives.slate.100}",
        "$type": "color"
      },
      "default": {
        "$value": "{primitives.slate.200}",
        "$type": "color"
      },
      "strong": {
        "$value": "{primitives.slate.300}",
        "$type": "color"
      },
      "focus": {
        "$value": "{primitives.indigo.600}",
        "$type": "color"
      },
      "danger": {
        "$value": "{primitives.red.400}",
        "$type": "color"
      }
    }
  },
  "semantic-dark": {
    "bg": {
      "canvas": {
        "$value": "#0E1424",
        "$type": "color"
      },
      "surface": {
        "$value": "#131B2C",
        "$type": "color"
      },
      "surfaceAlt": {
        "$value": "#182135",
        "$type": "color"
      },
      "sunken": {
        "$value": "#0A101C",
        "$type": "color"
      },
      "brand": {
        "$value": "{primitives.indigo.400}",
        "$type": "color"
      },
      "brandSubtle": {
        "$value": "#1F2A55",
        "$type": "color"
      },
      "selected": {
        "$value": "#1F2A55",
        "$type": "color"
      }
    },
    "text": {
      "primary": {
        "$value": "#F4F6FA",
        "$type": "color"
      },
      "secondary": {
        "$value": "#C8CFDB",
        "$type": "color"
      },
      "tertiary": {
        "$value": "#94A0B4",
        "$type": "color"
      },
      "onBrand": {
        "$value": "#0A0F1A",
        "$type": "color"
      },
      "link": {
        "$value": "{primitives.indigo.300}",
        "$type": "color"
      }
    },
    "border": {
      "subtle": {
        "$value": "#1F2940",
        "$type": "color"
      },
      "default": {
        "$value": "#2A3654",
        "$type": "color"
      },
      "strong": {
        "$value": "#3A4870",
        "$type": "color"
      },
      "focus": {
        "$value": "{primitives.indigo.300}",
        "$type": "color"
      },
      "danger": {
        "$value": "{primitives.red.300}",
        "$type": "color"
      }
    }
  },
  "global": {
    "type": {
      "displayLg": {
        "$value": {
          "fontFamily": "Roboto",
          "fontSize": "40px",
          "lineHeight": "48px",
          "fontWeight": "600",
          "letterSpacing": "-0.02em"
        },
        "$type": "typography"
      },
      "displayMd": {
        "$value": {
          "fontFamily": "Roboto",
          "fontSize": "32px",
          "lineHeight": "40px",
          "fontWeight": "600",
          "letterSpacing": "-0.015em"
        },
        "$type": "typography"
      },
      "h1": {
        "$value": {
          "fontFamily": "Roboto",
          "fontSize": "28px",
          "lineHeight": "36px",
          "fontWeight": "600",
          "letterSpacing": "-0.01em"
        },
        "$type": "typography"
      },
      "h2": {
        "$value": {
          "fontFamily": "Roboto",
          "fontSize": "22px",
          "lineHeight": "30px",
          "fontWeight": "600",
          "letterSpacing": "-0.005em"
        },
        "$type": "typography"
      },
      "h3": {
        "$value": {
          "fontFamily": "Roboto",
          "fontSize": "18px",
          "lineHeight": "26px",
          "fontWeight": "600"
        },
        "$type": "typography"
      },
      "h4": {
        "$value": {
          "fontFamily": "Roboto",
          "fontSize": "16px",
          "lineHeight": "24px",
          "fontWeight": "600"
        },
        "$type": "typography"
      },
      "bodyLg": {
        "$value": {
          "fontFamily": "Roboto",
          "fontSize": "16px",
          "lineHeight": "26px",
          "fontWeight": "400"
        },
        "$type": "typography"
      },
      "bodyMd": {
        "$value": {
          "fontFamily": "Roboto",
          "fontSize": "14px",
          "lineHeight": "22px",
          "fontWeight": "400"
        },
        "$type": "typography"
      },
      "bodySm": {
        "$value": {
          "fontFamily": "Roboto",
          "fontSize": "13px",
          "lineHeight": "20px",
          "fontWeight": "400"
        },
        "$type": "typography"
      },
      "labelLg": {
        "$value": {
          "fontFamily": "Roboto",
          "fontSize": "15px",
          "lineHeight": "22px",
          "fontWeight": "500"
        },
        "$type": "typography"
      },
      "labelMd": {
        "$value": {
          "fontFamily": "Roboto",
          "fontSize": "14px",
          "lineHeight": "20px",
          "fontWeight": "500"
        },
        "$type": "typography"
      },
      "labelSm": {
        "$value": {
          "fontFamily": "Roboto",
          "fontSize": "12px",
          "lineHeight": "16px",
          "fontWeight": "500"
        },
        "$type": "typography"
      },
      "caption": {
        "$value": {
          "fontFamily": "Roboto",
          "fontSize": "12px",
          "lineHeight": "16px",
          "fontWeight": "400"
        },
        "$type": "typography"
      },
      "overline": {
        "$value": {
          "fontFamily": "Roboto",
          "fontSize": "11px",
          "lineHeight": "16px",
          "fontWeight": "500",
          "letterSpacing": "0.08em"
        },
        "$type": "typography"
      },
      "code": {
        "$value": {
          "fontFamily": "SF Mono",
          "fontSize": "13px",
          "lineHeight": "20px",
          "fontWeight": "400"
        },
        "$type": "typography"
      }
    },
    "spacing": {
      "0": {
        "$value": "0px",
        "$type": "dimension"
      },
      "1": {
        "$value": "4px",
        "$type": "dimension"
      },
      "2": {
        "$value": "8px",
        "$type": "dimension"
      },
      "3": {
        "$value": "12px",
        "$type": "dimension"
      },
      "4": {
        "$value": "16px",
        "$type": "dimension"
      },
      "5": {
        "$value": "20px",
        "$type": "dimension"
      },
      "6": {
        "$value": "24px",
        "$type": "dimension"
      },
      "8": {
        "$value": "32px",
        "$type": "dimension"
      },
      "10": {
        "$value": "40px",
        "$type": "dimension"
      },
      "12": {
        "$value": "48px",
        "$type": "dimension"
      },
      "16": {
        "$value": "64px",
        "$type": "dimension"
      },
      "20": {
        "$value": "80px",
        "$type": "dimension"
      }
    },
    "radius": {
      "xs": {
        "$value": "4px",
        "$type": "dimension"
      },
      "sm": {
        "$value": "6px",
        "$type": "dimension"
      },
      "md": {
        "$value": "8px",
        "$type": "dimension"
      },
      "lg": {
        "$value": "12px",
        "$type": "dimension"
      },
      "xl": {
        "$value": "16px",
        "$type": "dimension"
      },
      "2xl": {
        "$value": "20px",
        "$type": "dimension"
      },
      "pill": {
        "$value": "999px",
        "$type": "dimension"
      },
      "circle": {
        "$value": "9999px",
        "$type": "dimension"
      }
    },
    "elevation": {
      "xs": {
        "$value": {
          "color": "rgba(17,24,39,0.06)",
          "offsetX": "0px",
          "offsetY": "1px",
          "blur": "2px",
          "spread": "0px"
        },
        "$type": "shadow"
      },
      "sm": {
        "$value": {
          "color": "rgba(17,24,39,0.08)",
          "offsetX": "0px",
          "offsetY": "1px",
          "blur": "3px",
          "spread": "0px"
        },
        "$type": "shadow"
      },
      "md": {
        "$value": {
          "color": "rgba(17,24,39,0.10)",
          "offsetX": "0px",
          "offsetY": "4px",
          "blur": "8px",
          "spread": "-2px"
        },
        "$type": "shadow"
      },
      "lg": {
        "$value": {
          "color": "rgba(17,24,39,0.14)",
          "offsetX": "0px",
          "offsetY": "12px",
          "blur": "24px",
          "spread": "-6px"
        },
        "$type": "shadow"
      },
      "focus": {
        "$value": {
          "color": "rgba(52,56,201,0.32)",
          "offsetX": "0px",
          "offsetY": "0px",
          "blur": "0px",
          "spread": "3px"
        },
        "$type": "shadow"
      }
    },
    "motion": {
      "fast": {
        "$value": "120ms cubic-bezier(0.4,0,0.2,1)",
        "$type": "duration"
      },
      "base": {
        "$value": "200ms cubic-bezier(0.4,0,0.2,1)",
        "$type": "duration"
      },
      "slow": {
        "$value": "320ms cubic-bezier(0.4,0,0.2,1)",
        "$type": "duration"
      }
    }
  },
  "additions-light": {
    "ai": {
      "accent": {
        "$value": "{primitives.violet.600}",
        "$type": "color"
      },
      "accentSurface": {
        "$value": "{primitives.violet.50}",
        "$type": "color"
      },
      "accentBorder": {
        "$value": "{primitives.violet.200}",
        "$type": "color"
      },
      "onAccent": {
        "$value": "#FFFFFF",
        "$type": "color"
      }
    },
    "citation": {
      "bg": {
        "$value": "{primitives.slate.50}",
        "$type": "color"
      },
      "bgHover": {
        "$value": "{primitives.slate.100}",
        "$type": "color"
      },
      "border": {
        "$value": "{primitives.slate.200}",
        "$type": "color"
      },
      "text": {
        "$value": "{primitives.slate.700}",
        "$type": "color"
      },
      "icon": {
        "$value": "{primitives.slate.500}",
        "$type": "color"
      }
    },
    "diff": {
      "addBg": {
        "$value": "#E7F6EC",
        "$type": "color"
      },
      "addFg": {
        "$value": "{primitives.green.600}",
        "$type": "color"
      },
      "addMarker": {
        "$value": "{primitives.green.400}",
        "$type": "color"
      },
      "removeBg": {
        "$value": "#FFECEC",
        "$type": "color"
      },
      "removeFg": {
        "$value": "{primitives.red.500}",
        "$type": "color"
      },
      "removeMarker": {
        "$value": "{primitives.red.400}",
        "$type": "color"
      },
      "contextBg": {
        "$value": "#FFFFFF",
        "$type": "color"
      },
      "contextFg": {
        "$value": "{primitives.slate.700}",
        "$type": "color"
      },
      "gutterBg": {
        "$value": "{primitives.slate.50}",
        "$type": "color"
      },
      "gutterFg": {
        "$value": "{primitives.slate.500}",
        "$type": "color"
      }
    },
    "code": {
      "bg": {
        "$value": "{primitives.slate.50}",
        "$type": "color"
      },
      "bgInline": {
        "$value": "{primitives.slate.100}",
        "$type": "color"
      },
      "border": {
        "$value": "{primitives.slate.200}",
        "$type": "color"
      },
      "fg": {
        "$value": "{primitives.slate.900}",
        "$type": "color"
      }
    },
    "syntax": {
      "keyword": {
        "$value": "{primitives.indigo.700}",
        "$type": "color"
      },
      "string": {
        "$value": "{primitives.green.600}",
        "$type": "color"
      },
      "number": {
        "$value": "{primitives.amber.500}",
        "$type": "color"
      },
      "comment": {
        "$value": "{primitives.slate.500}",
        "$type": "color"
      },
      "operator": {
        "$value": "{primitives.slate.700}",
        "$type": "color"
      },
      "function": {
        "$value": "{primitives.violet.600}",
        "$type": "color"
      },
      "punctuation": {
        "$value": "{primitives.slate.600}",
        "$type": "color"
      }
    },
    "approval": {
      "draftBg": {
        "$value": "{primitives.slate.100}",
        "$type": "color"
      },
      "draftText": {
        "$value": "{primitives.slate.700}",
        "$type": "color"
      },
      "awaitingBg": {
        "$value": "{primitives.amber.50}",
        "$type": "color"
      },
      "awaitingText": {
        "$value": "{primitives.amber.500}",
        "$type": "color"
      },
      "approvedBg": {
        "$value": "{primitives.green.50}",
        "$type": "color"
      },
      "approvedText": {
        "$value": "{primitives.green.600}",
        "$type": "color"
      },
      "liveBg": {
        "$value": "{primitives.indigo.50}",
        "$type": "color"
      },
      "liveText": {
        "$value": "{primitives.indigo.700}",
        "$type": "color"
      },
      "rejectedBg": {
        "$value": "{primitives.red.50}",
        "$type": "color"
      },
      "rejectedText": {
        "$value": "{primitives.red.500}",
        "$type": "color"
      }
    }
  },
  "additions-dark": {
    "ai": {
      "accent": {
        "$value": "{primitives.violet.200}",
        "$type": "color"
      },
      "accentSurface": {
        "$value": "#2A1A4D",
        "$type": "color"
      },
      "accentBorder": {
        "$value": "{primitives.violet.400}",
        "$type": "color"
      },
      "onAccent": {
        "$value": "#0A0F1A",
        "$type": "color"
      }
    },
    "citation": {
      "bg": {
        "$value": "#182135",
        "$type": "color"
      },
      "bgHover": {
        "$value": "#1C2640",
        "$type": "color"
      },
      "border": {
        "$value": "#2A3654",
        "$type": "color"
      },
      "text": {
        "$value": "#C8CFDB",
        "$type": "color"
      },
      "icon": {
        "$value": "{primitives.slate.400}",
        "$type": "color"
      }
    },
    "diff": {
      "addBg": {
        "$value": "#0E2E1B",
        "$type": "color"
      },
      "addFg": {
        "$value": "#6FD995",
        "$type": "color"
      },
      "addMarker": {
        "$value": "{primitives.green.300}",
        "$type": "color"
      },
      "removeBg": {
        "$value": "#2E1010",
        "$type": "color"
      },
      "removeFg": {
        "$value": "#F58B8B",
        "$type": "color"
      },
      "removeMarker": {
        "$value": "{primitives.red.300}",
        "$type": "color"
      },
      "contextBg": {
        "$value": "#131B2C",
        "$type": "color"
      },
      "contextFg": {
        "$value": "#C8CFDB",
        "$type": "color"
      },
      "gutterBg": {
        "$value": "#0A101C",
        "$type": "color"
      },
      "gutterFg": {
        "$value": "{primitives.slate.400}",
        "$type": "color"
      }
    },
    "code": {
      "bg": {
        "$value": "#0A101C",
        "$type": "color"
      },
      "bgInline": {
        "$value": "#182135",
        "$type": "color"
      },
      "border": {
        "$value": "#2A3654",
        "$type": "color"
      },
      "fg": {
        "$value": "#F4F6FA",
        "$type": "color"
      }
    },
    "syntax": {
      "keyword": {
        "$value": "{primitives.indigo.200}",
        "$type": "color"
      },
      "string": {
        "$value": "#6FD995",
        "$type": "color"
      },
      "number": {
        "$value": "#F0B95B",
        "$type": "color"
      },
      "comment": {
        "$value": "{primitives.slate.400}",
        "$type": "color"
      },
      "operator": {
        "$value": "#C8CFDB",
        "$type": "color"
      },
      "function": {
        "$value": "{primitives.violet.200}",
        "$type": "color"
      },
      "punctuation": {
        "$value": "{primitives.slate.300}",
        "$type": "color"
      }
    },
    "approval": {
      "draftBg": {
        "$value": "#1C2640",
        "$type": "color"
      },
      "draftText": {
        "$value": "#C8CFDB",
        "$type": "color"
      },
      "awaitingBg": {
        "$value": "#2E2310",
        "$type": "color"
      },
      "awaitingText": {
        "$value": "#F0B95B",
        "$type": "color"
      },
      "approvedBg": {
        "$value": "#0E2E1B",
        "$type": "color"
      },
      "approvedText": {
        "$value": "#6FD995",
        "$type": "color"
      },
      "liveBg": {
        "$value": "#1F2A55",
        "$type": "color"
      },
      "liveText": {
        "$value": "#BCC4FF",
        "$type": "color"
      },
      "rejectedBg": {
        "$value": "#2E1010",
        "$type": "color"
      },
      "rejectedText": {
        "$value": "#F58B8B",
        "$type": "color"
      }
    }
  },
  "$schema": "https://design-tokens.github.io/community-group/format/2025-10/",
  "$description": "aiRA Design System First Edition \u2014 DTCG 2025.10 format. Agent identity tokens removed (rule: one aiRA)."
}

figma-components.md

aiRA DS v2 — Figma Component Specs

This file is the source of truth a Figma designer uses to draw v2 component frames. Component names below match CSS selectors in ../index.html 1:1, so Figma → code stays in sync.

How to use 1. Import tokens.json via the Tokens Studio plugin → tokens become Figma variables. 2. For each component below, create a Figma Component with the listed variants and properties. 3. Name the Figma component exactly as the Component name field — this matches code-connect.json. 4. Reference the live HTML demo in ../index.html for visual sizing.

All measurements in px. All colors reference Tokens Studio variables (e.g. ai.accent).

---

Tier 1 — Policy-critical components

---

1. ChatBubble

Component name: chat/Bubble Selector: .msg

Variants (Figma variant property):

  • role: user · ai · system · error
  • agent (ai only): config · qa · analytics · support · coder
  • state: default · hover · selected
  • density: comfortable · compact

Properties (Figma component properties):

  • Text → message body (instance swap, supports markdown)
  • Show meta → boolean (default true for ai/error)
  • Show actions → boolean
  • Timestamp → text

Anatomy (top → bottom): 1. Meta row (ai only) — Auto-layout horizontal, 8px gap, 4px bottom padding. - AgentBadge slot · AILabel · Timestamp (caption, tertiary) 2. Body — Max width 80%, padding 12/16, radius lg (12), one corner pinched to xs (4). - user: bg brand, text onBrand, right-pinch - ai: bg surface, text primary, border default, left-pinch - system: bg statusNeutral, text secondary, full radius md - error: bg state.aiError, border state.aiError, text state.aiError 3. Action row (optional) — 8px top gap, horizontal auto-layout. Copy · Support · Feedback group.

States | State | Treatment | |---|---| | default | base | | hover | actions reveal (opacity 0 → 1, 120ms) | | focus-visible | 2px ring border.focus, offset 2 | | streaming (ai) | trailing pulse dot, 1.2s ease loop |

A11y: list wrapper has aria-live="polite". Action buttons have aria-label. AI label is text + dot, never color only.

---

2. AILabel

Component name: ai/Label Selector: .ai-label

Variants:

  • size: inline (pill, 11/16 overline) · block (label, 12/16, full disclosure copy)

Anatomy:

  • Dot 6×6, fill ai.accent, 6px gap.
  • Text: ai.accent on ai.accentSurface, border ai.accentBorder.
  • Padding: inline 2/8, block 8/12. Radius pill for inline, md for block.

Mandatory use: on every AI-authored output (chat replies, generated configs, generated SQL, drafted copy).

---

3. ApprovalCard

Component name: approval/Card Selector: .approval

Variants:

  • status: draft · awaiting · approved · live · rejected
  • risk: standard · destructive (drives primary button variant)

Properties:

  • Title (header text)
  • Agent (AgentBadge swap)
  • Preview slot (instance swap: Diff, Table, Chart, RichText)

Anatomy (vertical auto-layout, radius lg, border ai.accentBorder): 1. Header — Padding 16/20, bg ai.accentSurface, border-bottom subtle. - AgentBadge · AILabel ("Proposal") · Title (h4, primary) 2. Body — Padding 16/20. - Framing line (body-md, secondary) - Preview slot 3. Footer — Padding 12/20, bg surfaceAlt, border-top subtle. - Status pill (left) · Spacer · Reject (tertiary) · Edit (secondary) · Accept (primary, right)

States | Status | Pill | Primary action | |---|---|---| | draft | Draft (slate) | Submit for review | | awaiting | Awaiting approval (amber) | Approve / Reject | | approved | Approved (green) | Make live | | live | Live (indigo) | (read-only) | | rejected | Rejected (red) | Revert to draft |

A11y: wrap as <article aria-labelledby>. For destructive risk, chain a Modal confirmation.

---

4. CitationChip

Component name: citation/Chip Selector: .citation

Variants:

  • mode: inline · listed
  • state: default · hover · focus

Properties:

  • Number (overline)
  • Title (label-sm)
  • Show snippet on hover (boolean — pairs with Popover)

Anatomy:

  • Padding 4/8, gap 6, radius sm.
  • Bg citation.bg, text citation.text, border citation.border.
  • Inline variant sits within prose; listed variant lives in a Sources block under the answer.

A11y: real anchor (<a href>) — never div onclick. Hover popover surfaces the cited snippet.

---

5. CodeBlock

Component name: code/Block Selector: .code-block

Variants:

  • lang: sql · javascript · python · yaml · text
  • state: default · collapsed
  • ai-labelled: true · false

Properties:

  • Code (text, monospace)
  • Lang (badge text)
  • Show Copy / Explain (boolean)

Anatomy:

  • Container: radius md, border code.border, bg code.bg.
  • Header bar: 8px padding, bg surfaceAlt, border-bottom. Lang overline (tertiary) · AILabel (optional) · spacer · Copy · Explain (tertiary buttons).
  • Body: padding 12/16, font code, color code.fg. Syntax token colors per v2-additions..syntax..

Collapsed: cap at 240px height with a "Show all" tertiary button below.

---

6. InlineDiff

Component name: diff/Inline Selector: .diff

Variants:

  • density: comfortable · compact
  • show-line-numbers: true · false

Anatomy (per line: 36px gutter + 1fr body):

  • Gutter: bg diff.gutterBg, fg diff.gutterFg, font overline, right-aligned.
  • Add line: bg diff.addBg, fg diff.addFg, gutter prefixed with +.
  • Remove line: bg diff.removeBg, fg diff.removeFg, gutter prefixed with −.
  • Context line: bg diff.contextBg, fg diff.contextFg.

Always pair sign + color — the + / − marker is the primary signal.

---

7. AgentActivity

Component name: activity/Row and activity/List Selector: .activity, .activity-list

Variants (Row):

  • state: thinking · streaming · stopped · error

Variants (List item):

  • state: pending · active · done · failed

Anatomy (Row):

  • Padding 8/12, radius md, bg state.aiThinking, border state.aiThinking, text state.aiThinking.
  • 14px spinner left, step text (medium), inline detail (tertiary).

Anatomy (List):

  • Each item: 20px status dot (pending: strong border, active: brand pulse, done: green fill, failed: red fill) + step name (label-md) + duration time (caption, right).

A11y: wrap in role="status" aria-live="polite". Honor prefers-reduced-motion (spinner stops, surface stays).

---

8. FeedbackControl

Component name: feedback/Control Selector: .feedback

Variants:

  • state: default · up-selected · down-selected · disabled
  • size: sm (24) · md (28) · lg (32)

Anatomy:

  • Inline group, 4px gap. Each button 28×28, radius sm, border default.
  • Up selected: bg statusSuccess, border green.300.
  • Down selected: bg statusDanger, border red.300.

A11y: role="group" + aria-label="Was this helpful?". Selected button uses aria-pressed="true". Down selection opens a reason picker Popover.

---

9. Modal

Component name: overlay/Modal Selector: .modal

Variants:

  • intent: confirmation · destructive · info · form
  • size: sm (440) · md (560) · lg (720)

Properties:

  • Title (h3, primary)
  • Body slot (instance swap)
  • Primary CTA (button)
  • Secondary CTA (button)

Anatomy:

  • Width per size, max 90vw. Radius xl (16). Shadow lg.
  • Header: padding 20, border-bottom subtle.
  • Body: padding 20, body-md text.
  • Footer: padding 12/20, bg surfaceAlt, border-top, actions right-aligned with 8px gap.

Scrim: bg.overlay (rgba slate-900 .48 light / black .60 dark).

A11y: role="dialog" aria-modal="true" aria-labelledby. Focus trap. Esc closes. Focus returns to trigger.

---

10. BottomSheet

Component name: overlay/BottomSheet Selector: .sheet

Variants:

  • size: peek (25%) · default (50%) · full (95%)
  • dismissible: true · false (drag handle visibility & scrim click)

Anatomy:

  • Width 100%, anchored bottom. Radius top 20px. Shadow lg. Bg surface.
  • Padding: 12/16/20 with bottom = 20 + safe.bottom.
  • Drag handle: 36×4, radius 999, bg border.strong, centered, 12px below the top edge.
  • Title h4 + body-sm description + actions row (right-aligned).

A11y: same as Modal. Handle is decorative — always include explicit Cancel / Apply buttons.

---

Tier 2 — General component coverage

---

11. Tabs

Component name: nav/Tabs Selector: .tabs

Variants: size: sm · md. style: line · contained. Anatomy: horizontal auto-layout, border-bottom default. Each tab: padding 12/12, 2px bottom border (transparent → border.focus when selected). Selected: text link. A11y: role="tablist", items role="tab" with aria-selected. Arrow-key nav.

---

12. Select

Component name: form/Select Selector: .field select

Variants: state: default · hover · focus · open · filled · disabled · error. Anatomy: 36px height, padding 8/12 (right 32 for chevron). Bg surface, border default, radius md. Chevron: two 5px slate-tertiary stripes.

---

13. Combobox

Component name: form/Combobox Selector: .combo

Variants: state: closed · open · loading · empty. Anatomy:

  • Input identical to Select sans chevron.
  • Menu (popover): bg surface, border default, radius md, shadow md, max-height 240, scrollable.
  • Item: 32px, padding 8/12, hover bg hover, selected bg selected. Optional sub-label (caption, right-aligned, tertiary).

A11y: role="combobox" + aria-expanded + aria-controls. Menu role="listbox". Items role="option" + aria-selected.

---

14. Toggle

Component name: form/Toggle Selector: .toggle

Variants: state: off · on · disabled · focused. Anatomy: Track 36×20, radius 999. Thumb 16×16, white, shadow xs. Off: bg slate.300. On: bg brand. Translate 16 on toggle, 200ms ease.

---

15. Checkbox

Component name: form/Checkbox Selector: .opt input[type=checkbox]

Variants: state: unchecked · checked · indeterminate · disabled · error. Anatomy: 18×18, radius xs. Unchecked: border default. Checked: bg brand, white tick. Indeterminate: bg brand, white horizontal bar.

---

16. RadioGroup

Component name: form/RadioGroup Selector: fieldset + .opt input[type=radio]

Variants (per option): state: unchecked · checked · disabled. Anatomy: 18×18 circle, radius 50%. Checked: 6px inner dot in brand. A11y: wrap in <fieldset> with <legend>. Arrow keys move between options.

---

17. Tooltip & Popover

Component name: overlay/Tooltip, overlay/Popover Selector: .tip, .popover

Tooltip:

  • Bg slate.900 (light) / slate.100 (dark). Text white/slate-900. Padding 6/8. Radius sm. Shadow md. Max-width 220.
  • Shown on hover AND focus.

Popover:

  • Bg surface, border default, radius md, shadow md, padding 12/16. Max-width 320.
  • Includes title (h4) + body (body-sm) + optional action row.
  • Behaves like a small dialog (role="dialog").

---

18. Avatar

Component name: avatar/Avatar Selector: .avatar

Variants:

  • size: sm (24) · md (32) · lg (44)
  • kind: user · ai · agent.config · agent.qa · agent.analytics · agent.support · agent.coder

Anatomy: circle, font label-md (sm: label-sm, lg: h4). user: bg brand, text onBrand. ai: gradient aira-gradient. agent.: bg matching agent. token.

---

19. ListRow

Component name: list/Row Selector: .list-row

Variants:

  • density: comfortable (min-height 44) · dense (min-height 36)
  • state: default · hover · selected · disabled

Anatomy: 3-column grid (auto · 1fr · auto), 12px gap, padding 12/16. Border-bottom subtle.

  • Leading slot (avatar/icon)
  • Content: title (label-md) over subtitle (caption, tertiary)
  • Trailing slot (timestamp / count / chevron)

A11y: entire row is one interactive surface (<a> or <button>); never split.

---

20. DataTable

Component name: data/Table Selector: .dt

Variants (cell): align: start · end. type: text · number · delta. Anatomy:

  • Header row: bg surfaceAlt, label-sm, text primary.
  • Row: body-sm, border-bottom tableDivider. Hover bg hover.
  • Number cells: tabular-nums, right-aligned.
  • Delta cells: positive uses delta.positive (green) with +; negative uses delta.negative (orange, NOT red) with −.

---

21. Stepper & Progress

Component names: progress/Stepper, progress/Bar Selectors: .stepper, .progress

Stepper variants (per step): state: pending · active · done · failed. Anatomy:

  • Each step: horizontal auto-layout, padding 8/12, flex 1, 2px bottom border.
  • Number circle 22×22 with label-sm digit. done: green ✓. active: brand. pending: slate.

Progress bar:

  • Height 6, radius 999, bg sunken. Fill brand, animated to width.
  • A11y: role="progressbar" + aria-valuemin/max/now.

---

Extras

Skeleton — loading/Skeleton. Selector .skeleton. Bg = animated gradient sunken → hover → sunken, 1.4s loop. aria-hidden="true". Variants: line · circle · block · bubble.

Form anatomy — composition pattern, not a component. Wrap fields in .field; label connected via for/id; helper text (caption) below; error text (caption, danger) replaces helper on aria-invalid="true"; required marker = visible asterisk + required attribute.

---

Naming convention

All Figma components are named category/Name (Pascal-cased name), e.g. chat/Bubble, overlay/Modal. The slash creates Figma's nested-component menu structure. The code-connect.json file pairs each name to its file path so Figma's Dev Mode can jump to source.

code-connect.json

{
  "$schema": "https://schemas.figma.com/code-connect/v1/code-connect.schema.json",
  "version": "1.0.0",
  "designSystem": "aiRA Design System v2",
  "sourceRoot": "../",
  "components": [
    {
      "figmaName": "chat/Bubble",
      "source": "index.html#chat-bubble",
      "selector": ".msg",
      "variants": ["role", "agent", "state", "density"],
      "props": ["text", "showMeta", "showActions", "timestamp"]
    },
    {
      "figmaName": "ai/Label",
      "source": "index.html#ai-label",
      "selector": ".ai-label",
      "variants": ["size"]
    },
    {
      "figmaName": "approval/Card",
      "source": "index.html#approval-card",
      "selector": ".approval",
      "variants": ["status", "risk"],
      "props": ["title", "agent", "previewSlot"]
    },
    {
      "figmaName": "citation/Chip",
      "source": "index.html#citation",
      "selector": ".citation",
      "variants": ["mode", "state"],
      "props": ["number", "title", "showSnippetOnHover"]
    },
    {
      "figmaName": "code/Block",
      "source": "index.html#code-block",
      "selector": ".code-block",
      "variants": ["lang", "state", "aiLabelled"],
      "props": ["code", "lang", "showCopyExplain"]
    },
    {
      "figmaName": "diff/Inline",
      "source": "index.html#diff",
      "selector": ".diff",
      "variants": ["density", "showLineNumbers"]
    },
    {
      "figmaName": "activity/Row",
      "source": "index.html#activity",
      "selector": ".activity",
      "variants": ["state"]
    },
    {
      "figmaName": "activity/List",
      "source": "index.html#activity",
      "selector": ".activity-list",
      "variants": ["itemState"]
    },
    {
      "figmaName": "feedback/Control",
      "source": "index.html#feedback",
      "selector": ".feedback",
      "variants": ["state", "size"]
    },
    {
      "figmaName": "overlay/Modal",
      "source": "index.html#modal",
      "selector": ".modal",
      "variants": ["intent", "size"],
      "props": ["title", "bodySlot", "primaryCta", "secondaryCta"]
    },
    {
      "figmaName": "overlay/BottomSheet",
      "source": "index.html#bottom-sheet",
      "selector": ".sheet",
      "variants": ["size", "dismissible"]
    },
    {
      "figmaName": "nav/Tabs",
      "source": "index.html#tabs",
      "selector": ".tabs",
      "variants": ["size", "style"]
    },
    {
      "figmaName": "form/Select",
      "source": "index.html#select",
      "selector": ".field select",
      "variants": ["state"]
    },
    {
      "figmaName": "form/Combobox",
      "source": "index.html#combobox",
      "selector": ".combo",
      "variants": ["state"]
    },
    {
      "figmaName": "form/Toggle",
      "source": "index.html#toggle",
      "selector": ".toggle",
      "variants": ["state"]
    },
    {
      "figmaName": "form/Checkbox",
      "source": "index.html#checkbox",
      "selector": ".opt input[type=checkbox]",
      "variants": ["state"]
    },
    {
      "figmaName": "form/RadioGroup",
      "source": "index.html#radio",
      "selector": "fieldset",
      "variants": ["optionState"]
    },
    {
      "figmaName": "overlay/Tooltip",
      "source": "index.html#tooltip",
      "selector": ".tip"
    },
    {
      "figmaName": "overlay/Popover",
      "source": "index.html#tooltip",
      "selector": ".popover"
    },
    {
      "figmaName": "avatar/Avatar",
      "source": "index.html#avatar",
      "selector": ".avatar",
      "variants": ["size", "kind"]
    },
    {
      "figmaName": "list/Row",
      "source": "index.html#list-row",
      "selector": ".list-row",
      "variants": ["density", "state"]
    },
    {
      "figmaName": "data/Table",
      "source": "index.html#data-table",
      "selector": ".dt",
      "variants": ["cellAlign", "cellType"]
    },
    {
      "figmaName": "progress/Stepper",
      "source": "index.html#stepper",
      "selector": ".stepper",
      "variants": ["stepState"]
    },
    {
      "figmaName": "progress/Bar",
      "source": "index.html#stepper",
      "selector": ".progress"
    },
    {
      "figmaName": "loading/Skeleton",
      "source": "index.html#skeleton",
      "selector": ".skeleton",
      "variants": ["shape"]
    }
  ],
  "tokens": {
    "format": "tokens-studio",
    "source": "tokens.json",
    "css": "../tokens.css"
  },
  "notes": [
    "Import tokens.json via the Tokens Studio Figma plugin to materialize variables/styles.",
    "Each figmaName above must be created as a Figma Component with the listed variants.",
    "After creating the components, run Figma → Plugins → Code Connect → Sync to bind them to this file.",
    "The figmaName slashes ('chat/Bubble') create Figma's nested-component menu structure."
  ]
}

Deferred

Work consciously left out of the First Edition. Each item has a trigger condition for re-opening.

ItemWhy deferredRe-open when
React component libraryBuilding it now would lock the system to a framework before the patterns are stable.Patterns hold for two consecutive product releases without breaking changes.
Cmd+K command palette + slash commandsThe command surface isn't designed yet (which commands, namespaced how, scoped to which agent).Product defines the command vocabulary.
Multi-brand theming hooksThe reseller/white-label contract isn't agreed yet. See Approval card · Multi-brand override path for the shape.First reseller deployment commits.
State-layer system (opacity-based hover/pressed)Token cost and complexity outweighs the benefit at the current system scale.System grows past ~80 components.
Density modes (compact / comfortable / spacious)Two density requirements would force every component to re-spec. Premature.A product asks for compact view in writing.
Responsive breakpoint tokensThe First Edition surfaces are desktop-first; mobile is a single break.Tablet or kiosk surface ships.
Style Dictionary export (JS / Swift / Kotlin)Only the web app consumes tokens today.Native client (iOS or Android) commits to the system.
i18n & bidi foundation pagesThe earlier standalone pages were dropped; the rule lives in tokens.css §7a but isn't documented.Next edition.