# Swivel Design System > The design system for Swivel, the command center for modern ad operations. One stylesheet > carries the brand fonts, every token, and every component class. The system is dark by > default; light is an opt-in. > > - Human reference: https://design-system.swivel-dev.net/ > - Drop-in stylesheet: https://design-system.swivel-dev.net/stylesheet.css > - This file: https://design-system.swivel-dev.net/llms.txt ## Quick start Link one file. Nothing else is required for tokens, type, and component classes. ```html

A million decisionS a minute.

You set the rules. Swivel executes against them.

``` `stylesheet.css` already contains: - `@font-face` for **Avantt** (display, 600/700) and **Antarctica Condensed** (body, 400/500 plus italics), served from https://design-system.swivel-dev.net/fonts/ with CORS enabled, so it works from any origin. - JetBrains Mono (code and IDs) and Material Symbols Outlined (product icons) from Google Fonts. - Every token as a CSS custom property, in both themes. - Every component class (`.btn`, `.field`, `.chip`, `.table-card`, `.app-shell`, ...). Do not add Inter, Roboto, or any other family. Do not add a second icon font in product UI. ### React components without a build step The React components are compiled into a browser bundle that registers them on `window.SwivelDesignSystem_498725`. Load React 18 globals first. ```html ``` ## Theming **Dark is the default.** With no attribute set, every token resolves to its dark value. | You want | Set | |---|---| | Dark (default) | nothing, or `` | | Light | `` | | A dark island inside a light page | `data-theme="dark"` on that element | | A light island inside a dark page | `data-theme="light"` on that element | Rules that are not optional: 1. **Every artifact ships a working light/dark switch.** Never a decorative or stubbed toggle. 2. It is the canonical `.theme-switch` (sun icon, track, moon icon, no text label). It is a switch, never a button group. `aria-checked="true"` means dark. 3. Persist the choice in `localStorage` under `swivel-theme`. Apply it before first paint with an inline script in `` so there is no flash. First visit with nothing stored is dark. 4. Build exclusively from tokens (`var(--surface-base)`, `var(--graphite-90)`). A hard-coded hex breaks one of the two themes. 5. Verify both themes before you finish. ```html ``` Drop `.is-fixed` to place the switch inline in a header or sidebar instead of pinned top-right. ### How the token names behave across themes Token names describe a **role**, and the ramp inverts between themes so the role holds. `--graphite-90` is the primary text color in both themes: near-black `#131514` in light, near-white `#F2F3F2` in dark. `--surface-base` is the card surface in both. Use the role; never pick a ramp step because of how it looks in one theme. ## Brand foundations ### Color Four canonical colors. No other hues. No gradients unless derived from the palette. No warm tones outside data and status indicators. | Token | Hex | Role | |---|---|---| | `--switch-100` | `#A8F8FF` | Primary accent. Energy, CTAs, the "live" color. | | `--graphite-base` | `#131514` | Dark backgrounds. Primary text on light. | | `--slate-100` | `#637D9C` | Supporting elements and secondary text. Mid-tone, never dominant. | | `--white` | `#FFFFFF` | Light backgrounds. Reverse text. | Approved pairings: Graphite background + Switch accent + white text (product default); white background + Graphite text + Switch accent (light marketing); Switch background + Graphite text (high-contrast call-out blocks). Status colors (`--success`, `--warning`, `--info`, `--error`, each with `-bg` and `-fg`) mark **data states only**, never decoration. ### Type | Role | Family | Token | |---|---|---| | Headlines, display | Avantt (600, 700) | `--font-display` | | Body, UI | Antarctica Condensed (400, 500, italics) | `--font-body` | | Code, IDs, data | JetBrains Mono | `--font-mono` | Type classes: `.t-display`, `.t-h1` to `.t-h4`, `.t-body-l`, `.t-body`, `.t-body-s`, `.t-body-label`, `.t-body-caption`. - Every headline ends with a period. That period is the brand signature. - Mixed case. A single stylized capital inside a word ("decisionS") is allowed on display moments only, never in product UI headings. - Never fully uppercase headlines outside display moments. No ALL CAPS body. ### Spacing, radius, elevation - 4px base: `--space-sm` 4, `--space-md` 8, `--space-lg` 12, `--space-xl` 16, `--space-xxl` 20, `--space-3xl` 24, `--space-4xl` 32, `--space-5xl` 40, `--space-section` 64. - Radii lean sharp: `--radius-xs` 2, `--radius-sm` 4 (default UI), `--radius-md` 6, `--radius-lg` 8 (max on cards), `--radius-xl` 12, `--radius-full` for pills. - Shadows are minimal. `--elev-1` to `--elev-3` and `--elev-modal` are for floating layers only (menus, modals, popovers). Inline cards get a 1px `--surface-stroke` border, no shadow. - Control heights: `--control-h-sm` 24px, `--control-h` 34px, `--control-h-lg` 40px. ### Motion Lines and arrows in motion, angular geometry, directional energy. Sharp transitions, no bounce, no springs, no parallax. - Durations: `--motion-instant` 80ms (press), `--motion-fast` 120ms (hover, focus), `--motion-base` 200ms (default), `--motion-slow` 240ms (expand, drawer), `--motion-deliberate` 360ms (attention reveals). - Easings: `--ease-standard` (default), `--ease-emphasized` (large surfaces), `--ease-enter`, `--ease-exit`, `--ease-linear`. - Press is `scale(0.98)` over 80ms with no color flicker. - Respect `prefers-reduced-motion`. ### Interaction states - Focus: `box-shadow: var(--focus-ring)` (the Switch glow). Never remove a focus ring. - Hover on a dark surface lifts one graphite step; Switch text moves to `--switch-contrast`. - Disabled: `.is-disabled` on the control, plus the native `disabled` attribute on inputs. ### Iconography - **Product UI:** Material Symbols Outlined, `bolt`. Outline by default, filled only for a selected or active state. 16px in body, 20px in nav, 24px in headers. Approved subset: https://design-system.swivel-dev.net/design-system/icons.json. - **Marketing:** Lucide at 1.5px stroke. Never mix the two families in one surface. - Unicode arrows (→ ↑ ↗ ↘) are brand elements in metric readouts and CTAs, at headline weight. - No emoji. No multi-color icons. - Every icon-only control carries a tooltip (`data-tooltip`, a verb phrase) and an `aria-label`. ### Logo Logos live under https://design-system.swivel-dev.net/assets/logo/. Do not alter, rotate, recolor, or add effects. - `swivel-lockup-switch.svg`, `swivel-lockup-white.svg`, `swivel-lockup-graphite.svg`, ... - `swivel-symbol-switch.svg`, `swivel-symbol-white.svg`, `swivel-symbol-graphite.svg`, ... ## Component rules - **Button groups filter or segment. They never take actions.** Anything that saves, runs, deletes, or opens is a `Button`, `IconButton`, or `SplitButton`. - **There are no radio buttons.** A single choice is a `ButtonGroup` (2 to 5 short options) or a `SelectField` (6 or more, or long labels). - **Light/dark is `.theme-switch`.** Any other binary setting that applies immediately is `.switch`. A value submitted with a form is a `Checkbox`. - **Tables are always `.table-card` + `.table-toolbar` + `.table` + `.table-pagination`.** Never a bare ``. - **A count opens its records in place.** When a row counts child objects (demand tags, creatives, packages), the count is a `.count-btn`; clicking it opens a sub-table directly beneath the row. One level, one sub-table per row, at most 10 rows plus a "View all" link. Never a chevron column, a row click, a Drawer, or a Modal. Full contract under Table → Expandable rows below. - **Async regions have three states:** Skeleton shaped like the content while loading, EmptyState when nothing came back, ErrorState with a retry on failure. Never a blank card. - **Every product page sits in `.app-shell`**: `TopHeader` pinned on top, `Sidenav` and the scrolling `main` in the row below. ### Choosing a component **binary input** - A value submitted with a form or list — multi-select, table row selection, an opt-in → **Checkbox**. Not: Switch — a checkbox is pending until the form is saved - A setting that takes effect the moment it is flipped → **Switch**. Not: Checkbox — a switch has no save step - Light / dark mode → **ThemeSwitch**. Not: Switch or a ButtonGroup — this control is canonical and must actually work **single choice** - Two to five short options, all visible, used to filter or segment a view → **ButtonGroup**. Not: A SelectField — hiding a short filter set behind a menu costs a click for nothing - Six or more options, or long labels → **SelectField**. Not: A ButtonGroup — it will wrap and lose its segmented read - Anything that saves, runs, deletes, or navigates → **Button / IconButton / SplitButton**. Not: A ButtonGroup — it is selection only, never an action - A single choice from a fixed small set in a form → **ButtonGroup or SelectField**. Not: Radio buttons — this system has none **multi value input** - Pick several from a known list → **SelectField multi (chips)**. Not: A count label like "3 selected" — the values must stay visible - Type arbitrary values — IDs, keywords, emails → **TagInput**. Not: A SelectField — there is no list to pick from - A bare option list inside a popover you own → **SelectMenu**. Not: SelectField — that is the field plus this menu - A prose answer — a note, a description, a reason → **Textarea**. Not: TextField — a single-line input hides everything past the first line **navigation** - Get back up the hierarchy from a detail page → **Breadcrumb, above the page h1**. Not: A back link — a trail shows where you are, not just where you came from. And never a heading-scale trail: the breadcrumb never replaces the

