Design System Reference

Living reference for pure HTML/CSS prototyping (WCAG 2.2 AA on light backgrounds). Last updated 2026.10.05.

Stored in BeameryHQ/design-artefacts, aligning to BeameryHQ/lib-design-system. We build prototypes directly in this repo, test new patterns here before proposing them to the lib-design-system.

For prototyping, create a dated YYYY-MM-simple-name/ folder, starting from starter.html (or forking from any of the other existing folders). See the repo's AGENTS.md for conventions.

Key

Candidate in progress still being designed or tested – don't spec from it.

Candidate for prod built and proven in prototype, proposed for lib-design-system.

Table of contents

Colours

Components must consume semantic tokens (e.g. text.*, borders.*, backgrounds.*, controls.*), never raw primitive shades.

Primary band colours

Click a row to copy the hex.

Hex Primitive Token
#150E3Fbrand-navy
#F15852brand-orange
#7E4BBCbrand-deep-lavender

Key colours

Click a row to copy the hex. These shades are most often referenced in semantic tokens and components.

Hex Primitive Token
#1F1F23neutral-900 text.default, forms.label, borders.formActive, backgrounds.dark
#6C6C7Aneutral-700 text.secondary, forms.placeholder, borders.form
#492FF4indigo-500 text.primary, borders.primary, outlines.focus, controls.primary
#EBEBFEindigo-100 backgrounds.accent
#BF0338red-500 text.error, borders.error, controls.destructive
#FFE6E9red-100 backgrounds.error
#FFBF00yellow-500
#FFF1C7yellow-100 backgrounds.warning
#06742Bgreen-500 text.success, controls.activation
#D1FFD9green-100 backgrounds.success
Primitives – Full colour scales: neutral, indigo, red, yellow, green (50 → 950)

Click to copy hex values. Solid swatches are in-use theme colours.

neutral

Greyscale

Hex Primitive Token
not used
#0B0B0Cneutral-950
#1F1F23neutral-900 text.default, forms.label, borders.formActive, backgrounds.dark
not used
#34343Aneutral-850
#484851neutral-800
#5B5B67neutral-750 text.secondaryOnGray, forms.placeholderOnGray
#6C6C7Aneutral-700 text.secondary, forms.placeholder, borders.form
not used
#858593neutral-650
#A0A0ABneutral-600
not used
#AEAEB7neutral-550
#BCBCC3neutral-500 controls.secondaryActive
not used
#CCCCD1neutral-450
#DBDBE0neutral-400 borders.default, controls.secondaryHover
not used
#E1E1E5neutral-350
#E7E7E9neutral-300 backgrounds.secondaryOnGray
not used
#EBEBEDneutral-250
#F1F1F3neutral-200 backgrounds.secondary, controls.secondary
not used
#F3F3F4neutral-150
#F6F6F7neutral-100 backgrounds.default
not used
#FBFBFBneutral-50
#FFFFFFneutral-0 text.onDark, backgrounds.onGray

indigo

Primary accent

Hex Primitive Token
not used
#252FB1indigo-950
#2222C3indigo-900 text.primaryActive, borders.primaryActive, controls.primaryActive
not used
#2B1FD6indigo-850
#391EE6indigo-800 controls.primaryHover
not used
#3B21E9indigo-750
not used
#3E23EBindigo-700
not used
#4126EDindigo-650
not used
#4329F0indigo-600
not used
#462CF2indigo-550
#492FF4indigo-500 text.primary, borders.primary, outlines.focus, controls.primary
not used
#5D4AF6indigo-450
not used
#7366F8indigo-400
not used
#8A82FAindigo-350
not used
#A39EFBindigo-300
not used
#BCBBFDindigo-250
#D7D7FEindigo-200 backgrounds.accentHover
not used
#E1E1FEindigo-150
#EBEBFEindigo-100 backgrounds.accent
not used
#F5F5FFindigo-50

red

Semantic error

Hex Primitive Token
not used
#7B0526red-950
#860329red-900 controls.destructiveActive
not used
#92022Bred-850
#9E002Dred-800 controls.destructiveHover
not used
#A4002Fred-750
not used
#A90131red-700
not used
#AF0132red-650
not used
#B40234red-600
not used
#BA0236red-550
#BF0338red-500 text.error, borders.error, controls.destructive
not used
#D2043Ered-450
#E70545red-400
not used
#FC275Ared-350
not used
#FD5D7Dred-300
not used
#FE94A5red-250
#FFCCD2red-200 borders.errorSubtle
not used
#FFD9DDred-150
#FFE6E9red-100 backgrounds.error
#FFF2F4red-50 backgrounds.errorSubtle

