Calm by default. Unmistakable when it matters.
The v2 system keeps what people recognise in Talaria, near-black and white neutrals, the #0070F3 blue, Inter and JetBrains Mono. It fixes the parts that slowed triage down: colours that disagreed with each other, a flat type scale, missing interaction states, unlabelled navigation and patterns that changed from page to page. v2.1 makes the whole system responsive, from a 390 phone to a 1440 desk, and polishes the details. It's written for developers and for the coding agents that work beside them.
What's new in v2.1
Same visual language, now at every size, with a polish pass over the details that separate good from great.
Responsive by design
Four size classes (compact, medium, expanded, large) drive navigation, grid, type, density and every component. All mockups below are live layouts you can switch between Mobile, Tablet, Desktop and Wide.
Adaptive shell
Sidebar 240 / 56 on desktop, a labelled 72 rail on tablet, a bottom bar with More on phones. Env and project switchers become bottom sheets.
Touch-first below 900
44–48 px targets, 16 px input text, pressed states instead of hover, safe-area insets on bars, sheets and the save bar.
Polish pass
Optical icon strokes, a strict 4 pt rhythm (no more 14 px paddings), quieter dark shadows with a top highlight, HTML chart axes at 12 px, new empty-state art and a micro-interaction spec.
Tokens and Flutter
breakpoints, responsive, adaptiveType, touch and safeArea in tokens.json; TalariaBreakpoints, TalariaLayout, context.sizeClass and TalariaAdaptiveShell in Dart.
Still AA
Every text token keeps ≥ 4.5:1 in both themes, and touch targets now meet WCAG 2.2 target size (2.5.8) with room to spare.
Six rules we design by
When a decision is unclear, these settle it, in this order.
Triage comes first
Show the list of problems before any summary. Charts and insights support the list and never push it below the fold.
Colour carries meaning
Each hue family has one job. Blue is interaction and info, red is bad, amber is caution, green is good, magenta is fatal, iris is AI and teal is production. Decoration never borrows a status colour.
Quiet until it matters
Use neutral surfaces, hairline borders and no gradients except for AI. Each row gets at most one tinted badge, and every other attribute stays quiet text.
Legible at a glance
Text is at least 12px. Every text token passes WCAG AA on every surface in both themes. Numbers use tabular figures, and nothing relies on colour alone.
One way to do a thing
There's one page header, one back affordance, one filter bar, one save pattern and one empty state. A new page is built from these parts.
Built for you and your agent
Every token is machine-readable, every state can be deep-linked, and every widget renders on its own. An agent should be able to read the UI as easily as you do.
The fixes that matter most
Each pair puts the current production style (left, using the literal old values) beside the v2 replacement.
Three reds become one ramp plus fatal
Warning keeps its hue across themes
Error and fatal are clearly distinct
Type scale with real steps
Badge casing has one rule
Loading keeps width and label
Deltas are coloured by meaning
Environments are coloured and shaped
Colour
Primitive ramps are raw material, and components only ever read semantic or tone tokens. Neutrals are one ramp, the Tailwind-neutral greys the brand already uses, with three extra steps (450, 550, 875) added to make AA text possible. Click any swatch to copy its hex.
Primitive ramps tokens.json → primitive.color
Semantic tokens both themes · contrast against canvas / surface / muted
Ratios marked AAA are ≥ 7:1 and AA are ≥ 4.5:1. fg.disabled is intentionally below AA (disabled controls are exempt) but stays ≥ 2.2:1. Success fills never carry text.
Tone matrices bg / fg / border, rendered live
Colour rules
- Blue is for interaction (primary, links, selection, focus) and the info severity. It's never a status of "good".
- Red covers danger, error, down, regressed and bad deltas. It's a single ramp, with red.500 as the brand red.
- Amber covers warning and degraded. Text on an amber fill is always dark.
- Magenta is reserved for fatal and only fatal.
- Iris marks AI. It always appears with the sparkle glyph, and it's the only place a gradient is allowed.
- Teal and plum are environment identity (production and staging), shown as a dot plus a mono label.
- Tinted tones use soft bg, fg text and a border. Solid fills are only used for fatal, primary, danger and AI.
- Custom seed themes remap neutrals and accent only. Status, severity and environment tones stay fixed, so meaning never drifts.
Type scale
Inter for UI, JetBrains Mono for anything a machine wrote, such as frames, IDs, tags and env values. The scale follows roughly a 1.3 ratio from 15 to 32, with tracking tightening as size grows. Every number on screen uses tnum.
Space, radius, layout, elevation and motion
A 4pt grid with a half-step for icon nudges, five radii mapped to component size, three content widths chosen by page type, four elevation levels and six durations.
Spacing TalariaSpacing
Radius TalariaRadii
Nested radius = outer − padding. A card (8) holding a 4px-inset item uses 4.
Content widths
Gutters: 16 mobile · 24 tablet · 32 desktop. Each page type uses one width, and the header always aligns to the same column as its content.
Density row heights for tables and event or issue rows
Event streams, logs, tags, power users (toggle).
Tables, settings lists, API keys.
Issue list (the default). Never three lines, and never repeat the title as the message.
Elevation TalariaElevation · shadows deepen and surfaces lighten in dark
Motion TalariaMotion · hover a row to play
Micro-interactions tokens.json → interactions
| Element | Trigger | Change | Duration | Easing |
|---|---|---|---|---|
| Button, icon button, nav item, row | hover (pointer only) | state layer 0 → 5%/6% | fast 100 | standard |
| Button, icon button, nav item, row | press | state layer → 9%/10%; primary fill → accent.pressed; translateY 0.5px | fast 100 in / base 160 out | standard |
| Any focusable | keyboard focus | 2px ring + 2px gap, appears instantly; never animates layout | instant 0 | — |
| Checkbox, switch, segmented thumb, tab indicator | toggle / select | fill, thumb position, indicator x/width | base 160 | standard |
| Menu, popover, select | open / close | opacity 0→1, scale .98→1, y −4→0 / reverse | moderate 240 / fast 100 | enter / exit |
| Dialog, side drawer | open / close | scrim fade; panel y 8→0 (dialog) or x 24→0 (drawer) | slow 320 / moderate 240 | enter / exit |
| Bottom sheet | open / drag / dismiss | y 100%→0; follows finger; flings past 30% or 800px/s dismiss | slow 320 / moderate 240 | enter / exit |
| Toast | enter / exit | y 16→0 + fade / fade | moderate 240 / fast 100 | enter / exit |
| Skeleton | loading | shimmer sweep; appears after 150ms, stays ≥300ms | 1200 cycle | linear |
| Chart | first paint / range change | bars grow from baseline / cross-fade | deliberate 480 / base 160 | standard |
| Focus trail row | return from detail | 2px accent edge fades out | slow 320 after 2s | exit |
| Sidebar | collapse / expand | width 240↔56, labels fade at 40% | moderate 240 | standard |
Everything honours reduced motion: durations drop to 0 and only opacity changes remain.
Under prefers-reduced-motion (or MediaQuery.disableAnimations), positional motion becomes a 100ms cross-fade, the shimmer stops, and spinners keep turning at half speed so progress is still visible.
Icons and the navigation icon map
Outlined, 1.75 stroke on a 24 grid, rendered at 16 (inline), 18 (collapsed nav) or 20 (empty states). In Flutter, keep AppIcons as the only source and use the rounded outlined Material set. The glyphs here are drawn to match. Each destination gets exactly one icon, and no icon is reused for two destinations.
| Destination | Icon | AppIcons / Material | Was |
|---|---|---|---|
| Workspace | |||
| Projects | folder_outlined | folder ✓ | |
| Dashboards | space_dashboard_outlined | grid (dup) | |
| Groups | folder_copy_outlined | lock ✗ | |
| Team | group_outlined | group ✓ | |
| Billing | credit_card_outlined | card ✓ | |
| Integrations | power_outlined | hub | |
| AI chat | auto_awesome_outlined | sparkle ✓ | |
| Account | account_circle_outlined | gear (dup) | |
| Project · Observe | |||
| Overview | home_outlined | grid (dup) | |
| Issues | bug_report_outlined | gear-like | |
| Events | bolt_outlined | bolt ✓ | |
| Replays | play_circle_outline | play ✓ | |
| Whiteboards | draw_outlined | none ✗ | |
| Project · Analyse | |||
| Performance | speed_outlined | gauge ✓ | |
| Analytics | bar_chart_outlined | trend | |
| Releases | rocket_launch_outlined | tag (dup) ✗ | |
| Runtime | memory_outlined | tag (dup) ✗ | |
| Project · Configure | |||
| Alerts | notifications_active_outlined | bell (dup) ✗ | |
| Monitors | monitor_heart_outlined | bell (dup) ✗ | |
| Flags | flag_outlined | flag ✓ | |
| Settings | settings_outlined | tune | |
Responsive system
Layout is chosen by window width, not device. Four size classes map onto Material 3 window size classes: compact is touch-first with a bottom bar, medium keeps a labelled rail, expanded and large get the full sidebar and every column.
Layout per size class tokens.json → breakpoints, responsive
| Token · tokens.json → responsive | Compact | Medium | Expanded | Large |
|---|---|---|---|---|
| Columns · gutter · margin | 4 · 16 · 16 | 8 · 16 · 24 | 12 · 24 · 32 | 12 · 24 · 40 |
| Navigation | Bottom bar 64 (4 + More) · modal drawer | Rail 72, labelled · modal drawer | Sidebar 240, collapses to 56 with tooltips | Sidebar 240, collapses to 56 with tooltips |
| Top bar | 56 px · back, title, env, search | 56 px · crumbs, env, ⌘K | 48 px · crumbs, env, ⌘K | 48 px · crumbs, env, ⌘K |
| Content width | fluid | fluid | max standard 1200 | max wide 1440 |
| Input · density | touch · comfortable | touch · comfortable | pointer · standard | pointer · standard |
| Tables | Stacked cards | Fewer columns (no sparkline) | All columns | All columns |
| Filters | Search + Filters (n) → sheet | Inline search + Filters (n) → sheet | Inline chips | Inline chips |
| Detail rail | Status + assignee, rest collapsed | Card above content, 2 columns | Side, 300 | Side, 340 |
| Dialogs | fullscreen | bottomSheet | centered 560 | bottomSheet (pickers) | centered 440-560 | centered 440-560 |
| Drawers | fullscreen | side 400 (>=720) | fullscreen | side 400-480 | side 480 |
| Pagination | Load more | Load more | Numbered pages | Numbered pages |
| Header actions shown | 1 + overflow menu | 2 + overflow menu | 3 + overflow menu | 3 + overflow menu |
| Chart ticks (x / y) | 3 / 3 · swipe | 5 / 4 | 6 / 5 | 8 / 5 |
Grid 4 · 8 · 12 columns
4 columns · 16 gutter · 16 margin
8 columns · 16 gutter · 24 margin
12 columns · 24 gutter · 32 margin
Adaptive type tokens.json → adaptiveType
| Style | Compact | Medium | Expanded | Large |
|---|---|---|---|---|
| display | Page not found 28/36 | Page not found 30/38 | Page not found 32/40 | Page not found 36/44 |
| headline | Issues 22/28 | Issues 24/32 | Issues 24/32 | Issues 24/32 |
| heading | Needs attention 17/24 | Needs attention 18/26 | Needs attention 18/26 | Needs attention 18/26 |
| metric | 1,284 24/28 | 1,284 26/32 | 1,284 28/32 | 1,284 28/32 |
Title, Body, Label and Caption never change, so dense data reads the same on every device. In Flutter: context.tTextAdaptive or TalariaTextStyles.forSizeClass(c).
Touch, pointer and safe areas tokens.json → touch, safeArea
Control heights
- Minimum target
- 48 × 48 (44 on iOS)
- Row height
- pointer 48 · touch 56 · card 72
- Nav item
- sidebar 32 · rail 56 · bottom bar 64
- Input text
- 16 px on touch
- Safe area
- Pad top app bar by the top inset, bottom bar / sheets / save bar / AI composer by the bottom inset; landscape phones also pad left/right.
Pointer vs touch
- Hover styles only apply under (hover: hover) and (pointer: fine). On touch, the pressed state layer (10%) is the only feedback.
- Never hide actions behind hover on touch: show an overflow button or support long-press and swipe.
- Tooltips need a long-press equivalent; icon-only buttons keep a visible label in the bottom bar and rail.
- Inputs use 16px text on touch so iOS Safari never zooms on focus (web build).
- Hit area is at least 48x48 (44x44 on iOS) even when the visual control is smaller; adjacent targets keep 8px between hit areas.
Components at each breakpoint live container queries, true pixel widths
Each box is a real container at the stated width, so what you see is exactly what the layout rules produce.
Filter bar
The bar never wraps into three lines. Below 900 the chips collapse into one Filters button with a count.
- ExpandedSearch, status segments, chips, time range, sort
- MediumSearch + status + Filters (n) sheet
- CompactSearch + Filters (n); status scrolls below
Issue list → cards
One markup, three layouts. Counts keep their labels when columns disappear.
- ExpandedAll columns, sparkline, pages
- MediumSparkline column hidden, 64 rows, load more
- CompactTwo-line cards: title, message, meta wraps; events on the right
Page header and tabs
Primary action stays visible; the rest overflow into ⋯. Tabs scroll with an edge fade instead of wrapping.
- ExpandedInvestigate, Resolve, ⋯
- MediumSame, culprit hidden
- CompactBack lives in the app bar; Resolve fills the row; AI becomes an icon
Detail rail
The rail is a side column on desktop, a two-column card on tablet and a short summary on phones.
- ExpandedSide rail 300 (340 wide)
- MediumCard above the stack trace
- CompactStatus + assignee, then More details
Charts
Axis text stays 12 px at every width; compact shows 3 ticks and scrolls with the newest day pinned right.
- Expanded6 x-ticks, tooltip on hover
- Medium5 ticks, tap for tooltip
- Compact3 ticks, swipe for history
Events by severity
Events by severity
Events by severity
Billing plans
Plans sit side by side from 900 and stack below, with the recommended plan's primary button always last in reading order.
- Expanded3 columns
- MediumStacked, full width
- CompactStacked, full width
- 5k events
- 1 project
- 50k events, 500 replays
- 5 projects
- 500k events, 5k replays
- SSO, audit log
- 5k events
- 1 project
- 50k events, 500 replays
- 5 projects
- 500k events, 5k replays
- SSO, audit log
- 5k events
- 1 project
- 50k events, 500 replays
- 5 projects
- 500k events, 5k replays
- SSO, audit log
Confirm dialog → bottom sheet
Same content and button order. On phones the primary action is on top, within thumb reach.
- ExpandedCentred 440 dialog
- MediumCentred dialog
- CompactBottom sheet, safe-area padded
Resolve 3 issues?
They reopen automatically if a new event arrives in a later release. You can undo this for 8 seconds.
Resolve 3 issues?
They reopen automatically if a new event arrives in a later release. You can undo this for 8 seconds.
Resolve 3 issues?
They reopen automatically if a new event arrives in a later release. You can undo this for 8 seconds.
Toast
Bottom-centre on desktop and tablet; on phones it spans the width just above the bottom bar.
- ExpandedBottom-centre, min 320
- MediumBottom-centre
- Compact16 inset, 12 above the nav
AI drawer
A 420 side panel from 720 up, full-screen below so the answer and composer get the whole phone.
- ExpandedSide panel 420 over a scrim
- MediumSide panel 420
- CompactFull screen, composer above the home indicator
One page, four sizes
Switch pages to compare. Nothing here is a separate design: each frame is the same component tree at a different width.
Buttons and icon buttons
Five variants, three sizes and six states. Hover and pressed are state layers (fg at 5% or 9%) over the resting fill, so they work on any surface and in custom seed themes. Focus is a 2px ring with a 2px canvas gap that never moves the layout. Loading locks the width and keeps the label.
| Default | Hover | Pressed | Focus | Disabled | Loading | |
|---|---|---|---|---|---|---|
| Primary | Resolve | Resolve | Resolve | Resolve | Resolving… | |
| Secondary | Assign | Assign | Assign | Assign | Assigning… | |
| Ghost | View trace | View trace | View trace | View trace | Loading… | |
| Danger | Delete | Delete | Delete | Delete | Deleting… | |
| Danger quiet | Revoke | Revoke | Revoke | Revoke | Revoking… | |
| AI | Investigate | Investigate | Investigate | Investigate | Investigating… |
sm for tables, toolbars and dense rows, md as the default, lg for empty states and onboarding. The hit target is always ≥ 44px using an invisible extension.
A split button handles "Resolve in next release". Overflow goes to "More" and keeps the same order as the visible actions.
The visual size is 32 (24 for sm) and the hit target is 44. Idle uses fg.subtle and hover uses fg.default. A tooltip on hover and focus is mandatory.
One primary per view region.
Competing fills. Archive isn't destructive.
Text inputs, number inputs and select
Filled with bg.muted and bordered with border.strong. Focus switches to the surface fill with a 1px focus border and a 3px soft ring. Labels sit above, and helper or error text sits below. Error text always comes with an icon, never colour alone.
A preset writes range=7d to the URL, never "time:custom".
Checkbox, radio, toggle, segmented control and tabs
Selected states use the accent fill, and hover adds a 4px state-layer halo. Toggles take effect immediately. A form's checkboxes wait for Save. Status is never shown as green text such as "Enabled".
| Off | On | Indeterminate | Hover | Focus | Disabled | Error | |
|---|---|---|---|---|---|---|---|
| Checkbox | Unhandled | Unhandled | 3 of 8 | Hover | Focus | Locked | Required |
| Radio | Slack | — | Hover | Focus | Disabled | Pick one | |
| Toggle | Live | Live | — | Hover | Focus | Disabled | — |
Use 2–5 mutually exclusive options. The thumb animates at base 160ms. Never use it for navigation.
Tabs switch views of one object and are reflected in the URL (?tab=events). A 2px fg indicator, not accent, keeps the blue for links.
Severity, status, environment and health
Every badge pairs a glyph with a label, so meaning never depends on colour. They're 20px high, use 12/500 text and Sentence case, with no uppercase. A row carries at most one tinted badge, and the others become quiet text or icons.
Unknown envs fall back to the neutral tone with a hollow dot. Production is the only teal.
- Semantic labels use Sentence case: "Resolved", "Third party", "Over limit".
- Literal values (env, tag, release, slug) use mono, verbatim: production.
- Never uppercase. The uppercase flag is removed from AppBadge.
- Badges are not buttons. Interactive chips use the filter-chip pattern.
Cards and tables
Cards are flat (e0) with a hairline border. Interactive cards gain border.strong and e1 on hover. A table row colours only its badges, never whole cells. Numbers align right and use tabular figures.
Latest release
checkout-api
Monitor · nightly-backup
Table default density · sortable header · selected and focus rows
Header uses captionStrong in fg.subtle and is sticky. Hover adds a state layer, selected uses state.selected, and keyboard focus draws a 2px inset bar. Never tint a whole cell.
Issue rows and event rows
An issue row is two lines at most. The first line is the type and message. The second line is the culprit, environment, release and age. A 3px left edge carries severity, status is an icon, and volume is a sparkline plus tabular counts. An event row is a single 40px line.
Event rows 40px · one line · timestamp, level, environment, message
Tags move to the detail view. In the old event row a single event took around 95px and three lines, repeating the title as the message.
One filter bar for every list
Issues, Events, Replays, Traces and Releases share one component: a search field, a status segmented control (where it applies), token chips (key, operator, value) and "Add filter", with time range and saved views on the right. Every change writes to the URL immediately using replace, never push, so Back works in one press.
No issues match these filters
3 filters are hiding 41 issues from the last 7 days.
One page header, one way back
Breadcrumbs in the top bar say where you are. The page header says what this is and what you can do. Detail pages get a single "← Parent" back link above the title, which replaces the focus bar, the extra breadcrumb and the X.
Issues
TimeoutError
Banners and toasts
Banners are persistent, inline and scoped to a page or card. Toasts are transient (5s, or 8s with an action), confirm something you did, and always offer Undo for reversible bulk actions. Errors that need action are never toasts.
Empty, error, 404 and skeletons
Every state names the cause and offers a real next step. Empty is not error, and error is not not-found. A 404 renders inside the app shell so navigation still works. Raw exceptions are never shown. A request ID sits behind "Details".
No replays yet
Session replay is off for this project. Turn it on to see what users did before an error.
Couldn't load issues
The Talaria API didn't respond in time. Your data is safe, and this is usually brief.
This issue doesn't exist
It may have been merged or deleted, or the link has a typo. No retry needed.
404, in the shell
There's no page at this address in Northline. Did you mean Performance › Vitals?
Skeletons shape matches the content · shimmer 1.2s · appear after 150ms · never mix spinners and skeletons in one region
- Skeletons mirror the final layout box for box, so nothing shifts when data lands. The issue detail page no longer fills in piecemeal.
- Show them only after a 150ms delay to avoid flashes, and keep them for at least 300ms once shown.
- Refetching keeps the stale data in place with a 2px progress line at the top of the region, not a skeleton.
- Buttons show a spinner and keep their label. Whole-page cold loads use the blade loader.
Stat tiles, deltas and the chart palette
Every tile has the same anatomy: label, value, delta and comparison, with a fixed height so a row of tiles always lines up. Delta colour comes from polarity. An empty or tiny baseline shows "—" or an absolute change rather than an alarming percentage.
Events by severity
p95 latency by route
Chart palette ChartPalette · Okabe–Ito derived · re-tuned per theme
Plan cards and usage meters
The current plan has a strong border and a "Current plan" badge. The recommended plan has the accent ring and the only primary button. Others use secondary buttons, so the outline and primary styles no longer clash. Prices use display with tabular figures.
- 5k events
- 1 project
- MCP for agents
- 50k events, 500 replays
- 5 projects, unlimited seats
- 100 AI investigations
- 500k events, 5k replays
- SSO, audit log
- PII scrubbing rules
- Volume pricing
- Data residency (NZ, AU)
- Dedicated support
Usage this period
AI surfaces
AI has its own tokens: ai.default, ai.fg, ai.soft, ai.border and a two-stop gradient. It's the only gradient in the product. AI output is always labelled, cites its sources and offers a way to verify it. Markdown uses Talaria tokens rather than Material #2196F3 and #BBDEFB.
submitOrder…Likely cause: SessionStore.refresh() returns before the new token is written, so submitOrder reads a null session for about 300ms after a refresh. This started in 1.4.2, which removed an await.
One AI button style everywhere, replacing the earlier one-off "Investigate with AI" and "Explain with AI" styles.
Dialogs, drawers and the focus trail
Dialogs are for decisions. Drawers are for context you look at alongside the page. The focus trail stacks drawers, so you can go from an issue to an event to a trace and back without losing the list underneath. All overlays trap focus, close on Esc, return focus to the element that opened them and sit at e3 over the scrim.
Issues list, triage first
The first issue starts about 200px from the top of the page, not at 740px. Insights fold into a single strip that can be expanded, and the chart and "Top issues" move into it. Every filter lives in the URL. Switch the viewport to see it at 390, 820, 1280 and 1440.
Issue detail
One back link, one header and tabs for the views. The main column shows the exception with your own frames expanded and library frames folded. The 300px rail (340 at wide) holds state and facts. Skeletons match this layout, so it never fills in piecemeal. Switch the viewport to see it at 390, 820, 1280 and 1440.
Project overview
One row of equal-height tiles, a severity chart with a prior-period ghost line, and "Needs attention" ranked by impact rather than raw count. Empty metrics say why they're empty. Switch the viewport to see it at 390, 820, 1280 and 1440.
Settings: sections, save bar and danger zone
Settings pages use the narrow 720 column with a left sub-nav. Each section has a label column and a field column. Changes collect in a sticky save bar that appears only when the form is dirty. Destructive actions sit in a separate danger zone at the very bottom, each with its own confirmation dialog. Switch the viewport to see it at 390, 820, 1280 and 1440.
Pages on phone and tablet
The four page templates at 390 and 820, plus the phone-only flows: bottom sheets for pickers and filters, full-screen dialogs for forms, the More drawer and toasts that clear the bottom bar.
Issues list Cards, Filters (n) sheet, load more
Issue detail Back in the app bar, rail summary, scrolling tabs
Project overview 2-up tiles, swipeable chart
Settings Scrolling section tabs, single-column form, floating save bar
Phone flows sheets, full-screen dialogs, drawers and toasts
Accessibility and voice
The baseline is WCAG 2.2 AA. Flutter web semantics are on by default, not hidden behind "Tab → Enable accessibility".
Perceivable
- All text tokens are ≥ 4.5:1 on canvas, surface, muted and raised.
- Focus ring is ≥ 3:1 against adjacent colours (4.36 light, 7.02 dark).
- Severity, status and env carry a glyph as well as colour.
- Charts include an aria summary and a "View as table" option.
Operable
- Hit targets are ≥ 44×44, including icon buttons and chips.
- Tooltips show on hover and focus, and dismiss with Esc.
- Every list has keyboard navigation. Every overlay traps and restores focus.
- Reduced motion is respected throughout.
Voice (NZ English)
- Plain, concrete and dry. Use "colour", "organisation", "analyse" and "behaviour".
- Use Sentence case everywhere. Buttons are verbs ("Resolve", "Send fix").
- Errors say what happened, then what to do. Don't blame the user or the time range when a filter is the cause.
- Dates read "11 Oct 2026" and times read "14:02 NZDT".
Talaria Design System v2.1 · tokens.json is the source of truth · Made in Aotearoa New Zealand