. - Switch between views of the same object → **Tabs**. Not: A ButtonGroup — tabs swap a panel, a group filters a list - Move between top-level areas of the product → **Sidenav**. Not: Tabs **status and feedback** - A persistent state word on a row or object → **Badge**. Not: Chip — a chip is a value the user put there, a badge is a state the system reports - A value the user selected or entered, removable → **Chip**. Not: Badge - A receipt for something the user already did — saved, paused, archived → **Toast (useToasts + ToastRegion)**. Not: An inline Alert — it would persist on a page the user is done with - Anything the user must read, act on, or refer back to → **Alert, inline where it applies**. Not: A Toast — it disappears, so the task becomes unfinishable - A message about the page or form, staying put → **Alert**. Not: A Modal — do not block the page for information - Extra detail on hover for an icon-only control → **Tooltip / data-tip**. Not: Help text — and never a tooltip for information the user needs to complete a field **containers** - Group related content on a page → **Card**. Not: A bare div — surface, stroke, and radius are contracted - A decision that must be answered before continuing → **Modal**. Not: Drawer - Secondary or ephemeral UI beside the page — filters, detail, settings → **Drawer**. Not: Modal — a drawer does not block, and never holds primary navigation - A titled block the user can collapse → **Section**. Not: A hand-rolled card-header with a chevron icon you swap on click **async regions** - Data is loading → **Skeleton, shaped like the content that will land**. Not: A spinner or a blank card - The query returned nothing → **EmptyState**. Not: A blank card - The request failed → **ErrorState with a retry**. Not: EmptyState — the user needs to know it can be retried **tables** - Any list of records → **Table (.table-card + .table-toolbar + .table + Pagination)**. Not: A bare