yellow

Semantic warning

Hex Primitive Token
not used
#291B00yellow-950
not used
#412B00yellow-900
not used
#583B00yellow-850
#704D00yellow-800 text.warning
#885E00yellow-750 text.warningSubtle, borders.warningSubtleHover
not used
#A07100yellow-700
not used
#B88400yellow-650
not used
#CF9700yellow-600
not used
#E7AB00yellow-550
#FFBF00yellow-500
not used
#FFC518yellow-450
not used
#FFCB30yellow-400
not used
#FFD147yellow-350
not used
#FFD75Fyellow-300
not used
#FFDD77yellow-250
#FFE38Dyellow-200
#FFEAAByellow-150 borders.warningSubtle
#FFF1C7yellow-100 backgrounds.warning
#FFF8E3yellow-50 backgrounds.warningSubtle

green

Semantic success

Hex Primitive Token
not used
#054319green-950
#064C1Dgreen-900 controls.activationActive
not used
#085421green-850
#095D25green-800 controls.activationHover
not used
#096126green-750
not used
#086427green-700
not used
#086828green-650
not used
#076C29green-600
not used
#07702Agreen-550
#06742Bgreen-500 text.success, controls.activation
not used
#0AA239green-450
not used
#0FD044green-400
not used
#24ED56green-350
not used
#54EF76green-300
not used
#83F298green-250
#B1F6BDgreen-200
not used
#C0FCCAgreen-150
#D1FFD9green-100 backgrounds.success
not used
#E5FFEAgreen-50

Typography

Single family (Inter / system stack). Hierarchy by size and weight. Default: 14px / 400 / neutral-900.

H1 – Page (24px / 800 / 36px)

H2 – Section (20px / 700 / 32px)

H3 – Section or Modal (16px / 700 / 24px)

H4 – Small Header (14px / 700 / 24px)

H5 – Group Title (12px / 700 / 20px / uppercase)

Body – default product copy at 14px.

Body Emphasised – 600 weight for inline emphasis.

Small – 12px for captions and metadata.

Secondary – neutral-700 for descriptions and hints.

Link – brand primary, darkens on hover.

Error text – validation messages.

Success text – confirmations.

Spacing, Sizing & Shape

4px grid. Gap classes .gap-1….gap-8; most common gap is 16px (--space-4).

Control height: 32px (lib-design-system sizes.x8) for buttons, inputs and selects; chips are 28px, or 24px with .chip-sm. Every interactive target clears the 24×24 minimum (SC 2.5.8).

Radii: 4px (sm – connector chips, small controls), 6px (md – buttons, inputs, selects: the workhorse), 8px (lg – cards), 9999px (pill – value chips, status pills, toggle tracks).

Logo & Icons

All assets ship as SVG sprite symbols. Click an icon to copy its SVG source.

Logo

Full wordmark, 118:32. The bexa mark is always brand-orange; the wording defaults to neutral-900, brand-navy on marketing surfaces.

Bexa

The logo mark without the wording, brand-orange. Scales freely.

Icon library (151 icons)

Most icons are stroke-only – outline icons using stroke="currentColor" – set color on the parent to tint. Partner marks and special cases appear at the end.

Symbols carry their own viewBox — 16-unit for most, but workday is 10-unit and several Phosphor-derived icons (info, warning, seal-check) are 256-unit. Write <svg class="ds-icon"><use href="#ds-icon-…"/></svg> and size it with CSS; hardcoding viewBox="0 0 16 16" on the wrapper silently breaks the non-16 symbols. ds-icon-beamery is the 118×32 wordmark, not an icon — use ds-icon-bexa for a square mark.

Buttons

Base class .btn + variant. One filled primary action per surface. The secondary family carries a visible resting border (borders.form, rendered as an inset box-shadow). Add a leading sprite with .btn-icon; use .btn-icon-only for icon-only controls (include aria-label). Action menus attach a floating .list-panel to a secondary button — labelled or overflow (three dots).

Variants

Disabled = 0.5 opacity + cursor: not-allowed.

With icons

Icon-only

