---
description: Kaptio Flux 2.0 design system. Applies to all UI, CSS, markup and design output.
globs: ["**/*.css","**/*.scss","**/*.html","**/*.astro","**/*.tsx","**/*.jsx","**/*.vue","**/*.svelte","**/*.cmp","**/*.md"]
alwaysApply: true
---

# Flux 2.0 — Kaptio design system

All visual output in this repository uses Flux 2.0. This is a constraint, not a
default. Reference: https://flux.kaptio.com · Contract: https://flux.kaptio.com/llms.txt

## Before writing UI

1. Import Flux: `@import url("https://flux.kaptio.com/tokens/v2/flux.css");` or the pinned
   package `@kaptio/flux-tokens`.
2. Reference Tier 2 semantic tokens. They are the only tier that themes.
3. If a token you need does not exist, say so and propose one. Do not invent a value.

## Hard rules

### Tokens

A raw value is invisible to every theme and to every future change. The token layer is the only mechanism by which a change propagates.

- Never write a raw colour. No hex, rgb(), hsl() or named colour in any component or page.
- Never write a raw dimension where a token exists. Spacing, radius, font size, line height, duration and easing all come from tokens. The only sanctioned literal is --flux-hairline (1px).
- Reference Tier 2 semantic tokens for anything with an interface role: text-*, layer-*, surface-*, border-*, icon-*, focus-*.
- Reference Tier 1 primitives only inside a semantic token definition, or for graphic content with no interface role (illustration fill, product accent mark).
- Never use Tailwind arbitrary values: bg-[#056F82], p-[14px], text-[15px]. They bypass the token layer entirely.
- If no token fits, say so and propose one. Do not invent a value.

### Typography

Flux loads two Lexend weights and three JetBrains Mono weights. A request for anything else is synthesised by the browser into a font that is not Lexend.

- Product prototypes are the one exception to the Lexend rules below. A prototype loads flux-prototype.css after flux.css and flux-components.css, and that layer sets its type. Never load it in a deck.
- In a prototype: Google Sans Flex Medium for headings and card titles; Google Sans Flex Bold for KPI values (.flux-kpi, and the Metric component’s value), in text-brand. Manrope Regular for body, tables, inputs, descriptions (.flux-description) and meta (.flux-meta); Manrope Medium for labels; Manrope Semi Bold for buttons and KPI change (.flux-kpi-change). Prototypes have no mono voice.
- flux-prototype.css changes typefaces and weights, never sizes. Size a prototype exactly as you would the Lexend product: the same scale tokens for headings, and the sizes each component already sets.
- Set prototype weights through --flux-prototype-weight-regular, -medium, -semibold and -bold, never as literal numbers.
- Outside prototypes, Lexend at 300 or 700 only. Never 400, 500 or 600.
- Lexend Light (300) is the default for all copy, including headings.
- Lexend Bold (700) for headings that anchor, emphasis, labels and table headers.
- JetBrains Mono is the second voice, for data rather than language: identifiers, references, metadata, values, structural labels, keyboard hints.
- Mono at 400 for code and values, 500 for inline metadata, 700 for small uppercase structural labels.
- Uppercase letter-spaced mono only at --flux-text-xs. Larger reads as a warning label.
- --flux-text-xs is reserved for badges, help text and short metadata. Never use it for paragraph copy, table cells, controls or primary values.
- Use at least --flux-text-sm for controls, table cells and compact secondary copy. Use --flux-text-base for sentences intended to be read. Never reduce type to fit more content in a viewport.
- Never introduce a family Flux does not load. Prototypes use exactly the two that flux-prototype.css loads; everything else uses Lexend.
- Set sizes from the scale (--flux-text-xs through -6xl). Never a literal rem or px.

### Colour

Most of the palette cannot carry text. Treating a mid-tone accent as a text colour is the most common way Flux output goes wrong.

- WCAG 2.2 AA is the contrast floor for the whole system: 4.5:1 for every text pairing, 3:1 for icons, control borders and the focus ring. Body and primary text still target AAA (7:1). Disabled is exempt per 1.4.3. Never invent a colour pairing the contrast audit does not already check.
- Teal carries the system. Other hues appear only when they carry meaning.
- Bumble-Me Yellow (--flux-surface-cta) is for one conversion action per view, and never for navigation. A second yellow element on a view is a defect.
- Yellow never carries white text. Pair --flux-surface-cta with --flux-text-on-cta, which is black.
- Never use a 400-weight accent as text on a light ground. Use a text-* role.
- Status colours are the four documented pairings. Map a new status onto the nearest existing one rather than introducing a hue.
- Map every domain status deliberately before rendering it. When sibling states have materially different consequences, leaving every badge on the neutral base style is a defect.
- A workflow stage is not automatically a warning. Use the yellow pending status only when waiting, delay or required attention is part of the meaning; otherwise use the neutral badge.
- Product accents (--flux-product-*) are graphic only: fills, marks, diagram keys. Never text, never a global theme.
- Chart series take --flux-chart-1 to -4 in order, and --flux-chart-muted for a comparison such as last year. A heatmap takes the --flux-chart-sequential-1 to -5 ramp. Never a chart library’s palette, a product accent or a status colour.
- Never use colour as the only signal for a state.

### Layout and geometry

Consistent spacing and near-square geometry are what make output read as Kaptio rather than as generic.

- All spacing comes from the 4px scale. If none of the steps fit, the structure is wrong, not the scale.
- Interface radius is --flux-radius-sm (4px): buttons, fields, cards, tables, notes. --flux-radius-md for modals only.
- --flux-radius-full is for pills, avatars, radio buttons, switches and the spinner ring, nothing else.
- Separate sections with --flux-seam-gap.
- Constrain reading columns to --flux-measure-prose.
- Text and controls never sit directly against the edge of a bordered or filled surface. Use the component padding token; tables apply inset through their cells, and intentional edge-to-edge media is the exception.
- Action rows keep the same inline inset as the content above and use a spacing token between actions. Wrap or stack the actions before a control touches the container edge.
- Do not scale an application with CSS zoom or transform, and do not compress type or spacing to make the whole workflow fit above the fold. Reflow the layout, wrap controls and let the page scroll; wrap wide tables in their horizontal scroll container.
- Never mix px into a layout built from rem tokens. The root size is fluid, so they diverge as the viewport grows.

### Elevation

Flux is a flat system. Depth is reserved for things that genuinely float, so that when it appears it means something.

- Separate nested surfaces with a layer step (--flux-layer-01 → -02 → -03) and a hairline. Never hand-pick a grey. Nest a surface only when it is a different kind of thing, such as a note or a table inside a card, never to group content that spacing could group.
- Shadow is for overlays, popovers and modals. --flux-shadow-md is the ceiling for anything in normal flow.
- Never use a coloured shadow or a glow.
- Never stack a border, an inner glow and a heavy shadow on the same container.
- Never nest three layer levels on the deep theme; layer-03 aliases layer-02 there.

### Motion

Motion in Flux confirms that something happened. Anything that draws attention to itself is describing the animation rather than the change.

- Default to --flux-duration-fast with --flux-ease-productive.
- Name the properties being transitioned. Never use transition: all.
- Animate opacity, transform and colour. Never width, height, top, left, margin or padding.
- Never scale, lift or grow on hover. Flux buttons change colour and nothing else.
- Never exceed --flux-duration-slow (300ms) in product UI. The Spinner and the Skeleton pulse are the exceptions: they are loops, not transitions, and run at --flux-spinner-duration and --flux-skeleton-duration.
- Do not write your own prefers-reduced-motion reset. The base layer handles it globally.

### Accessibility

Flux guarantees contrast and focus. Everything structural is the implementer’s responsibility, and it is where generated UI code fails most often.

- Do not ship a text or control pairing below WCAG 2.2 AA. Use a semantic text-*, icon-* or border-strong role on a documented ground so the audited pairings apply. A raw colour, or a primitive used as text, is already a defect.
- Never remove the focus outline. Focus is teal, 2px, offset 2px, in every theme, applied by the base layer.
- Use native elements before ARIA: button, a, dialog, table, input. A div with a click handler is not a button.
- Select and Date picker are the exception: never render a native select element or a date-type input, because their menus and calendars draw in the operating system style. Copy the documented Flux markup and load flux-components.js.
- Open modals with dialog.showModal(), never by toggling a class. Focus trapping, page inertness and Escape come from the platform.
- Every input needs a visible, programmatically associated label. A placeholder is not a label.
- Bind error text to its field with aria-describedby, and set aria-invalid.
- Explanatory field text — what a field means, its format, its constraints — goes in the documented info bubble beside the label, never as text below the field. Only an error message sits below a field. Reference the bubble from the control with aria-describedby.
- Every icon-only control needs an accessible name.
- On a coarse pointer every control is at least --flux-touch-target (44px) tall. Flux components do this themselves; a custom control sizes from the component tokens or sets min-block-size: var(--flux-touch-target) under @media (pointer: coarse).
- Tabs need roving tabindex: the selected tab is tabindex="0", the rest are tabindex="-1".
- Table headers need scope. Numeric columns are right-aligned with tabular figures.
- Choose heading levels for document structure, and size them with tokens.

### Themes

A component that references roles works in three themes for free. A component that references colours works in one.

- Never write theme-specific CSS inside a component. If you need to, a semantic token is missing — say which.
- Test new work in light, dark and deep before considering it done.
- A scoped theme region must set its own background from --flux-surface-page or a layer-* token.
- Deep is a brand ground for slides and marketing, not a dark mode for dense UI.

## Composition discipline

These are not token violations. They are the habits that make output look
generic even when every value is correct.

- No gradient fills on interface surfaces. Flat colour, or nothing.
- No decorative coloured bar along the edge of a card.
- No coloured dot beside a heading.
- No emoji as an icon system. Use a consistent icon set.
- No glow, no neon, no glassmorphism, no heavy blur behind content.
- No centred body copy. Centre a heading and a lede at most.
- No three-column feature grid of icon-plus-heading-plus-paragraph as a default layout.
- No badge on something that is not a status.
- Use eyebrows — the small uppercase label above a heading — and numbering sparingly. An eyebrow must add context the heading does not, so never put one above every heading or section. Number items only when order is the meaning, such as steps or a sequence, and never as section decoration like 01, 02, 03.
- No hero section with a gradient mesh background.
- Do not stack decoration: pick one of border, shadow, or fill change to signal a state.
- Do not add a container just to add a container. If it holds one thing, it is not grouping anything.
- Build every prototype view from documented Flux components. Prototype CSS places them — grid, flex, gap, spacing and size — and never draws a background, box border, radius or shadow, sets a font family, or restyles a .flux-* class. A class named like a component (booking-card, status-pill) is a hand-built copy of one. Run npx flux-check --prototype . before handoff.
- Do not nest boxes. A region has one bordered or filled surface, and everything inside it is grouped by spacing, a heading or a hairline divider — never by another bordered box. A Metric row inside a card drops its own surface automatically; do not rebuild one with borders. Count the outlines between the page edge and a value: more than one inside the page content means remove the inner ones.
- Do not remove a documented component’s internal padding. A parent inset does not replace the inset inside a card, note, table cell or action row.
- Do not turn every value into a card. Group related metrics on one surface with the Metric component and let the important exception carry the emphasis.
- Do not shrink the interface to show everything at once. Preserve readable type and row rhythm, then use scrolling, pagination or progressive disclosure for the remainder.
- Prefer one strong element over three competing ones. Density is not hierarchy.

## Components

48 components are documented with live specimens, markup, states, keyboard
behaviour and token bindings. Read the plain-text source before building one
from scratch:

    https://flux.kaptio.com/components/source/<name>.txt

Available: button, input, select, date-picker, checkbox, switch, slider, search, card, description-list, metric, chart, heatmap, timeline, itinerary, calendar, map, flow, app-shell, spinner, progress, skeleton, accordion, toast, reorder, board, editable-cell, tooltip, empty-state, breadcrumbs, pagination, segmented-control, stepper, walkthrough, split-view, file-upload, assistant-panel, rich-text-editor, badge, table, menu, kbd, command-palette, modal, drawer, note, tabs, avatar.

If the repository already has a component for the job, extend it. Do not create
a second implementation of a documented component.

## Slide decks

Decks are a different surface with a different contract. If you are asked for a
presentation, stop reading this file and read that one instead:

    https://flux.kaptio.com/decks/llms.txt

The two disagree deliberately — a slide relaxes scale, ground and bleed in ways
a dashboard cannot. Do not average them. Never load flux-deck.css into product
UI: it overrides the fluid root, which is correct for a deck and wrong for an
application. Never use --flux-display-* sizes outside a deck.

## Verify

Run the compliance check before you consider UI work finished:

    npx flux-check src/

It reports the violations that can be detected mechanically — raw colours,
arbitrary Tailwind values, synthesised font weights, transition: all, removed
focus outlines, gradients, raw dimensions, divs with click handlers, native
selects and date inputs, help text below fields, table headers without scope,
token fallbacks that hide a missing flux.css, raw slide grounds, Lexend loaded at
extra weights — each with the rule name and the fix. It exits non-zero,
so it also works as a CI gate.

In a product prototype, run it as npx flux-check --prototype . instead. That
adds the composition rules: Flux loaded from the published URL or package, no
hand-built surfaces, no restyled components, and no classes named like a
documented component.
Without the package installed, download the same check from
https://flux.kaptio.com/tokens/v2/check.mjs and run it with node.

Fix every finding. If one is genuinely correct, annotate the line above it with
the reason and the rule name:

    /* Glyph, not a surface: gradients inherit currentColor, an SVG cannot.
       flux-ignore-next-line gradient */

## Self-check before returning UI code

- No raw hex, rgb() or named colour anywhere.
- No raw px, rem or ms where a token exists (--flux-hairline excepted).
- No Tailwind arbitrary values.
- Lexend at 300 or 700 only — except in a prototype, which loads flux-prototype.css and sets weights through its tokens.
- No paragraph, table cell, control or primary value set at --flux-text-xs.
- No CSS zoom, scaled application shell or fit-to-viewport compression.
- Radius is --flux-radius-sm on every control.
- At most one yellow CTA element.
- Text and control pairings meet WCAG 2.2 AA (4.5:1 text, 3:1 non-text). Use semantic roles, not a colour that looks dark enough.
- Focus outline present and not overridden.
- Native elements used for buttons, links, dialogs and tables.
- No native select or date input. Select and Date picker use the documented Flux markup with flux-components.js loaded.
- In a prototype, every element is a documented Flux component placed by layout-only CSS, and npx flux-check --prototype . passes.
- No explanatory text below fields. Explanation is in the info bubble beside the label; only errors sit below.
- No transition: all, and no scale or lift on hover.
- Would this render correctly with data-flux-theme="dark" on the root? If any
  part would not, that part is referencing a primitive or a raw value.