- Row count and page navigation → **Pagination**. Not: Hand-rolled markup — it exists as a component - A count on a list that is not paged → **Pagination countOnly**. Not: A loose - See the child records behind a count in a row (demand tags, creatives, packages) without leaving the list → **Count expander: .count-btn opening a sub-table beneath the row (Table.expandable_rows)**. Not: A chevron column, a click-anywhere row, a Drawer, or a Modal. The count is the way in, and the children stay in the context of their parent **data viz** - A continuous metric over time → **Chart type="line"**. Not: Bars — they compare categories, not a trajectory - Magnitude over time where the total matters more than the rate of change → **Chart type="area"**. Not: line — the fill is the point - Ranked categories, "top N" → **Chart type="bar-horizontal", sorted by value**. Not: A vertical bar chart — long labels do not fit under vertical bars - Part of a whole where the total is also interesting → **Chart type="bar" stacked, six slices maximum**. Not: A pie chart — this system has none - One dominant percentage or count with a few supporting slices → **Donut with a centre anchor, four slices maximum**. Not: A pie — the anchor is what justifies the format - Actual versus target → **Bullet**. Not: A gauge — it reads angle instead of length - How far along something is against a known whole — delivery, an upload, an export → **ProgressBar**. Not: Bullet — that is actual against a target with qualitative bands - Actual against a target where the bands matter (behind / on / ahead) → **Bullet**. Not: ProgressBar — it carries one reference line, not a qualitative range - Where a flight is in its window, and which days it went dark → **FlightProgress**. Not: ProgressBar — it measures one quantity and cannot show gaps - How much of a goal has been delivered → **ProgressBar**. Not: FlightProgress — that measures elapsed time, which is not the same as delivery - Pacing in a table column → **ProgressCell**. Not: A bare percentage — the bar makes the row scannable at a glance - One number on a dashboard → **KpiCard**. Not: A chart with one point - Trend beside a number, in a table cell or a card → **Sparkline**. Not: A full Chart — no axes fit at that size - More than three series → **Small multiples**. Not: Stacking them on one plot - Axis labels on a plot you built yourself → **ChartFormat.axis**. Not: Hand-formatted strings — the currency, suffix, and precision rules are easy to half-apply **dates** - A date that is a property of the object being edited → **DateField (default, stacked label)**. Not: variant="inset-label" — a stacked label reads as a form field being filled in - A relative range — last 7 days, this week → **DatePicker mode="range" presets**. Not: Making the user count backwards from today in the grid - A date that changes what a table or list shows → **DateField variant="inset-label"**. Not: The stacked default — an inset label reads as a control acting on the view, and fits a --control-h toolbar row - A single date the user types more often than picks → **DateField, with onTrailingClick opening a DatePicker**. Not: A bare TextField — the calendar affordance is what says this is a date - A flight, campaign window, or reporting period → **DatePicker mode="range" — two months, ribbon fill**. Not: Two separate single pickers — the user cannot see the span - A time of day, usually a round one → **TimePicker (dial, the default)**. Not: A TextField — the dial is the M3 spec and handles the meridiem - A time needing exact minutes, or a time field in a dense form → **TimePicker defaultMode="input"**. Not: A bare TextField — the toggle lets the user go back to the dial - Anything about picker behaviour the components do not cover → **The Material Design 3 spec, then recolour through the --md-* tokens**. Not: Inventing a Swivel-specific interaction — the policy is M3 structure, Swivel colour only **activity** - What a user or agent did, with the ability to see what changed → **ActivityPanel**. Not: A Table — it cannot carry a nested before/after diff, and it offers sorting and selection an audit log does not have - A record that was created → **detail mode 'value'**. Not: mode 'diff' with empty befores — there was no previous value - A record that was changed → **detail mode 'diff'**. Not: mode 'value' showing only the new state — the previous value is why the row is opened - Filters for an activity stream → **.filter-rail beside the stream — date range, user select, grouped checkbox facets**. Not: A filter bar above it: four axes either wrap or push the first event below the fold - An event with nothing to expand → **Omit detail; the chevron disappears**. Not: An empty detail table **table cells** - A record identifier → **kind id — .cell-id with an .id-chip, hugging**. Not: Plain text in a stretched column - A number to compare down the column → **kind count or money — .cell-num header, .cell-metric body, right-aligned**. Not: Left-aligned text; digits only line up when right-aligned - A status or lifecycle state → **kind state — one filled 18px icon with a hover label**. Not: A Badge per row; a column of pills is a wall - A date or timestamp → **kind date — .cell-date, --graphite-bg at 13px**. Not: Body size at full strength, which competes with the row name - Position in a fixed set of stages → **kind lifecycle — .lifecycle segments**. Not: ProgressBar; the stages are countable - Delivery or pacing → **kind progress — ProgressCell with the figures in a tooltip**. Not: A bare bar - A per-row action → **kind actions — last column, .cell-actions, revealed on hover**. Not: A column of always-visible glyphs, or three actions inline - A count that opens a sub-list → **kind children — .children-stamp, centred**. Not: A bare number, which reads as a metric - A count the user can click into → **kind count-button — .count-btn, --link coloured, .is-zero when empty**. Not: A .children-stamp, which does not look pressable - Several values in one cell → **kind chips — first three plus a "+n" carrying the full list**. Not: All of them, which widens the row unpredictably - A boolean qualifying the object name → **kind mark — an icon before the name inside .name-cell**. Not: Its own column, or a word repeated down every row **lists** - Homogeneous records to sort, select, and act on in bulk → **The data-table page**. Not: Cards, which cannot align values for comparison - Records whose state is the first thing scanned for, with a few attributes read together → **ObjectCard in an ObjectStack — every card opens its detail page; add onMore for a per-row action**. Not: A table, which flattens them into columns that only make sense read together - Two or more related record types on one page → **The tabbed list page**. Not: Separate pages - What happened and what changed → **ActivityPanel**. Not: Either of the above ### Inventory #### Button - Root class: `.btn` · category: actions - Variants: `.btn-primary`, `.btn-secondary`, `.btn-tertiary`, `.btn-tonal`, `.btn-accent`, `.btn-danger`, `.btn-tonal-error` - Sizes: `.btn-sm`, md (default), `.btn-lg` - React: `Button` from `components/Button/Button.jsx` #### ButtonGroup - Root class: `.btn-group` · category: inputs - Variants: `text`, `.icon-only`, `icon-and-text` - React: `ButtonGroup` from `components/ButtonGroup/ButtonGroup.jsx` - Note: Unselected segments sit in --slate-100 with a --surface-muted hover fill; the selected segment takes the --slate-bg fill and deepens fill + label together on hover. Segment glyphs may carry .is-success / .is-warning / .is-error / .is-neutral for status tones. Filtering and segmented single-selection only — never actions. #### Tooltip - Root class: `.tooltip` · category: feedback - Variants: `.tooltip`, `.tooltip tooltip-top`, `.tooltip tooltip-bottom`, `.tooltip tooltip-right`, `.tooltip tooltip-left` - React: `Tooltip` from `components/Tooltip/Tooltip.jsx` - Note: Fill contrasts page background (graphite/base). Typography: UI/Small (12px Avantt SemiBold). Two usage patterns: [data-tooltip] attribute for pure-CSS auto-tooltip; .tooltip class for JS-positioned overlays. #### SplitButton - Root class: `.btn-split` · category: actions - Variants: `.btn-split-primary`, `.btn-split-primary btn-split-compact`, `.btn-split-ghost` - React: `SplitButton` from `components/SplitButton/SplitButton.jsx` - Note: Two-segment control: left fires the primary action, right opens a dropdown of related options. Use when 2-4 closely related actions share a surface and one is a clear default. Not for unrelated actions. #### IconButton - Root class: `.btn-icon` · category: actions - Variants: `.btn-icon btn-primary`, `.btn-icon btn-secondary`, `.btn-icon btn-tertiary`, `.btn-icon btn-tonal`, `.btn-icon btn-danger` - Sizes: `.btn-sm`, md (default), `.btn-lg` - React: `IconButton` from `components/IconButton/IconButton.jsx` - Note: Standalone square icon-only button. Shares variant/size tokens with Button — do not use ButtonGroup's .bg-btn.icon-only for standalone actions. #### TextField - Root class: `.field` · category: inputs - Variants: `default`, `.field-inset` - React: `TextField` from `components/TextField/TextField.jsx` - Note: The icon adornment is not a variant — set the icon / iconRight props on either variant. Fields floor at min-width:240px. Wrap a field in .field-narrow to opt out of that floor (popover rows, packed toolbars); the modifier also clears the floor on .field-inset and .tag-input. An unlabelled field (a min/max pair under one group heading, a toolbar filter) takes ariaLabel — without a visible label or ariaLabel the input has no accessible name. #### Textarea - Root class: `textarea.field-input` · category: inputs - Variants: `default`, `.field-inset` - React: `Textarea` from `components/Textarea/Textarea.jsx` - Note: Multi-line sibling of TextField — the same .field / .field-input / .field-help contract, so stroke, focus, and error treatment are shared. The one difference that matters: height is a row count, never --control-h. A textarea must not resolve to the 34px control height. Use it whenever the answer is prose (a note, a description, a pause reason) rather than a value. #### DateField - Root class: `.md-input` · category: inputs - Variants: `.field`, `.md-input` - React: `DateField` from `components/DatePicker/DatePicker.jsx` - Note: Date input with a calendar affordance, in both label treatments. A text input, not a masked control — the host owns parsing and validation. Pair with DatePicker on the trailing glyph (see docked_pairing). #### DatePicker - Root class: `.md-datepicker` · category: inputs - Variants: `.md-datepicker`, `.md-range`, `.md-range has-presets` - React: `DatePicker` from `components/DatePicker/DatePicker.jsx` - Note: Calendar for one date or a range. Single is 360px with 40x40 cells; range is 680px showing two months with a ribbon fill and 36x36 cells — an ad-ops flight is almost always read across a month boundary. Six week rows always render, so the grid height never jumps between months. #### TimePicker - Root class: `.md-timepicker` · category: inputs - Variants: `dial`, `.is-input` - React: `TimePicker` from `components/DatePicker/DatePicker.jsx` - Note: M3 time picker in both spec'd modes. Dial is the default and is faster for round times (00, 15, 30, 45); input turns the hour and minute into text fields for sub-minute precision or a dense form. The M3 toggle sits on the action row and is preserved. 12-hour with an AM/PM stack; dial hours step 1, dial minutes step 5. #### SelectField - Root class: `.field` · category: inputs - Variants: `default`, `.field-inset` - React: `SelectField` from `components/SelectField/SelectField.jsx` - Note: Field-shaped trigger that owns a SelectMenu popover. Closes on outside click, Escape, and on choice when not multi. With multi + chips it renders the .tag-input box, showing each selection as a dismissible chip rather than an 'N selected' summary — prefer that when the user needs to see and remove individual values. Multi-select shows dismissible chips (chips prop), not an "N selected" summary. Inside .field-inset the chips trigger draws no box of its own. #### SelectMenu - Root class: `.select-menu` · category: inputs - Variants: `default`, `with-icons`, `with-search`, `multi-select`, `with-sections` - React: `SelectMenu` from `components/SelectMenu/SelectMenu.jsx` - Note: Popover dropdown — the surface SelectField opens, also usable on its own. Options are set by props, not variant classes: icons come from item.icon / item.iconUrl, sections from items with a header, search and multi from the search and multi props. #### TagInput - Root class: `.tag-input` · category: inputs - Variants: `.tag-input`, `.field-inset tag-inset` - React: `TagInput` from `components/TagInput/TagInput.jsx` - Note: Chips inside are the canonical .chip element. Enter and comma commit the draft; Backspace on an empty draft removes the last tag. #### Checkbox - Root class: `.checkbox` · category: inputs - Variants: `md`, `.checkbox-sm` - React: `Checkbox` from `components/Checkbox/Checkbox.jsx` - Note: A real , appearance:none, styled to match .sm-check so a menu row and a standalone box read identically. Table selection columns inherit the same rules via .table-card .cell-select input[type="checkbox"] at 15px. For a setting that applies the moment it is flipped, use Switch. There is no Radio in this system — single-choice pickers are a ButtonGroup or a SelectField. #### Chip - Root class: `.chip` · category: data - Variants: `.is-selected`, `.is-outlined`, `.is-accent`, `.chip-square` - Sizes: `.chip-sm`, md (default), `.chip-lg` - React: `Chip` from `components/Chip/Chip.jsx` - Note: Pill is the default shape; there is no round class. Style is set by boolean props, not a variant enum. #### Badge - Root class: `.badge` · category: data - React: `Badge` from `components/Badge/Badge.jsx` #### PlatformIcon - Root class: `.pf-tile` · category: data - Sizes: symbol (default), lockup (default) - React: `PlatformIcon` from `components/PlatformIcon/PlatformIcon.jsx` - Note: Third-party platform logos. Never recolor, restyle, or substitute. Always render on a neutral white plate. Assets in assets/partners/. #### ChatSticker - Root class: `.chat-sticker` · category: brand - Variants: `.chat-sticker-reaction`, `.chat-sticker-thread`, `.chat-sticker-emphasis`, `.chat-sticker-inline` - Sizes: reaction (default), thread (default), emphasis (default) - React: `ChatSticker` from `components/ChatSticker/ChatSticker.jsx` - Note: Animated brand GIF for use as a standalone chat message. Never embed inside a text bubble. Transparent background — no plate or shadow. The class sits on the itself, not a wrapper — target it as img.chat-sticker. basePath takes a prefix like "../.." and the component adds the separator. Its chat context is the .as-* agent workspace vocabulary — .as-working uses size="inline" (32px) — a real ChatSticker size, not a caller-side override — sized to sit beside the working-state text without crowding it. A separate .beacon-* demo block used to exist in components.css and was removed: there is one chat contract, not two. #### Switch - Root class: `.switch` · category: inputs - Variants: `default`, `.accent` - React: `Switch` from `components/Switch/Switch.jsx` - Note: Binary on/off for a single setting (dark mode, auto-pause, notifications). Not for choosing between options — that is a ButtonGroup. Track is .switch-track; the label sits beside it inside .switch. Add role="switch" and aria-checked when the element is interactive. #### ThemeSwitch - Root class: `.theme-switch` · category: inputs - Variants: `inline`, `.is-fixed` - React: `ThemeSwitch` from `components/ThemeSwitch/ThemeSwitch.jsx` - Note: THE canonical light/dark control. Sun icon, track, moon icon, no text label — the icon on the active side tints slate, the other stays muted. Drive it from aria-checked (true = dark), never from a button group. Track is .ts-track, icons are .ts-ico.ts-sun / .ts-ico.ts-moon. Add .is-fixed to pin it top-right on a specimen page. Persist the choice in localStorage under swivel-theme and apply it before first paint; first visit defaults to dark. #### ActivityPanel - Root class: `.activity` · category: feedback - React: `ActivityPanel` from `components/ActivityPanel/ActivityPanel.jsx` - Note: A reverse-chronological stream of what users and agents did, each row expandable into the record it wrote. Use for any audit, history, or activity surface. Not a Table: a table cannot carry a nested before/after diff, and it would offer sorting and selection that an audit log does not have. Not a bare timeline either — the point is that each row opens. #### Alert - Root class: `.alert` · category: feedback - Variants: `.alert-filled`, `.alert-outlined` - React: `Alert` from `components/Alert/Alert.jsx` #### Toast - Root class: `.toast-region` · category: feedback - Variants: `.alert-info`, `.alert-success`, `.alert-warning`, `.alert-error` - React: `Toast` from `components/Toast/Toast.jsx` - Note: A transient receipt for an action the user already took — "Plan saved", "3 rows paused". Reuses the .alert body wholesale (.alert-icon, .alert-body, .alert-title, .alert-dismiss) and adds only the fixed stack, --elev-3, and enter/exit motion. Anything the user must READ, act on, or refer back to is an inline Alert; a destructive confirmation is a Modal. #### IdChip - Root class: `.id-chip` · category: navigation - Note: Copyable object-ID pill, promoted out of Breadcrumb/Table so every consumer shares one contract: mono-ish body type, --graphite-10 fill, --graphite-90 text, pill radius, 22px, white-space: nowrap and flex: 0 0 auto (real IDs are hyphenated and the pill cannot grow). Used in Breadcrumb's object segment, the leading cell of any object table, ActivityPanel entries, and Beacon focus-object headers. #### Breadcrumb - Root class: `.breadcrumb` · category: navigation - React: `Breadcrumb` from `components/Breadcrumb/Breadcrumb.jsx` - Note: A small trail (--fs-ui-small, body font) sitting ABOVE the page's own