Small (28px)

.btn-sm matches chip height — for inline filter rows and compact toolbars.

Chip (28px)

Action menus

Overflow commands in a .list-panel (role="menu"). Wire with DS.initActionMenu. Use a labelled btn-secondary trigger or an icon-only overflow button (ds-icon-more). Icons only on high-signal items; secondary actions stay text-only.

States (secondary family)
StateFillBorder (inset)
Restcontrols.secondary (neutral-200)borders.form (neutral-700)
Hover / Focuscontrols.secondaryHover (neutral-400)borders.formActive (neutral-900)
Activecontrols.secondaryActive (neutral-500)borders.formActive
Pressed (toggled)–borders.primary

Inputs

Grey-fill control with a visible resting border. Placeholders never replace a visible <label>.

We'll never share your email.
Readable/copyable, not editable — distinct from disabled.
Please enter a valid email address.
Uses .typeahead-results as alias of .list-panel.
List-panel picker via DS.initSelectPicker
Labeled select — Empty / default state shows Any … (from data-select-any or the empty <option> text). Prefix inside trigger when a value is chosen (Sort by: Match score). No external label. Inline auto-width variant — .select-picker-labeled without .select-full.
To match 👉 Chip
28px chip trigger for inline chip-pair rows — .select-picker-chip or .qualifier-select inside .keyword-group.
States

Text inputs

StateFillBorderNotes
Restcontrols.secondary (neutral-200)borders.form (neutral-700)Placeholder forms.placeholder (neutral-700; 750 on grey surfaces)
Hovercontrols.secondaryHover (neutral-400)borders.formActive (neutral-900)
Focuswhiteborders.primary (indigo-500)+ focus ring
Errorneutral-200borders.error (red-500)Label text.error; aria-invalid + aria-describedby
Disabledneutral-200borders.form0.5 opacity

Select picker

btn-secondary trigger + list-panel listbox — same chrome as the Actions menu.

StateTriggerPanelNotes
Restcontrols.secondary + carethiddenSelected value on trigger
Openaria-expanded="true"white surface + shadowClick-outside / Escape closes
Option hover–backgrounds.secondary
Selected option–aria-selected="true"Medium weight + primary text
Focusfocus ringitem focus ring

Select picker (labeled)

Prefix label inside the trigger — .select-picker-labeled + data-select-label. Selected: Sort by: value. Empty: Any … via data-select-any or empty <option> text. Full width with .select-full; inline auto-width without.

StateTriggerPanelNotes
SelectedLabel: + value + carethiddenPrefix bold; value regular weight
Empty / defaultAny … + carethiddenPrefix hidden; full “Any X” phrase bold
Openaria-expanded="true"white surface + shadowSame list-panel behaviour

Select picker (chip)

28px pill trigger matching .chip — for chip-pair keyword rows (location radius, language level). Add .select-picker-chip or use .qualifier-select inside .keyword-group (excludes .threshold-value-select, filter-level .status-select).

StateTriggerPanelNotes
Restwhite fill + borders.form + 12px carethiddenSame geometry as value chip
Hoverbackgrounds.secondary + borders.formActive–
Openaria-expanded="true"white surface + shadowClick-outside / Escape closes
Focusfocus ringitem focus ring

Chips and toggles

Status pills, boolean connectors, interactive chips, single checkboxes, and on/off switches – shared affordances for filters, selections, and settings.

Pills

Compact status labels – 12px / 500, full pill radius. *-100 fill with matching dark text token. Typically not interactive; used to communicate state (e.g. Active, Pending).

Neutral Accent Success Warning Error

Boolean connectors

Boolean-logic connectors for the filter system. 11px / 700, radius 4px. All pairings ≥ 4.5:1 (AA). Full state spec in Filter System/connector-chip-spec.md.

IS IS NOT AND OR OR👈 locked

Chips

Default is 28px / 14px. Resting border borders.form; hover darkens the border, not just the fill. .chip-sm is 24px / 12px for dense filter rows — the same modifier on value, radio, and checkbox chips.

Default – 28px

Chip Chip selected Chip removable

Small – 24px

Chip Chip selected Chip removable

Toggles

Off track is neutral-700 (≥3:1 vs page and thumb); on is brand primary. Three non-colour cues: thumb position, track colour, icon (SC 1.4.1). Use for immediate on/off settings with no separate save step. Default track is 44×24 (18px knob + 3px padding). When a label is used, it must declare the active state and stay the same when off — never swap to an inactive phrasing (that confuses which side is on).

