Futurism Design System
A web design language: Italian-Futurist energy, restrained editorial execution.
Motion, diagonals, one red, bold italic display — on warm paper (light) or warm
carbon (dark).
Use this skill to produce UI that obeys the system instead of generic AI styling.
The look is fully specified; your job is to apply it faithfully.
How to apply
- Reuse the stylesheet — do not re-derive it. The complete kit lives in
assets/futurism.css. Link or inline it; never hand-rewrite the tokens or
component CSS. Behaviour for interactive parts is in assets/futurism.js.
- For a single self-contained file (artifact, demo), inline both assets.
- For a project, copy
assets/futurism.css + assets/futurism.js in and
import them once at the root. Both headers carry a version stamp — keep
it in the vendored copy. To check for or apply an upgrade, compare that stamp
against the plugin's CHANGELOG.md (one line per version) and read only the
newer entries — don't diff the kit files; a kit update is a wholesale
re-vendor either way.
- When you need exact values or component markup, read the references:
references/tokens.md — color/spacing/type/motion tokens + the :root
and [data-theme="dark"] blocks.
references/components.md — copy-paste HTML for every component, with the
classes and the small JS hooks they need.
- Default theme is light. Enable dark by setting
data-theme="dark" on the
root element (<html>); call fdTheme() to flip it.
The eight laws
These are what make output read as this system rather than a red-tinted
generic site. Break one and the aesthetic collapses, so hold them unless the
user explicitly overrides:
- Square corners always.
border-radius: 0. The system has no rounding —
the tension between hard rectangles and skewed type is the point.
- Solid offset shadows, never blur.
box-shadow: 6px 6px 0 var(--shadow).
A blurred shadow instantly reads as Material/SaaS, not Futurism.
Gotcha 1: the offset shadow is clipped by any overflow:auto/hidden
ancestor. Keep the shadowed element's scroll container at overflow:visible
and move the scroll inward (this is why the modal sets dialog{overflow:visible}
and scrolls inside .modal).
Gotcha 2: in dark, --shadow follows the accent, so an accent-filled control
must throw an ink shadow, not --shadow — an accent shadow under an accent fill
is the same hue and smears into one blob. Surfaces (cards/modals on --surf) are
fine with --shadow; only same-hue fills need the override (see .btn-primary:hover).
Density & scale: the offset follows surface weight — 10px hero (modal),
6px default (card, popovers), 4px dense/stacked (already kit vocabulary:
.btn.ok, .tip). Color scales with density too: the dark accent shadow is right
for a singular surface but a scrolling stack of repeated shadowed cards floods
the screen red and buries the one legitimate accent moment (an error card) — so
repeated siblings in a list use 4px 4px 0 var(--shadow-mute) (ink in light,
true carbon in dark); the accent shadow stays reserved for singular surfaces.
- 2px ink borders on surfaces and controls — structure is drawn, not
implied by elevation.
- One accent only. Red (
--accent) carries links, CTAs, rules, highlights.
Adding a second hue dilutes the manifesto. If you need differentiation, use
ink vs. red vs. outline, not new colors. To show a scale or ordinal range
(DPI low→high, weak→strong), use a single-hue intensity ramp
(linear-gradient(90deg, var(--muted), var(--accent))) — never a rainbow
blue→red gradient or a set of multi-hue swatches. And don't hand-roll
multi-color "category" chips: a single-select set is .seg, a static tag is
.badge.out.
Status follows the same discipline: ink = done/committed, accent =
error/attention/live, muted = idle/disabled — never a green success or an
amber warning (success rides the label: "SAVED ✓"; urgency rides blink rate,
see .dot.warn). Budget the accent per view: one accent-filled CTA per bar
or view region — when everything points red, nothing does.
Two-step confirm is a state, not just the .row-act widget. The idle →
armed → confirmed flow (auto-disarm on timeout, Esc, or outside-click) rides
this same discipline — ink/muted at rest → accent when armed, the armed step
being the attention moment — and is reusable independent of the row chrome.
fdConfirm is the row-bound realization; for a destructive toolbar button,
menu item, or keyboard-triggered action, drive the same arm-state and swap the
label (armed = accent "sure?"/"Confirm X") rather than misusing .row-act or
firing a bare native confirm().
- Motion is machine, not toy. Fast (
--fast: .12s), medium (--med: .2s),
easing cubic-bezier(.2,.9,.1,1), and always directional — things slide,
dart, lurch, march. Never spring, bounce, or fade-puff. Respect
prefers-reduced-motion (the CSS already does).
Narrow carve-out: --ease is for discrete state transitions (open/close,
hover, armed/refreshing). A value live-tracked to a pointer every frame — not
a state transition — is exempt: .pull-fill's width uses transition:width 60ms linear because a cubic-bezier visibly lags a 1:1 finger-tracked drag (and
.pull's height gets the same treatment via the .pulling class fdPull toggles
during the drag — --ease resumes on release). Don't generalize this to
anything that isn't following a pointer in real time.
- Skewed CTAs. Primary actions use
transform: skewX(-8deg) with the label
counter-skewed (skewX(8deg)) so it stays upright. This is the speed cue.
Reserve the skew for standalone/hero CTAs and rows of skewed siblings (a
confirm+cancel pair, a button group) — there the parallel slants read as
deliberate. A lone skewed button sitting flush against a square element (an
input+action group, one action in a square toolbar) makes an awkward wedge gap
against the straight edge: use a square affordance there — .btn-square
(un-skewed .btn), .iconbtn, or a welded group — so edges align.
- Never trust native form popups.
<select>, date pickers, etc. render with
OS chrome that ignores the system. Use the custom .sel component (and the
same approach for any other native popup) so the whole control is on-brand.
Its dropdown uses progressive enhancement: the CSS default is
position:absolute, self-anchored under the trigger, so it works with the
stylesheet alone (no JS, or your own select JS) and is immune to ancestor
transforms. When futurism.js is loaded, fdSelPosition() promotes it to
position:fixed anchored to the trigger, so it escapes a scrolling
overflow:auto ancestor (modal/<dialog>, card with internal scroll) and
flips up near a bottom edge. So: vendor only the CSS → a working (absolute)
dropdown that's clipped only inside an overflow ancestor; add the JS → the
top-layer escape too. Any custom popup you build for a native-popup replacement
should follow the same pattern (a CSS default that stands alone).
Gotcha (JS/fixed mode only): a position:fixed element is re-rooted by any
ancestor with transform, filter, perspective, will-change, contain, or
backdrop-filter — that ancestor becomes its containing block, breaking the
anchoring and dropping it behind a modal backdrop in Chromium. Either keep those
off a scrolling/modal ancestor of a .sel (animate modal entrance with
opacity, not a persistent transform — the kit's own fd-lurch keyframe
is safe because it ends at transform:none with no fill-mode, so the containing
block dissolves when the entrance finishes; the hazard is a transform that sticks
around), or simply don't load the anchoring JS for that select — the absolute
default is transform-safe.
- Theme native
<button> explicitly. Buttons don't inherit color — the UA
gives them ButtonText (dark), which becomes dark-on-dark in the carbon theme,
and an inline background beats class rules. So every button must set its
color/background from tokens; never rely on inheritance. The kit's
button{} reset and each .btn/.iconbtn variant already do this — match it
when you add a new button-like control.
Type discipline
- One family:
'Helvetica Neue', Arial, sans-serif (the --font token).
- Display/headings carry the drama: weight 900, italic, negative tracking,
tight line-height. Body stays calm: regular weight, line-height 1.55,
--muted color, comfortable size. Do not make body text italic or heavy.
- Kickers/labels: 11px, uppercase, wide letter-spacing, in accent or ink.
Iconography
Icons are inline stroke SVG in currentColor, never emoji. Emoji bring their
own color (breaks law 4's one-red) and soft rounded shapes (breaks the square,
flat look), and they won't theme. A currentColor SVG inherits the token color
and flips on hover for free. Keep stroke-linecap:square /
stroke-linejoin:miter so the icon's corners match the 2px-border hardness.
Monospace text glyphs (keycaps, arrows like →) are fine — they're machined, not
pictorial.
Theming
Every component is built on CSS variables, so light/dark is a single attribute
flip — no per-component dark overrides. When adding a new component, express all
colors as the existing tokens (--bg, --surf, --ink, --muted, --accent,
--line, --shadow, --field, --on-accent, --scrim) and it will theme for
free. If a value can't be expressed in tokens, add a new token to both theme
blocks rather than hard-coding a hex.
Text on an accent fill is --on-accent, never --ink. Any text or icon sitting
on a var(--accent) background uses color:var(--on-accent) — its paired foreground
in each theme (cream-on-red in light, near-black-on-red in dark). --ink is the
opposite in both themes, so an ink label on an accent fill is the wrong, low-contrast
foreground. Every accent-filled control already does this (.btn-primary, .badge.red,
.sel-opt:hover, the skewed header mark); match it. This also applies to a partial
fill — if an accent bar grows under a centered label (a progress/pull indicator), the
label must flip to --on-accent once the fill is behind it.
Overlays use --scrim, never --ink. Backdrops must dim, so they need a
theme-independent dark wash. --ink is the cream foreground in dark — an ink
backdrop would lighten the page. --scrim (rgba(0,0,0,.55)) is the same in both
themes for exactly this reason.
Machine-readable artifacts own their contrast. A QR code, barcode, or other
scannable artifact never themes: it renders literal ink-on-paper hexes
(#1a1714 modules on #efe9dc) in both themes — scanners want dark-on-light,
and tokens would invert it on carbon — framed with the standard 2px ink border so
the fixed-contrast block still sits on the grid. Don't express artifact pixels in
tokens; a caption next to it is ordinary UI and stays tokenized (ink, not accent —
it's decorative, see law 4's budget).
Runtime accent. Law #4 is one accent, but letting the user pick that one
accent is on-system — see the .accpick component and fdAccent(). Three couplings
to respect: in dark the offset shadow follows the accent (--shadow = accent),
in light it stays ink; fdAccent() also re-derives --on-accent from the picked
color's luminance (choosing the kit's cream or near-black, whichever contrasts) so
text/icons on an accent fill stay legible for any accent — for the shipped red this
reproduces the static tokens, and an accent entry can override with onLight/onDark;
and a theme control (the .switch pill or a .toggle)
must use ink for its active/.on state, not accent — otherwise the control that
picks the accent visibly absorbs it. (.switch .act already fills ink, matching
the ink = selected convention of .seg/.tab/.pager.)
Accessibility
The custom controls are div-built, so futurism.js carries their semantics — keep
it loaded. fdInit() runs on load and wires ARIA roles + keyboard on .sel
(button + listbox popup, Enter/↑↓/Esc), .tabs (tablist, ←/→ roving), and .toggle
(switch, Space/Enter); call fdInit() again after injecting components at runtime.
Every focusable control shows the same 3px accent :focus-visible ring — don't
remove it. Native elements stay native on purpose: checkbox/radio are styled
<input>s, and the modal is a real <dialog> (Tab-trap + Esc); give the dialog
an autofocus button. When an overlay restores focus to its opener on close,
guard the opener's isConnected first — a captured opener can be removed while
the overlay is open (a deleted row, a closed tab), and focusing a detached node
throws or dumps focus to <body>; fall back to a stable target then. Overlays dim
with --scrim, motion respects
prefers-reduced-motion (the live .dot keeps a static ring there), and a
forced-colors block re-asserts selected/active state for Windows High Contrast.
Scope. The interactive controls require futurism.js — with JS off, the custom
select/tabs/toggle/drawer/toast are inert (use native elements if you need a no-JS
fallback). The motion language is directional and LTR-oriented (skews, slide-ins,
darting underlines); for RTL, flip those with :dir(rtl) overrides.
SPA / framework use. The kit drives state through DOM classes (.on/.open)
and imperative ARIA, with one set of document-level delegates. Treat those controls
as uncontrolled — don't let React/Vue re-render over the toggled classes — or port
the control to framework state. Call fdInit() from your mount hook for components
added after load; the document delegates are attached once and keep working.
Responsive & touch
The laws are resolution-independent, but the layout patterns below keep an app
usable on a phone without breaking the aesthetic:
- Off-canvas drawer. A fixed sidebar slides in over a
--scrim backdrop on
mobile and becomes static at your desktop breakpoint. Use .drawer /
.scrim-bg + fdDrawer().
- Touch targets ≥ ~40px. The kit already grows icon buttons, keycaps, accent
swatches, pager buttons, toggles, and select rows to ~40px+ under
@media (max-width:640px) — the 24–32px desktop sizes are too small for thumbs.
Match that bump for any new small control you add.
- No horizontal body scroll. Contain wide content (tables, tab bars, command
rows) with
overflow-x:auto on that element; never let the page body scroll
sideways. Add min-width:0 to flex children that hold ellipsised text.
- Collapse header chrome. Hide the wordmark/subtitle and toggle labels at
narrow widths, keeping just the logo mark + essential controls. Use
.only-desktop
/ .only-mobile for chrome that swaps entirely (a desktop hint vs. a mobile-worded
one, not just a shrink) — .only-desktop is hidden by default and shown
(display:block) at the min-width:768px breakpoint; .only-mobile is the
inverse. The base utility only sets display:none/block; if the shown state
needs flex/inline-flex/inline-block instead, mirror the kit's own two-rule
shape with a compound selector rather than overriding just one side — e.g.
.keyhint.only-desktop{display:none} plus
@media (min-width:768px){.keyhint.only-desktop{display:flex}}. A compound
selector outranks the utility's single-class rule on any specificity tie, but
only within the media condition it's written under — a single unscoped override
would win at the hidden breakpoint too and defeat the hide, so give your override
the same two-rule shape as the utility itself, just with your display value
swapped in.
- Popover, not inline rows, on mobile. A wide control (e.g. the 7-swatch
accent row) collapses to a single trigger + popover — that's what
.accpick is.
- App-scale surfaces. A fullscreen panel is a composition, not a page: cap a
single column at ~680px only when the content is genuinely linear (a form, an
article), otherwise let a grid fill the surface (form + live preview side by
side, chart rows). A desktop panel with a dead right half reads as a mobile
layout blown up.
- Compact/preview projections. A live miniature of a running surface (a dock
preview, picture-in-picture) needs its own type scale: hide chrome and controls,
keep the live data, and pin font sizes in px — fluid
clamp()/vw display
sizes are computed from the viewport, not the ~200px card they're projected
into, so they overflow it.
Drive these with min-width media queries (mobile-first): style the compact
layout as the default, then restore the desktop layout at @media (min-width:768px).
When extending the kit
New components must obey the eight laws and use only tokens. Match the existing
motion vocabulary (slide/dart/lurch/march at --fast/--med). Add the component
to references/components.md so the kit stays the single source of truth.
The kit already carries these conventions on its own components; a new affordance
that doesn't generalize them silently breaks parity. So for any new control:
- Accent fill →
--on-accent text (never --ink). See Theming.
- Loading/progress → reuse
fd-march (the barber-pole), never a rotary spinner;
shape a .skel-style placeholder, don't invent a new loader.
- A status/selected/loading affordance needs a forced-colors fallback. Color-mix
fills, gradients, and
box-shadow are stripped under @media (forced-colors:active)
— re-assert the cue with a border/outline or Highlight, the way the kit does for
.skel/.prog/.dot and the selected states. Data-viz strokes (sparklines,
chart paths) re-stroke with CanvasText so the data survives too.
- A state that changes without focus moving (a toast, a "refreshing" indicator)
needs
role="status" + aria-live so it's announced — match .toast.
- Progressive enhancement: a component's CSS should stand alone; if it needs JS to
be usable (positioning, etc.), give it a working CSS default and let the JS upgrade
it (see the
.sel dropdown — absolute default, JS promotes to fixed).
1---2name: futurism-design3description: Apply the Futurism Design System — a Paper-Futurist web aesthetic (bold italic display type, a single red accent, square corners, solid offset shadows, and fast directional motion) with paired light and dark themes. Use this skill whenever the user asks to build, style, or restyle any web UI — a page, component, landing hero, dashboard, form, or full app — AND wants it to follow the Futurism look, OR invokes /futurism-design, OR references "futurism", "futurist", "paper futurism", "the red/cream design", or "our design system" in this repo. Trigger it even when the user only says "make a page" or "build a component" in a project that has adopted this system, so the output stays on-brand instead of defaulting to generic styling. This is an explicit, web-only design language — do not use it for terminal/CLI output.4---56# Futurism Design System78A web design language: **Italian-Futurist energy, restrained editorial execution.**9Motion, diagonals, one red, bold italic display — on warm paper (light) or warm10carbon (dark).1112Use this skill to produce UI that obeys the system instead of generic AI styling.13The look is fully specified; your job is to apply it faithfully.1415## How to apply16171. **Reuse the stylesheet — do not re-derive it.** The complete kit lives in18 `assets/futurism.css`. Link or inline it; never hand-rewrite the tokens or19 component CSS. Behaviour for interactive parts is in `assets/futurism.js`.202. For a single self-contained file (artifact, demo), inline both assets.213. For a project, copy `assets/futurism.css` + `assets/futurism.js` in and22 import them once at the root. Both headers carry a **version stamp** — keep23 it in the vendored copy. To check for or apply an upgrade, compare that stamp24 against the plugin's `CHANGELOG.md` (one line per version) and read only the25 newer entries — don't diff the kit files; a kit update is a wholesale26 re-vendor either way.274. When you need exact values or component markup, read the references:28 - `references/tokens.md` — color/spacing/type/motion tokens + the `:root`29 and `[data-theme="dark"]` blocks.30 - `references/components.md` — copy-paste HTML for every component, with the31 classes and the small JS hooks they need.325. Default theme is **light**. Enable dark by setting `data-theme="dark"` on the33 root element (`<html>`); call `fdTheme()` to flip it.3435## The eight laws3637These are what make output read as *this* system rather than a red-tinted38generic site. Break one and the aesthetic collapses, so hold them unless the39user explicitly overrides:40411. **Square corners always.** `border-radius: 0`. The system has no rounding —42 the tension between hard rectangles and skewed type is the point.432. **Solid offset shadows, never blur.** `box-shadow: 6px 6px 0 var(--shadow)`.44 A blurred shadow instantly reads as Material/SaaS, not Futurism.45 *Gotcha 1:* the offset shadow is **clipped by any `overflow:auto/hidden`46 ancestor**. Keep the shadowed element's scroll container at `overflow:visible`47 and move the scroll inward (this is why the modal sets `dialog{overflow:visible}`48 and scrolls inside `.modal`).49 *Gotcha 2:* in dark, `--shadow` follows the accent, so an **accent-filled control50 must throw an ink shadow, not `--shadow`** — an accent shadow under an accent fill51 is the same hue and smears into one blob. Surfaces (cards/modals on `--surf`) are52 fine with `--shadow`; only same-hue fills need the override (see `.btn-primary:hover`).53 *Density & scale:* the offset follows surface weight — **10px** hero (modal),54 **6px** default (card, popovers), **4px** dense/stacked (already kit vocabulary:55 `.btn.ok`, `.tip`). Color scales with density too: the dark accent shadow is right56 for a *singular* surface but a scrolling stack of repeated shadowed cards floods57 the screen red and buries the one legitimate accent moment (an error card) — so58 **repeated siblings in a list use `4px 4px 0 var(--shadow-mute)`** (ink in light,59 true carbon in dark); the accent shadow stays reserved for singular surfaces.603. **2px ink borders** on surfaces and controls — structure is drawn, not61 implied by elevation.624. **One accent only.** Red (`--accent`) carries links, CTAs, rules, highlights.63 Adding a second hue dilutes the manifesto. If you need differentiation, use64 ink vs. red vs. outline, not new colors. To show a **scale or ordinal range**65 (DPI low→high, weak→strong), use a single-hue **intensity ramp**66 (`linear-gradient(90deg, var(--muted), var(--accent))`) — never a rainbow67 blue→red gradient or a set of multi-hue swatches. And don't hand-roll68 multi-color "category" chips: a single-select set is `.seg`, a static tag is69 `.badge.out`.70 Status follows the same discipline: **ink = done/committed, accent =71 error/attention/live, muted = idle/disabled** — never a green success or an72 amber warning (success rides the label: "SAVED ✓"; urgency rides blink rate,73 see `.dot.warn`). Budget the accent per view: **one accent-filled CTA per bar74 or view region** — when everything points red, nothing does.75 *Two-step confirm is a **state**, not just the `.row-act` widget.* The idle →76 armed → confirmed flow (auto-disarm on timeout, Esc, or outside-click) rides77 this same discipline — **ink/muted at rest → accent when armed**, the armed step78 being the attention moment — and is reusable independent of the row chrome.79 `fdConfirm` is the row-bound realization; for a destructive **toolbar button,80 menu item, or keyboard-triggered action**, drive the same arm-state and swap the81 label (armed = accent "sure?"/"Confirm X") rather than misusing `.row-act` or82 firing a bare native `confirm()`.835. **Motion is machine, not toy.** Fast (`--fast: .12s`), medium (`--med: .2s`),84 easing `cubic-bezier(.2,.9,.1,1)`, and always **directional** — things slide,85 dart, lurch, march. Never spring, bounce, or fade-puff. Respect86 `prefers-reduced-motion` (the CSS already does).87 *Narrow carve-out:* `--ease` is for **discrete state transitions** (open/close,88 hover, armed/refreshing). A value **live-tracked to a pointer every frame** — not89 a state transition — is exempt: `.pull-fill`'s width uses `transition:width 60ms90 linear` because a cubic-bezier visibly lags a 1:1 finger-tracked drag (and91 `.pull`'s height gets the same treatment via the `.pulling` class fdPull toggles92 during the drag — `--ease` resumes on release). Don't generalize this to93 anything that isn't following a pointer in real time.946. **Skewed CTAs.** Primary actions use `transform: skewX(-8deg)` with the label95 counter-skewed (`skewX(8deg)`) so it stays upright. This is the speed cue.96 *Reserve the skew for standalone/hero CTAs and rows of skewed siblings* (a97 confirm+cancel pair, a button group) — there the parallel slants read as98 deliberate. A lone skewed button sitting **flush against a square element** (an99 input+action group, one action in a square toolbar) makes an awkward wedge gap100 against the straight edge: use a **square affordance** there — `.btn-square`101 (un-skewed `.btn`), `.iconbtn`, or a welded group — so edges align.1027. **Never trust native form popups.** `<select>`, date pickers, etc. render with103 OS chrome that ignores the system. Use the custom `.sel` component (and the104 same approach for any other native popup) so the whole control is on-brand.105 Its dropdown uses **progressive enhancement**: the CSS default is106 `position:absolute`, self-anchored under the trigger, so it **works with the107 stylesheet alone** (no JS, or your own select JS) and is immune to ancestor108 transforms. When `futurism.js` is loaded, `fdSelPosition()` promotes it to109 `position:fixed` anchored to the trigger, so it **escapes a scrolling110 `overflow:auto` ancestor** (modal/`<dialog>`, card with internal scroll) and111 flips up near a bottom edge. So: vendor only the CSS → a working (absolute)112 dropdown that's clipped only inside an overflow ancestor; add the JS → the113 top-layer escape too. Any custom popup you build for a native-popup replacement114 should follow the same pattern (a CSS default that stands alone).115 *Gotcha (JS/fixed mode only):* a `position:fixed` element is re-rooted by any116 ancestor with `transform`, `filter`, `perspective`, `will-change`, `contain`, or117 `backdrop-filter` — that ancestor becomes its containing block, breaking the118 anchoring and dropping it behind a modal backdrop in Chromium. Either keep those119 off a scrolling/modal ancestor of a `.sel` (animate modal entrance with120 **`opacity`**, not a *persistent* `transform` — the kit's own `fd-lurch` keyframe121 is safe because it ends at `transform:none` with no fill-mode, so the containing122 block dissolves when the entrance finishes; the hazard is a transform that sticks123 around), or simply **don't load the anchoring JS for that select** — the absolute124 default is transform-safe.1258. **Theme native `<button>` explicitly.** Buttons don't inherit `color` — the UA126 gives them `ButtonText` (dark), which becomes dark-on-dark in the carbon theme,127 and an inline `background` beats class rules. So every button must set its128 `color`/`background` from tokens; never rely on inheritance. The kit's129 `button{}` reset and each `.btn`/`.iconbtn` variant already do this — match it130 when you add a new button-like control.131132## Type discipline133134- One family: `'Helvetica Neue', Arial, sans-serif` (the `--font` token).135- **Display/headings carry the drama**: weight 900, italic, negative tracking,136 tight line-height. **Body stays calm**: regular weight, line-height 1.55,137 `--muted` color, comfortable size. Do not make body text italic or heavy.138- Kickers/labels: 11px, uppercase, wide letter-spacing, in accent or ink.139140## Iconography141142Icons are **inline stroke SVG in `currentColor`**, never emoji. Emoji bring their143own color (breaks law 4's one-red) and soft rounded shapes (breaks the square,144flat look), and they won't theme. A `currentColor` SVG inherits the token color145and flips on hover for free. Keep `stroke-linecap:square` /146`stroke-linejoin:miter` so the icon's corners match the 2px-border hardness.147Monospace text glyphs (keycaps, arrows like `→`) are fine — they're machined, not148pictorial.149150## Theming151152Every component is built on CSS variables, so light/dark is a single attribute153flip — no per-component dark overrides. When adding a new component, express all154colors as the existing tokens (`--bg`, `--surf`, `--ink`, `--muted`, `--accent`,155`--line`, `--shadow`, `--field`, `--on-accent`, `--scrim`) and it will theme for156free. If a value can't be expressed in tokens, add a new token to both theme157blocks rather than hard-coding a hex.158159**Text on an accent fill is `--on-accent`, never `--ink`.** Any text or icon sitting160on a `var(--accent)` background uses `color:var(--on-accent)` — its paired foreground161in each theme (cream-on-red in light, near-black-on-red in dark). `--ink` is the162*opposite* in both themes, so an ink label on an accent fill is the wrong, low-contrast163foreground. Every accent-filled control already does this (`.btn-primary`, `.badge.red`,164`.sel-opt:hover`, the skewed header mark); match it. This also applies to a *partial*165fill — if an accent bar grows under a centered label (a progress/pull indicator), the166label must flip to `--on-accent` once the fill is behind it.167168**Overlays use `--scrim`, never `--ink`.** Backdrops must *dim*, so they need a169theme-independent dark wash. `--ink` is the cream foreground in dark — an ink170backdrop would lighten the page. `--scrim` (`rgba(0,0,0,.55)`) is the same in both171themes for exactly this reason.172173**Machine-readable artifacts own their contrast.** A QR code, barcode, or other174scannable artifact never themes: it renders literal ink-on-paper hexes175(`#1a1714` modules on `#efe9dc`) in **both** themes — scanners want dark-on-light,176and tokens would invert it on carbon — framed with the standard 2px ink border so177the fixed-contrast block still sits on the grid. Don't express artifact pixels in178tokens; a caption next to it is ordinary UI and stays tokenized (ink, not accent —179it's decorative, see law 4's budget).180181**Runtime accent.** Law #4 is one accent, but letting the *user* pick that one182accent is on-system — see the `.accpick` component and `fdAccent()`. Three couplings183to respect: in dark the offset shadow follows the accent (`--shadow` = accent),184in light it stays ink; `fdAccent()` also **re-derives `--on-accent`** from the picked185color's luminance (choosing the kit's cream or near-black, whichever contrasts) so186text/icons on an accent fill stay legible for *any* accent — for the shipped red this187reproduces the static tokens, and an accent entry can override with `onLight`/`onDark`;188and a **theme control** (the `.switch` pill or a `.toggle`)189must use **ink** for its active/`.on` state, not accent — otherwise the control that190*picks* the accent visibly absorbs it. (`.switch .act` already fills ink, matching191the `ink = selected` convention of `.seg`/`.tab`/`.pager`.)192193## Accessibility194195The custom controls are div-built, so `futurism.js` carries their semantics — keep196it loaded. `fdInit()` runs on load and wires ARIA roles + keyboard on `.sel`197(button + listbox popup, Enter/↑↓/Esc), `.tabs` (tablist, ←/→ roving), and `.toggle`198(switch, Space/Enter); call `fdInit()` again after injecting components at runtime.199Every focusable control shows the same 3px accent `:focus-visible` ring — don't200remove it. Native elements stay native on purpose: checkbox/radio are styled201`<input>`s, and the modal is a real `<dialog>` (Tab-trap + Esc); give the dialog202an `autofocus` button. When an overlay restores focus to its opener on close,203**guard the opener's `isConnected` first** — a captured opener can be removed while204the overlay is open (a deleted row, a closed tab), and focusing a detached node205throws or dumps focus to `<body>`; fall back to a stable target then. Overlays dim206with `--scrim`, motion respects207`prefers-reduced-motion` (the live `.dot` keeps a static ring there), and a208`forced-colors` block re-asserts selected/active state for Windows High Contrast.209210**Scope.** The interactive controls require `futurism.js` — with JS off, the custom211select/tabs/toggle/drawer/toast are inert (use native elements if you need a no-JS212fallback). The motion language is directional and LTR-oriented (skews, slide-ins,213darting underlines); for RTL, flip those with `:dir(rtl)` overrides.214215**SPA / framework use.** The kit drives state through DOM classes (`.on`/`.open`)216and imperative ARIA, with one set of document-level delegates. Treat those controls217as uncontrolled — don't let React/Vue re-render over the toggled classes — or port218the control to framework state. Call `fdInit()` from your mount hook for components219added after load; the document delegates are attached once and keep working.220221## Responsive & touch222223The laws are resolution-independent, but the layout patterns below keep an app224usable on a phone without breaking the aesthetic:225226- **Off-canvas drawer.** A fixed sidebar slides in over a `--scrim` backdrop on227 mobile and becomes static at your desktop breakpoint. Use `.drawer` /228 `.scrim-bg` + `fdDrawer()`.229- **Touch targets ≥ ~40px.** The kit already grows icon buttons, keycaps, accent230 swatches, pager buttons, toggles, and select rows to ~40px+ under231 `@media (max-width:640px)` — the 24–32px desktop sizes are too small for thumbs.232 Match that bump for any new small control you add.233- **No horizontal body scroll.** Contain wide content (tables, tab bars, command234 rows) with `overflow-x:auto` on *that* element; never let the page body scroll235 sideways. Add `min-width:0` to flex children that hold ellipsised text.236- **Collapse header chrome.** Hide the wordmark/subtitle and toggle labels at237 narrow widths, keeping just the logo mark + essential controls. Use `.only-desktop`238 / `.only-mobile` for chrome that swaps entirely (a desktop hint vs. a mobile-worded239 one, not just a shrink) — `.only-desktop` is hidden by default and shown240 (`display:block`) at the `min-width:768px` breakpoint; `.only-mobile` is the241 inverse. The base utility only sets `display:none`/`block`; if the shown state242 needs `flex`/`inline-flex`/`inline-block` instead, mirror the kit's own two-rule243 shape with a compound selector rather than overriding just one side — e.g.244 `.keyhint.only-desktop{display:none}` plus245 `@media (min-width:768px){.keyhint.only-desktop{display:flex}}`. A compound246 selector outranks the utility's single-class rule on any specificity tie, but247 only within the media condition it's written under — a single *unscoped* override248 would win at the hidden breakpoint too and defeat the hide, so give your override249 the same two-rule shape as the utility itself, just with your display value250 swapped in.251- **Popover, not inline rows, on mobile.** A wide control (e.g. the 7-swatch252 accent row) collapses to a single trigger + popover — that's what `.accpick` is.253- **App-scale surfaces.** A fullscreen panel is a composition, not a page: cap a254 single column at ~680px only when the content is genuinely linear (a form, an255 article), otherwise let a grid fill the surface (form + live preview side by256 side, chart rows). A desktop panel with a dead right half reads as a mobile257 layout blown up.258- **Compact/preview projections.** A live miniature of a running surface (a dock259 preview, picture-in-picture) needs its own type scale: hide chrome and controls,260 keep the live data, and pin font sizes in **px** — fluid `clamp()`/`vw` display261 sizes are computed from the viewport, not the ~200px card they're projected262 into, so they overflow it.263264Drive these with `min-width` media queries (mobile-first): style the compact265layout as the default, then restore the desktop layout at `@media (min-width:768px)`.266267## When extending the kit268269New components must obey the eight laws and use only tokens. Match the existing270motion vocabulary (slide/dart/lurch/march at `--fast`/`--med`). Add the component271to `references/components.md` so the kit stays the single source of truth.272273The kit already carries these conventions on its own components; a new affordance274that doesn't generalize them silently breaks parity. So for any new control:275276- **Accent fill → `--on-accent` text** (never `--ink`). See Theming.277- **Loading/progress → reuse `fd-march`** (the barber-pole), never a rotary spinner;278 shape a `.skel`-style placeholder, don't invent a new loader.279- **A status/selected/loading affordance needs a forced-colors fallback.** Color-mix280 fills, gradients, and `box-shadow` are stripped under `@media (forced-colors:active)`281 — re-assert the cue with a border/outline or `Highlight`, the way the kit does for282 `.skel`/`.prog`/`.dot` and the selected states. Data-viz strokes (sparklines,283 chart paths) re-stroke with `CanvasText` so the data survives too.284- **A state that changes without focus moving** (a toast, a "refreshing" indicator)285 needs `role="status"` + `aria-live` so it's announced — match `.toast`.286- **Progressive enhancement:** a component's CSS should stand alone; if it needs JS to287 be usable (positioning, etc.), give it a working CSS default and let the JS upgrade288 it (see the `.sel` dropdown — absolute default, JS promotes to fixed).