— it never replaces the heading. Parents in --graphite-bg, the current segment in --graphite-90 at weight 600. On an object page the object's ID always appears as a copyable IdChip (its own component — see "id-chip" below). The separator is always "/" and the last item is never a link (aria-current="page"). Required on every detail page: it is how an operator returns to the list they arrived from. Replaces the showcase-only .pp-crumb scaffolding, which rendered the trail at title scale — that treatment is retired. #### TopHeader - Root class: `.top-header` · category: navigation - React: `TopHeader` from `components/TopHeader/TopHeader.jsx` - Note: Fixed slot order, left to right: hamburger, brand lockup, account selector, spacer, theme switch, user indicator, logout. The bare .top-header tracks the page theme through tokens — use it in real product shells. .is-dark / .is-light are hardcoded specimen variants for showing both themes side by side on the showcase page; never ship them. Clicking the user indicator opens a grouped account popover (userMenu prop) when userMenu or onLogout is set — self-managed open state, closes on outside click, Escape, or picking an item. #### Sidenav - Root class: `.sidenav` · category: navigation - Variants: `expanded`, `.is-collapsed` - React: `Sidenav` from `components/Sidenav/Sidenav.jsx` - Note: Production root is .sidenav; .sidenav-demo is the showcase alias with identical rules. Rows are .sn-item, active row takes .is-selected, groups sit under .sn-section headers. #### Tabs - Root class: `n/a` · category: navigation - Variants: `undefined`, `undefined` - React: `Tabs` from `components/Tabs/Tabs.jsx` - Note: A tab never wraps: .tab is white-space: nowrap and flex: 0 0 auto, because a shrinkable tab breaks a multi-word label and leaves the underline indicator under a two-line box. Counts are items[].count, rendered as .tab-count — concatenating "(12)" into the label makes the preceding space a break opportunity and orphans the count on its own line. #### Card - Root class: `.card` · category: containers - Variants: `.card-outline`, `.card-elevation`, `.card-flat` - Sizes: `.card-sm`, md (default), `.card-lg` - React: `Card` from `components/Card/Card.jsx` #### Drawer - Root class: `.drawer-overlay` · category: containers - Variants: `.drawer-overlay`, `.drawer-overlay drawer-left` - Sizes: `.drawer-sm`, default (default), `.drawer-lg` - React: `Drawer` from `components/Drawer/Drawer.jsx` - Note: Slide-in panel anchored to viewport edge. Use for settings, detail views, filter panels. Open by adding .is-open to .drawer-overlay. #### Modal - Root class: `n/a` · category: containers - React: `Modal` from `components/Modal/Modal.jsx` #### EditableTitle - Root class: `.editable-title` · category: layout - Variants: `.is-h1`, `.is-h2` - Note: Click-to-rename h1/h2 for object detail pages. Static state shows a pencil glyph on hover/focus; click (or Enter/Space) swaps to a text input; Enter/blur commits, Escape reverts. Sits below the Breadcrumb, replacing the plain heading — never used on a list/collection heading, which has no single name to rename. #### Chart - Root class: `.chart` · category: data-viz - Variants: `line`, `area`, `bar`, `bar-horizontal` - React: `Chart` from `components/Chart/Chart.jsx` - Note: Line, area, and bar charts with the Swivel axis, tooltip, and palette contract built in. design-system/charts.rules.yaml is the full rule registry and Charts.html the rendered showcase — this component implements the universal rules so a consumer cannot half-apply them. The plot is SVG; the .chart-* classes own the chrome (head, axis type, tooltip, legend). Bars always compare from zero; the axis is never truncated. #### Sparkline - Root class: `.chart-sparkline` · category: data-viz - React: `Sparkline` from `components/Chart/Chart.jsx` - Note: Inline trend for one metric — no axes, no tooltip. Context beside a number, not analysis. Also the trend inside a KpiCard. #### Donut - Root class: `.chart-donut-wrap` · category: data-viz - React: `Donut` from `components/Chart/Chart.jsx` - Note: One anchor number with up to four supporting slices. The centre is what justifies the format — never a pie. Past four slices, or when values sit within 5% of each other, or when there is no anchor number, switch to Chart type="bar-horizontal": humans rank lengths, not arcs. #### Bullet - Root class: `.chart-bullet-measure` · category: data-viz - React: `Bullet` from `components/Chart/Chart.jsx` - Note: Actual versus target with qualitative bands. Replaces gauges — a gauge spends a lot of space on one number and reads its angle, not its length. #### ProgressBar - Root class: `.progress` · category: data-viz - Variants: `determinate`, `.is-indeterminate` - Sizes: sm (default), md (default), lg (default) - React: `ProgressBar` from `components/ProgressBar/ProgressBar.jsx` - Note: Completion of a known whole — a delivery share, an upload, an export. Distinct from two neighbours: Bullet is actual against a target with qualitative bands, and Skeleton stands in for content that has not arrived. A progress bar says how far along something is; a skeleton says nothing is here yet. #### FlightProgress - Root class: `.progress-flight` · category: data-viz - React: `FlightProgress` from `components/ProgressBar/ProgressBar.jsx` - Note: Elapsed time across a flight window, with dark days blocked out. This measures TIME, not delivery — impressions against a goal is a ProgressBar, pacing against a target with qualitative bands is a Bullet. Two encodings in deliberately different colour families: --slate-100 for time spent, --graphite-80 for time the flight was not delivering. #### KpiCard - Root class: `.kpi-card` · category: data-viz - React: `KpiCard` from `components/KpiCard/KpiCard.jsx` - Note: One number, one label, one delta. If a second dimension is needed the answer is a chart, not a bigger card. The label states the window the delta covers ("vs. last 7d") — never the card chrome. #### Table - Root class: `.table-card` · category: data - Variants: `.table-card-elevation`, `.table-card-outline` - CSS only: class hooks, no React component. - Note: A table is always .table-card + a variant class wrapping .table-toolbar, the table itself, and .table-foot. The List page pattern requires the toolbar — never ship a bare