Default – 24px track

Compact exploration – 16px track Candidate for prod

Smaller visuals for dense panels (e.g. filter rows). Hit target kept at ≥24×24 via padding on the label (.toggle--compact-hit), not the track – otherwise the thumb looks undersized inside a padded pill.

With description

Matches the lib-design-system description prop. Wrap in .toggle-field and put the .toggle-description outside the label, wired to the input with aria-describedby – it explains the setting without becoming part of its name.

Description

Checkbox

Binary choices that apply on Save / Apply / Submit, or as part of a batch action. Single option: 16×16 box, 4px radius, label at 14px / 400. Resting border borders.form; hover border borders.primary; checked fill controls.primaryActive with white check. Matches the lib-design-system Checkbox component.

Single option

When to use toggle vs checkbox

toggle = “turn this on now”; checkbox = “include this”.

Use a toggle when flipping the control should take effect immediately — a live on/off for a single setting. Examples: enable/disable an active filter without clearing it, show/hide a panel section in place.

Use a checkbox when the choice is part of a form, list, or batch action — especially if several options can be selected, or the change only applies after Save / Apply / Submit. Examples: row selection, “Select all”, optional columns, consent, sub-filters in a filter panel.

Multiple options

When several choices need configuring together, wrap chips around native checkbox or radio inputs – use .chip-check; selected styles apply when the nested input is checked. Every option stays visible as a pill. Add .chip-sm for the 24px / 12px size.

Default – 28px

Radio – choose one day
Checkbox – choose all toppings that apply

Small – 24px

Radio – choose one day
Checkbox – choose all toppings that apply
States — chips
StateFillBorderText
Restwhiteborders.form (neutral-700)text.default; close icon text.secondary
Hoverbackgrounds.secondary (neutral-200)borders.formActive (neutral-900)close icon darkens to text.default
Activebackgrounds.secondaryOnGray (neutral-300)borders.formActive
Selectedbackgrounds.accent (indigo-100); hover accentHover (indigo-200)borders.primaryActive (indigo-900)text.primaryActive
Remove preview (prototype pattern)whitetext.error (red-500)text.error – hovering a remove-only chip previews the destructive outcome
FocusFocus ring (--focus-ring) on the chip; remove buttons get their own ring
States — checkbox
StateFillBorderCheck
Restwhiteborders.form (neutral-700)hidden
Hoverwhiteborders.primary (indigo-500)hidden
Checkedcontrols.primaryActive (indigo-900)borders.primaryActivewhite check icon
Disabled50% opacity; cursor: not-allowed
FocusFocus ring (--focus-ring) on the box
States — boolean connectors
ConnectorFillBorderTextHover
OR · IS (neutral)backgrounds.secondaryborders.defaulttext.secondary (4.6:1)Whole fill darkens one neutral step; border borders.form
AND (amber)backgrounds.warningSubtle (yellow-50)borders.warningSubtle (yellow-150)text.warningSubtle (yellow-750, 5.5:1)Border only → warningSubtleHover (yellow-750)
IS NOT (red)backgrounds.errorSubtle (red-50)borders.errorSubtle (red-200)text.error (red-500, 5.9:1)Border only → borders.error
LockedIdentical to rest; cursor: not-allowed; not focusable (tabindex="-1"); no hover response
Focusoutline: 2px solid outlines.focus, offset 2px

Data table

Author a real <table class="table">; DS.initDataTable() enhances it from attributes on each <th>. Mirrors lib-design-system's DataTable.

Column attributes and row classes
Attribute on <th> Effect
data-col="key"Makes the column sortable. Without it the column is inert — sorting is opt-in, as in production.
data-sort-typetext (default), number or date. Cells may carry data-sort-value to sort on something other than their visible text.
data-default-dirasc or desc — the direction a newly clicked column starts from.
data-varianttext · numeric · link · emphasisedLink · actions · expansion · custom
data-pinned="start"Sticks the column to the left under horizontal scroll. Requires a .table-scroll wrapper; offsets are measured from real widths, so several columns can pin.
data-pinned="end"Sticks the column to the right — keeps a row action reachable when the table is wider than the viewport. Gets an opaque fill and a left-edge shadow while there is more to scroll.