. Cell classes carry the type: .cell-select, .cell-id (with .id-chip), .cell-metric (right-aligned numerals), .cell-date, .cell-state. Sortable headers use a .th-sort button. Column visibility: add .col-hidden to a th/td pair to hide that column. Status cells are icon-only: .cell-state plus a tone class (.is-success, .is-warning, .is-error, .is-neutral) and .is-outline for an unfilled glyph; give the glyph data-tip so hover reveals the status text. Row hover uses --slate-bg so it reads as selection-adjacent and never matches the --graphite-10 header fill. The .id-chip in the ID column is the same contract the Breadcrumb uses; as a … … ``` #### ObjectCard - Root class: `.object-card` · category: data - Variants: `.oc-badge`, `.oc-badge is-platform` - React: `ObjectCard` from `components/ObjectCard/ObjectCard.jsx` - Note: One object, scanned as a card rather than a table row. The whole card opens the object's detail page (href/onClick) — there is no separate "clickable" variant, every ObjectCard behaves this way, so a kebab (onMore) is how a per-row action coexists with a click-through card. The badge identifies the object, not its status: square icon-over-ID by default (a type or lifecycle icon needs room to read as a symbol), or the short horizontal chip via isPlatform when the icon is a platform mark and the ID belongs to that third-party system. A real differentiator (a lifecycle state, a delivery figure) belongs in stats, where its colour means what it says. #### Pagination - Root class: `.table-pagination` · category: data - Variants: `paged`, `count-only` - React: `Pagination` from `components/Pagination/Pagination.jsx` - Note: Sits directly below the .table inside its .table-card — never floated on the page canvas. Every list table carries one, even an unpaged list (use countOnly for a bare count). #### Section - Root class: `.section` · category: containers - Variants: `elevated`, `.is-outline` - React: `Section` from `components/Section/Section.jsx` - Note: A titled, optionally collapsible block within a stack — reconciles the two ad hoc implementations that existed before it (beacon-workspace's icon-swap card-header, Data Viz Dashboard's rotating chevron). One chevron, rotating -90deg when collapsed; the body collapses via max-height + opacity rather than a hard cut, so multiple stacked sections animate consistently. #### Skeleton - Root class: `.skeleton` · category: feedback - Variants: `.skeleton-text`, `.skeleton-title`, `.skeleton-block`, `.skeleton-circle` - React: `Skeleton` from `components/Skeleton/Skeleton.jsx` - Note: Shape must match the content it stands in for. Sheen animation is suppressed under prefers-reduced-motion. #### EmptyState - Root class: `.state-panel` · category: feedback - React: `EmptyState` from `components/EmptyState/EmptyState.jsx` - Note: Icon + headline + one line of guidance + an action. Add .state-panel-compact (compact prop) inside a card; use .table-card-empty for the in-table variant. #### ErrorState - Root class: `.state-panel.is-error` · category: feedback - React: `ErrorState` from `components/ErrorState/ErrorState.jsx` - Note: Same anatomy as EmptyState with .is-error tinting the icon and role="alert" on the panel. Always offer a retry or a way forward. ## Page patterns ### App shell Every product page sits in the app shell. The header pins to the top; nav and content share the row below. - `.app-shell-header` → TopHeader - `.app-shell-nav` → Sidenav. The header hamburger drives Sidenav's `collapsed` prop. - `.app-shell-main > main`. Scrolls independently. Padding 28px 24px 40px. ### Universal rules - **page_header**: Every page opens with an H1 page title and a result/context line beneath it. The page-level primary action is right-aligned on that row. On a detail page a Breadcrumb sits ABOVE the H1 at --fs-ui-small — never at heading scale, and never instead of the H1 (the retired .pp-crumb scaffolding did both). - **action_placement**: Primary action top-right of the page header. Destructive actions bottom-left of a form footer, cancel + save bottom-right. Never put an action in a ButtonGroup. - **filters**: Filter controls sit in a bordered filter bar above the table card: search first, then field filters, then the status ButtonGroup, then a right-aligned reset IconButton. - **table_required_toolbar**: A list page's table is always .table-card + .table-toolbar. Never ship a bare
……
…optional…
. - **loading_and_states**: Use Skeleton in the shape of the eventual content while loading, EmptyState when a query returns nothing, ErrorState when a fetch fails. Never leave a blank card. - **density**: Default list density is 25 rows; the toolbar density control toggles compact (.is-compact). - **theme**: Dark is the default. Every page ships a working ThemeSwitch; light is opt-in via data-theme="light". ### Page layouts - **List page** (`list_page`): Browse, filter, and act on a collection of objects. The default for any collection: ad products, automations, scripts, deals. - **List page · tabbed** (`list_page_tabbed`): One collection split into named subsets that share the same chrome. When rows fall into a few mutually exclusive buckets that each deserve their own table (e.g. Active / Archived). - **Detail page · two-column form** (`detail_two_column_form`): Read and edit one object's attributes. Any object detail or create/edit flow with more than a handful of fields. - **Automation detail · split panel** (`detail_split_panel`): Monitor a running object: history on the left, live controls on the right. Objects with executions or runs (automations, scripts, agent jobs). - **Activity page** (`activity_page`): An audit stream of what users and agents did, each row expandable into the record it wrote. - **Agent workspace** (`agent_workspace`): Converse with an agent while the artifacts it produces stay open beside the transcript. ## Voice and copy Swivel is the agentic execution layer for ad operations. Purpose-built agents operate inside operator-defined guardrails. Humans set the goals and rules; agents execute against them. Audience: A precise practitioner talking to other practitioners. Knowledgeable, confident, empathetic to ad-ops pain ("we've been in the chair"). Never breathless, never apologetic. | Rule | Severity | Summary | |---|---|---| | `no_em_dashes` | error | Do not use em dashes (—). | | `no_fragment_sentences` | error | Every sentence must have a subject and a verb. | | `no_staccato` | error | Avoid three-word stab rhythms like "Fast. Smart. Built for scale." | | `no_not_x_but_y` | error | Do not use the "it's not X, it's Y" construction. | | `no_jargon_overload` | warn | Translate ad-tech jargon to plain language when the audience is broader than operators. | | `ai_augments_never_replaces` | error | AI augments operators; it never "takes over" or "replaces" them. | | `first_or_second_person` | warn | Prefer "you" and "we" over third-person passive. | | `headlines_end_with_period` | error | Every headline ends with a period. | | `no_all_caps_body` | error | No ALL CAPS body copy. Display moments may use stylized mixed case. | | `no_named_customer_categories` | error | Do not name specific broadcaster or OEM customer categories in public copy. | | `no_emoji` | error | Emoji are not part of the system. Use the Switch accent, the angular S mark, and brand iconography for visual energy. | | `unicode_arrows_allowed` | info | Unicode arrows (→, ↑, ↗, ↘) are part of the visual language for metric readouts and CTAs at headline weight. | Acceptable headlines: - A million decisionS a minute. - We built the sell-side stack. Now we automate it. - Scale your decisions, not your headcount. Unacceptable headlines: - ~~Fast. Smart. Built for scale.~~ - ~~It's not just automation, it's intelligence.~~ - ~~Ad ops is broken.~~ Format notes: - UI and product copy: short, action-oriented, active voice. "Loading your data." not "Data is being loaded." - Decks and one-pagers: the slide title is the argument, not the topic. Metrics are the proof. - Email: warm but efficient, one CTA per section. ## Tokens Every token in the system. Values come from the canonical registry; the CSS variable is what you write. Light values marked "same" do not change between themes. ### Color | Variable | Dark (default) | Light | Notes | |---|---|---|---| | `--switch-100` | `#A8F8FF` | same | | | `--switch-bg` | `#1F4A4E` | `#D6FEFF` | | | `--switch-contrast` | `#00E5FF` | `#0097A7` | | | `--graphite-10` | `#2C3030` | `#F0F1F0` | | | `--graphite-20` | `#3A3E3D` | `#D4D6D5` | | | `--graphite-30` | `#4A4F4E` | `#B8BAB9` | | | `--graphite-40` | `#646866` | `#9C9F9E` | | | `--graphite-50` | `#808382` | `#808382` | | | `--graphite-60` | `#9C9F9E` | `#646866` | | | `--graphite-70` | `#B8BAB9` | `#484C4B` | | | `--graphite-80` | `#D4D6D5` | `#2C3030` | | | `--graphite-90` | `#F2F3F2` | `#131514` | | | `--graphite-base` | `#F2F3F2` | `#131514` | Primary text on light, primary surface on dark. | | `--graphite-bg` | `#B8BAB9` | `#6B7170` | Default icon color. | | `--graphite-contrast` | `#131514` | `#000000` | | | `--slate-10` | `#F0F3F7` | same | | | `--slate-20` | `#DCE4ED` | same | | | `--slate-30` | `#C8D5E3` | same | | | `--slate-40` | `#B4C6D9` | same | | | `--slate-50` | `#A0B7CF` | same | | | `--slate-60` | `#8CA8C5` | same | | | `--slate-70` | `#7899BB` | same | | | `--slate-80` | `#6F8BA7` | same | | | `--slate-90` | `#677F9F` | same | | | `--slate-100` | `#637D9C` | same | | | `--slate-110` | `#567088` | same | | | `--slate-120` | `#496374` | same | | | `--slate-130` | `#3C5660` | same | | | `--slate-140` | `#2F494C` | same | | | `--slate-150` | `#223C38` | same | | | `--slate-base` | `#7B9BBF` | `#637D9C` | | | `--slate-bg` | `#1E3048` | `#DCE8F5` | | | `--slate-contrast` | `#A8C5E8` | `#1A2D42` | | | `--slate-bg-50` | `rgba(30, 48, 72, 0.5)` | `rgba(220, 232, 245, 0.5)` | Translucent slate fill — 50% of slate.bg. | | `--white` | `#E0E0E0` | `#FFFFFF` | | | `--white-contrast` | `#BDBDBD` | `#9E9E9E` | | | `--success` | `#43A047` | `#2E7D32` | | | `--success-bg` | `#1B3A1C` | `#C8E6C9` | | | `--success-fg` | `#B9F6CA` | `#0A3D0C` | | | `--warning` | `#FF8F00` | `#D96600` | Warning icons, fills and borders. Not for text in light mode — use warning-text. | | `--warning-text` | `#FF8F00` | `#9A4800` | Warning-coloured text (labels, alert titles). | | `--success-text` | `#66BB6A` | `#256B29` | Success-coloured text, e.g. a favourable delta in a table. Clears 4.5:1 on --surface-base and the --slate-bg row hover in both themes. | | `--error-text` | `#FF6F61` | `#B71C1C` | Error-coloured text, e.g. an unfavourable delta in a table. Clears 4.5:1 on --surface-base and the --slate-bg row hover in both themes. | | `--warning-bg` | `#3E2000` | `#FFE0B2` | | | `--warning-fg` | `#FFD54F` | `#7A3300` | | | `--info` | `#1E88E5` | `#1976D2` | | | `--info-bg` | `#12294D` | `#BBDEFB` | | | `--info-fg` | `#90CAF9` | `#0A2A6E` | | | `--error` | `#E53935` | `#D32F2F` | | | `--error-bg` | `#3B0000` | `#FFCDD2` | | | `--error-fg` | `#FF8A80` | `#5C0000` | | | `--btn-tonal-hover` | `#2A3F5C` | `#CDDCEC` | Hover fill for tonal buttons. | | `--btn-accent-hover` | `#C2FBFF` | `#C2FBFF` | Hover fill for accent buttons. | | `--btn-danger-hover` | `#C62828` | `#B71C1C` | Hover fill for danger buttons. | | `--btn-tonal-error-hover` | `#520A0A` | `#FFB3BA` | Hover fill for tonal error buttons — one step darker than --error-bg. | | `--surface-base` | `#1E2120` | `#FFFFFF` | Card, panel, row, tooltip fill. | | `--surface-canvas` | `#131514` | `#FCFCFC` | Page background. | | `--surface-muted` | `#1A1C1B` | `#F0F1F0` | Secondary fill — disabled, inset. | | `--surface-stroke` | `#2C3030` | `#D4D6D5` | Hairline border on all surfaces. | | `--surface-input` | `#1E2020` | `#FFFFFF` | | | `--viz-slate` | `{color.slate.base}` | same | | | `--viz-switch` | `{color.switch.contrast}` | same | | | `--viz-deep` | `oklch(0.48 0.070 250)` | `oklch(0.30 0.060 252)` | | | `--viz-steel` | `oklch(0.66 0.100 232)` | `oklch(0.55 0.090 235)` | | | `--viz-mist` | `oklch(0.86 0.045 218)` | `oklch(0.78 0.045 220)` | | | `--viz-graphite` | `{color.graphite.70}` | same | | | `--seq-slate-1` | `oklch(0.25 0.020 252)` | `oklch(0.96 0.015 252)` | | | `--seq-slate-2` | `oklch(0.34 0.040 252)` | `oklch(0.88 0.030 252)` | | | `--seq-slate-3` | `oklch(0.44 0.060 252)` | `oklch(0.78 0.045 252)` | | | `--seq-slate-4` | `oklch(0.54 0.075 252)` | `oklch(0.68 0.060 252)` | | | `--seq-slate-5` | `oklch(0.64 0.085 252)` | `oklch(0.56 0.060 252)` | | | `--seq-slate-6` | `oklch(0.76 0.090 252)` | `oklch(0.44 0.060 252)` | | | `--seq-slate-7` | `oklch(0.88 0.075 252)` | `oklch(0.30 0.050 252)` | | | `--seq-switch-1` | `oklch(0.25 0.030 200)` | `oklch(0.96 0.020 200)` | | | `--seq-switch-2` | `oklch(0.34 0.060 200)` | `oklch(0.88 0.045 200)` | | | `--seq-switch-3` | `oklch(0.44 0.085 200)` | `oklch(0.78 0.070 200)` | | | `--seq-switch-4` | `oklch(0.54 0.105 200)` | `oklch(0.68 0.080 200)` | | | `--seq-switch-5` | `oklch(0.64 0.115 200)` | `oklch(0.58 0.085 200)` | | | `--seq-switch-6` | `oklch(0.76 0.110 200)` | `oklch(0.46 0.080 200)` | | | `--seq-switch-7` | `oklch(0.88 0.090 200)` | `oklch(0.32 0.070 200)` | | | `--seq-warm-1` | `oklch(0.25 0.030 50)` | `oklch(0.96 0.020 60)` | | | `--seq-warm-2` | `oklch(0.34 0.065 45)` | `oklch(0.88 0.050 60)` | | | `--seq-warm-3` | `oklch(0.44 0.095 40)` | `oklch(0.80 0.080 60)` | | | `--seq-warm-4` | `oklch(0.54 0.120 38)` | `oklch(0.74 0.105 55)` | | | `--seq-warm-5` | `oklch(0.64 0.135 40)` | `oklch(0.66 0.130 45)` | | | `--seq-warm-6` | `oklch(0.76 0.135 55)` | `oklch(0.54 0.135 35)` | | | `--seq-warm-7` | `oklch(0.88 0.115 75)` | `oklch(0.40 0.120 30)` | | | `--div-1` | `oklch(0.84 0.080 252)` | `oklch(0.30 0.060 252)` | | | `--div-2` | `oklch(0.72 0.085 248)` | `oklch(0.42 0.075 250)` | | | `--div-3` | `oklch(0.58 0.080 244)` | `oklch(0.54 0.080 245)` | | | `--div-4` | `oklch(0.42 0.060 240)` | `oklch(0.68 0.060 240)` | | | `--div-5` | `oklch(0.30 0.012 220)` | `oklch(0.94 0.008 220)` | | | `--div-6` | `oklch(0.46 0.080 212)` | `oklch(0.78 0.060 215)` | | | `--div-7` | `oklch(0.62 0.095 204)` | `oklch(0.66 0.080 205)` | | | `--div-8` | `oklch(0.78 0.100 200)` | `oklch(0.54 0.090 198)` | | | `--div-9` | `oklch(0.90 0.095 198)` | `oklch(0.42 0.085 195)` | | | `--stack-1` | `oklch(0.50 0.075 252)` | `oklch(0.38 0.075 252)` | | | `--stack-2` | `oklch(0.56 0.080 248)` | `oklch(0.46 0.080 250)` | | | `--stack-3` | `oklch(0.62 0.080 240)` | `oklch(0.54 0.080 245)` | | | `--stack-4` | `oklch(0.66 0.080 232)` | `oklch(0.60 0.080 240)` | | | `--stack-5` | `oklch(0.70 0.082 224)` | `oklch(0.64 0.080 232)` | | | `--stack-6` | `oklch(0.74 0.082 216)` | `oklch(0.68 0.080 224)` | | | `--stack-7` | `oklch(0.78 0.085 210)` | `oklch(0.72 0.082 216)` | | | `--stack-8` | `oklch(0.82 0.085 206)` | `oklch(0.76 0.085 210)` | | | `--stack-9` | `oklch(0.86 0.085 202)` | `oklch(0.82 0.085 204)` | | | `--stack-10` | `oklch(0.90 0.080 198)` | `oklch(0.88 0.085 200)` | | | `--stack-other` | `{color.graphite.70}` | `{color.graphite.20}` | | | `--link` | `#00E5FF` | `#496374` | Link and primary-name text. Dark value (switch-contrast); light is #496374 (slate-120). Measured 10.55:1 card / 8.69:1 row-hover dark, 6.32:1 / 5.09:1 light. | | `--link-hover` | `#A8F8FF` | `#3C5660` | Link hover. Dark value (switch-100); light is #3C5660 (slate-130). Measured 13.56:1 card / 11.16:1 row-hover dark, 7.80:1 / 6.28:1 light. | | `--btn-primary-bg` | `#00E5FF` | `#637D9C` | Dark value (switch-contrast); light is slate-100 (#637D9C). | | `--btn-primary-bg-hover` | `#A8F8FF` | `#567088` | Dark value (switch-100); light is slate-110 (#567088). | | `--btn-primary-fg` | `#131514` | `#FFFFFF` | Dark value (graphite-contrast, near-black); light is white. Switch-cyan fill needs dark ink in both themes once it becomes the CTA color in dark mode. | ### Size | Variable | Dark (default) | Light | Notes | |---|---|---|---| | `--space-sm` | `4px` | same | Tight gaps, icon spacing, vertical button padding. | | `--space-md` | `8px` | same | Nav entries, section padding. | | `--space-lg` | `12px` | same | Horizontal button padding, alert/dialog inset. | | `--space-xl` | `16px` | same | Card padding, input horizontal inset. | | `--space-xxl` | `20px` | same | Page/section header top padding. | | `--space-3xl` | `24px` | same | | | `--space-4xl` | `32px` | same | | | `--space-5xl` | `40px` | same | | | `--space-section` | `64px` | same | | | `--control-h-sm` | `24px` | same | | | `--control-h` | `34px` | same | | | `--control-h-lg` | `40px` | same | | ### Icon | Variable | Dark (default) | Light | Notes | |---|---|---|---| | `--icon-2xs` | `12px` | same | Badge and chip glyphs. | | `--icon-xs` | `14px` | same | Header sort carets, avatar glyphs. | | `--icon-sm` | `16px` | same | Inline marks beside text, e.g. verified-mark. | | `--icon-md` | `18px` | same | Default UI icon: buttons, pagination, popover rows. | | `--icon-lg` | `20px` | same | Table toolbar, status glyphs, bare icon columns. | | `--icon-xl` | `22px` | same | Alerts, status icons, header menu. | | `--icon-2xl` | `24px` | same | Material Symbols default. | | `--icon-3xl` | `32px` | same | Empty-state and panel icons. | ### Radius | Variable | Dark (default) | Light | Notes | |---|---|---|---| | `--radius-none` | `0` | same | | | `--radius-xs` | `2px` | same | | | `--radius-sm` | `4px` | same | Default UI radius. | | `--radius-md` | `6px` | same | | | `--radius-lg` | `8px` | same | Max on cards. | | `--radius-xl` | `12px` | same | | | `--radius-full` | `9999px` | same | Pills, data badges. | ### Motion | Variable | Dark (default) | Light | Notes | |---|---|---|---| | `--motion-instant` | `80ms` | same | Tooltip show; state flips that should feel instantaneous. | | `--motion-fast` | `120ms` | same | Hover, focus, small color/opacity shifts. | | `--motion-base` | `200ms` | same | The default — most UI transitions. | | `--motion-slow` | `240ms` | same | Expand/collapse, drawer slide, sheet/modal. | | `--motion-deliberate` | `360ms` | same | Attention-getting changes, value count-ups, hero reveals. | | `--ease-standard` | `[0.2,0.7,0.2,1]` | same | Enter + exit — the default. | | `--ease-emphasized` | `[0.3,0,0,1]` | same | Large surface transitions. | | `--ease-enter` | `[0,0,0,1]` | same | Element appearing. | | `--ease-exit` | `[0.3,0,1,1]` | same | Element leaving. | | `--ease-linear` | `[0,0,1,1]` | same | Progress bars, scrub. | ### Elevation | Variable | Dark (default) | Light | Notes | |---|---|---|---| | `--elev-1` | `[{"offsetX":"0","offsetY":"1px","blur":"2px","spread":"0","color":"rgba(0,0,0,0.30)"},{"offsetX":"0","offsetY":"1px","blur":"3px","spread":"1px","color":"rgba(0,0,0,0.15)"}]` | same | | | `--elev-2` | `[{"offsetX":"0","offsetY":"1px","blur":"2px","spread":"0","color":"rgba(0,0,0,0.30)"},{"offsetX":"0","offsetY":"2px","blur":"6px","spread":"2px","color":"rgba(0,0,0,0.15)"}]` | same | | | `--elev-3` | `[{"offsetX":"0","offsetY":"1px","blur":"3px","spread":"0","color":"rgba(0,0,0,0.30)"},{"offsetX":"0","offsetY":"4px","blur":"8px","spread":"3px","color":"rgba(0,0,0,0.15)"}]` | same | | | `--elev-modal` | `[{"offsetX":"0","offsetY":"6px","blur":"10px","spread":"4px","color":"rgba(0,0,0,0.15)"},{"offsetX":"0","offsetY":"2px","blur":"3px","spread":"0","color":"rgba(0,0,0,0.30)"}]` | same | | ### Focus Ring | Variable | Dark (default) | Light | Notes | |---|---|---|---| | `--focus-ring` | `[{"offsetX":"0","offsetY":"0","blur":"0","spread":"3px","color":"rgba(168,248,255,0.4)"}]` | same | Standard focus outline. Switch-cyan glow at 40% opacity, 3px spread. | ### Typography | Variable | Dark (default) | Light | Notes | |---|---|---|---| | `--font-display` | `["Avantt","Inter","-apple-system","BlinkMacSystemFont","sans-serif"]` | same | Headlines and display moments. | | `--font-body` | `["Antarctica","Inter","-apple-system","BlinkMacSystemFont","sans-serif"]` | same | Body, UI, and labels. | | `--font-mono` | `["JetBrains Mono","ui-monospace","SF Mono","Menlo","monospace"]` | same | Code, data, mono UI strings. | | `--fs-display` | `72px` | same | | | `--fs-h1` | `48px` | same | | | `--fs-h2` | `36px` | same | | | `--fs-h3` | `28px` | same | | | `--fs-h4` | `22px` | same | | | `--fs-body-l` | `18px` | same | Lead body, alert/dialog titles. | | `--fs-body` | `14px` | same | Default body. | | `--fs-body-s` | `12px` | same | | | `--fs-body-label` | `12px` | same | Field labels, data keys. | | `--fs-body-caption` | `10px` | same | Timestamps, version strings. | | `--fs-ui-metric` | `28px` | same | KPI readouts. | | `--fs-ui-page-header` | `21px` | same | | | `--fs-ui-title` | `18px` | same | Alert/dialog titles. | | `--fs-ui-default` | `14px` | same | | | `--fs-ui-dense` | `13px` | same | Dense body text: menu items, dates, subtitles. | | `--fs-ui-caps` | `13px` | same | Button labels, card titles, tabs — UPPERCASE. | | `--fs-ui-nav` | `13px` | same | | | `--fs-ui-small` | `12px` | same | Tooltips, helper. | | `--fs-ui-xs` | `11px` | same | Chips, badges, small buttons, mono IDs and stamps. | | `--fs-ui-tag` | `10px` | same | Badges, status chips. | | `--fs-ui-section` | `10px` | same | ALL-CAPS section dividers. | | `--fs-ui-code` | `10px` | same | Mono ID chips, inline code. | | `--fs-axis-tick` | `{typography.font-size.ui-small}` | same | Chart tick + category labels and in-chart value labels. Alias of UI/Small (12px). | | `--fs-axis-title` | `{typography.font-size.ui-caps}` | same | Rotated uppercase value-axis title. Alias of UI/Caps (13px). | | `--ls-headline` | `0.04em` | same | Headline.* + Body.* axes per Figma. | | `--ls-ui-tracked` | `0.04em` | same | Caps / Tag / Nav / Section labels. | | `--lh-tight` | `0.95` | same | Figma 95% line-height. | ## Machine-readable sources | URL | What it answers | |---|---| | https://design-system.swivel-dev.net/design-system/tokens.json | Every token in W3C DTCG format. `$value` is dark; light is under `$extensions."com.swivel.theme".light`; the CSS variable is under `$extensions."com.swivel.cssVar"`. | | https://design-system.swivel-dev.net/design-system/tokens.css | The token registry on its own, without component classes. | | https://design-system.swivel-dev.net/design-system/components.css | The component classes on their own. | | https://design-system.swivel-dev.net/design-system/components.json | Component inventory: variants, sizes, slots, class hooks, and a `state_matrix` per component. Start at `selection_cheat_sheet`. | | https://design-system.swivel-dev.net/design-system/patterns.json | The app shell, universal layout rules, and six page layouts with their ordered regions. | | https://design-system.swivel-dev.net/design-system/templates.json | Manifest of the starting templates: what each composes and what a consumer must replace. The template folders it names live in the source repo and are not served here. | | https://design-system.swivel-dev.net/design-system/adherence.rules.yaml | Usage rules a linter cannot express, each with severity, detection, and fix. Read before shipping. | | https://design-system.swivel-dev.net/design-system/icons.json | Icon sizes, colors, reserved Swivel objects and statuses, the approved Material Symbols subset. | | https://design-system.swivel-dev.net/design-system/charts.rules.yaml | Chart rules: axes, tooltips, per-chart-type guidance. | ## Showcase pages Rendered references for humans. The JSON and YAML above are the source of truth for machines. - [Component library](https://design-system.swivel-dev.net/design-system/Swivel%20Component%20Library.html) - [Colors and tokens](https://design-system.swivel-dev.net/design-system/Colors%20and%20Tokens.html) - [Typography](https://design-system.swivel-dev.net/design-system/Typography.html) - [Iconography](https://design-system.swivel-dev.net/design-system/Iconography.html) - [Motion](https://design-system.swivel-dev.net/design-system/Motion.html) - [Page patterns](https://design-system.swivel-dev.net/design-system/Page%20Patterns.html) - [Date and time](https://design-system.swivel-dev.net/design-system/Date%20and%20Time.html) - [Charts](https://design-system.swivel-dev.net/design-system/Charts.html) - [Data viz palette](https://design-system.swivel-dev.net/design-system/Data%20Viz%20Palette.html) - [Data viz dashboard demo](https://design-system.swivel-dev.net/design-system/Data%20Viz%20Dashboard%20Demo.html)