.table--align-top starts every cell at the top of its row – use it once a column wraps. .cell-marker puts a marker on the same line as a cell's primary text (one flex line, so the text ellipsises rather than dropping below). Row classes are authored, not derived: .table__subrow and .table__subrow-label.

Sorting

Opt-in per column; the active column shows a single arrow.

People, sortable by name, role and status
Name Role Status
Jane SmithEngineeringActive
Tom BrownDesignPending
Alex ChenProductInactive
Spec

aria-sort carries the state. Unsorted columns show bidirectional arrows in text.secondary; the active column a single arrow in controls.primary. Sortable headers take tabindex="0" and respond to Enter and Space. Headers are 12px / 400, text.secondaryOnGray on backgrounds.secondary (5.9:1).

Expandable rows

The parent row carries the latest record; its subrows carry the history.

Sync failures grouped by record, with attempt history
Expand history Record Reason Attempts Last attempt
Priya RamanContact · C3-37228 Duplicate website/social links 3 11 Aug, 08:45
Attempt 2 of 3 duplicate-url 10 Aug, 14:53
Attempt 1 of 3 duplicate-url 7 Aug, 13:20
Ife AdeyemiContact · C3-41006 Email address is not valid 1 1 Sep, 11:37
Spec

A parent row travels with the .table__subrow rows that follow it, so sorting moves the whole group. The chevron is injected only where subrows exist, matching production. Columns that belong to the group rather than the attempt stay blank on subrows.

Read and unread rows Candidate in progress

Not user tested. A dot marks unread; acknowledging greys the row; flag reinstates unread.

Rejections with read state
Record Reason Actions
Not yet acknowledged. Ife Adeyemi Email address is not valid
Acknowledged. Hannah Okonjo Email can't be changed once set in Workday
Spec

Three redundant signals, all present or all absent: a .table__unread dot in controls.primary inside .cell-marker; row text in text.default, dropping to text.secondary (pills to 65%) via .is-acknowledged; and a .table__ack button revealed on hover or focus – check acknowledges, flag reinstates. Strikethrough was trialled and dropped: it obscures text the user may still need.

The reveal uses opacity, so the button stays in the tab order; touch devices pin it visible. The marker carries .sr-only text in both states (SC 1.4.1). Reserve for non-critical progress tracking – a control hidden until hover is one most users never find.

Pinned columns

Pin the identifier to the left and the row's ⋮ actions to the right; both hold under horizontal scroll.

Wide table with a pinned first column and pinned row actions
id timestamp entity status code direction type beameryId atsId traceId Actions
2026-08-11T08:45:47.572Z candidate rejected duplicate-url push workday
2026-08-10T14:53:35.103Z candidate rejected phone-number-or-email-address-required push workday —
Spec

Wrap the table in .table-scroll and set data-pinned="start" or "end" on the <th>. Offsets are measured from rendered widths; edge shadows appear only while there is more to scroll. Long identifiers go in .cell-copy: one line, ellipsised, click to copy.

The actions column is an icon-only btn-subtle with ds-icon-more and an aria-label naming the row. .table-scroll clips positioned children, so DS.initActionMenu floats a menu inside it with fixed positioning, opening upward when there is no room below; scrolling closes it. Cell tooltips must open into the table.

.table-scroll has no border of its own – wrap it in a .card for chrome. Add .table-scroll--fill to take the remaining height of a flex-column parent (display: flex; flex-direction: column; overflow: hidden); that is also what makes the sticky <thead> stick.

Empty and loading states

An empty state when no data; a skeleton stands in before first data.

Nothing matches these filters

Try widening the reason or the time range. If every filter is clear, there is nothing left to fix.

Spec

.table__empty replaces the rows when a filter matches nothing – say what would widen the result, not just that there is nothing. .table__skeleton stands in for the whole table before first data (production's LoadingTable); its shimmer is suppressed under prefers-reduced-motion.

Infinite scroll Candidate for prod

Built for applicant review: batches of 25 load as the list nears its end, with a spinner while they arrive and no pagination footer.

Showing 25 of 75 – scroll the list

    Spec

    Keep an empty .infinite-sentinel as the last child of the scrolling list and call DS.initInfiniteScroll({ root, sentinel, hasMore, loadMore }). When the sentinel comes within 160px of view and hasMore() is true, a .load-more spinner (role="status", announced as “Loading more”) goes in above it while loadMore() runs; return a Promise if it is async. Call .reset() on the returned handle after a new sort or filter replaces the list.

    Alerts

    Alert text uses text.warning (yellow-800) / text.error / text.success on the matching 100-tier fill. Error alerts carry role="alert"; others role="status".

    Success

    Account created successfully.

    Warning

    Your trial expires in 3 days.

    Tooltip

    Tooltip on top Tooltip on the right Tooltip on the bottom Tooltip on the left No hover delay From name This is what recipients see beside the address
    Markup and behaviour

    Brief, plain-text hint. Wrap a focusable trigger in .tooltip with a .tooltip__bubble child. It reveals on hover and keyboard focus after a short delay, and only the hovered or focused one shows. Set data-position to top (default), right, bottom or left — this version does not auto-flip. data-delay="instant" skips the delay.

    <span class="tooltip">
      <button aria-describedby="tip-save">Save</button>
      <span class="tooltip__bubble" id="tip-save" role="tooltip" data-position="top">Saves your changes</span>
    </span>

    When the tip adds information, point aria-describedby at a role="tooltip" bubble. When it only repeats an aria-label (icon-only controls), mark the bubble aria-hidden="true". .info-tip is the 24px icon beside a label: same wrapper, with an aria-label naming what it explains.

    Dialog

    A native <dialog class="dialog"> opened with showModal(), so Escape, focus trapping and the backdrop come from the browser. scripts.js closes it on a backdrop click with returnValue 'cancel'. Put the actions in a <form method="dialog">: each button's value becomes the returnValue, read on close. Cancel takes autofocus, and the highest-priority action sits bottom-right. Return focus to the trigger on close. Named .dialog because many prototypes already define a local .modal.

    Remove this address?

    recruiting@acme.com will no longer be available to send from.

    Settings nav

    Second-level navigation for the Settings area, placed in .app-body between .side-nav and the page. Groups take an uppercase .settings-nav__label; the current page's link carries .active and aria-current="page". Settings is reached from the account menu, so no .side-nav link is active.

    Settings page

    Accessibility (WCAG 2.2 AA)

    • Form-control boundaries are visible at rest. borders.form (neutral-700) gives 5.2:1 on white and 4.6:1 on the neutral-200 fill – comfortably above the 3:1 non-text minimum (SC 1.4.11). Hover state darkens to borders.formActive (neutral-900).
    • Placeholders: forms.placeholder (neutral-700, 4.6:1 on neutral-200) and placeholderOnGray (neutral-750, 5.9:1). Placeholders never substitute for a visible <label for>.
    • Focus ring (0 0 0 2px white, 0 0 0 4px indigo-500) on every interactive element; connector chips use a 2px offset outline. Never remove :focus-visible styles.
    • Hover is supplementary – every interactive element has a resting affordance (border, fill, or underline). Hover/colour change is never the only signal.
    • Text contrast: text.default 16.7:1, text.secondary 5.2:1 on white; connector chips 4.6–5.9:1; white on controls.primary 7.0:1.
    • Targets: 32px controls, 28px chips, 24px .chip-sm – all clear the 24×24 minimum (SC 2.5.8).
    • Error inputs carry aria-invalid="true" + aria-describedby; required fields aria-required="true" plus the visual *.
    • Icon-only buttons carry aria-label. Locked connectors are unfocusable (tabindex="-1") with cursor: not-allowed.
    • Disabled controls are 0.5 opacity (exempt from contrast requirements) but keep their border so the field shape remains discernible.

    TODO

    Tracked here so the reference and the live system stay honest about what exists versus what's planned. Grouped by type; not in priority order.

    Components to build

    • Toasts / notifications — formalise the existing copy-confirmation toast into a documented component.
    • Tabs — role="tablist", arrow-key navigation.
    • Breadcrumbs.
    • Progress indicators.

    Foundations & tokens

    • Motion tokens — standard durations and easings (currently hardcoded 0.15s).
    • Elevation / shadow tokens — documented scale (shadows exist but are undocumented).
    • Breakpoint scale + responsive guidance (only ad-hoc media queries today).
    • i18n considerations.

    Governance & docs

    • Decision guidance — which button variant when; an anti-patterns gallery; microcopy / voice.
    • Accessibility testing checklist (axe + manual screen-reader) and per-component keyboard patterns.