-tokens`
+ map and the `tokens()` mixin — and reads them below; modifiers change tokens, not properties
+ (the mechanics are in `core-scss`).
+3. Every new colour token is a `light-dark()` pair on `:root` in `layout/_root.scss`.
+4. Controls are 40px tall by default, 6px radius, `xs` shadow, gray border; only the primary
+ action is filled.
+5. Hover, active, focus and disabled use the five recipes in section 6, nothing bespoke.
+6. Flat surfaces: 1px translucent border + `xs` shadow. Floating surfaces: `overlay` / `lg`.
+7. Everything on the 4px grid; heights from the control ladder; text from the heading scale.
+8. Colour means state. Decorative colour lives only in explicitly decorative components
+ (card gradients, illustrations).
+9. Icons: outline, 1.5 stroke, 20px, inherit colour in buttons, `gray-400` elsewhere.
+10. Motion: 0.15–0.35s, and every decorative animation has a reduced-motion fallback.
+
+The long form of these, with examples, is [rules-of-thumb.md](rules-of-thumb.md).
diff --git a/.agents/skills/design-system/rules-of-thumb.md b/.agents/skills/design-system/rules-of-thumb.md
new file mode 100644
index 000000000..1e751e475
--- /dev/null
+++ b/.agents/skills/design-system/rules-of-thumb.md
@@ -0,0 +1,404 @@
+# Rules of thumb
+
+Part of the `design-system` skill. Short rules in the style of the Kumo design skill: an
+imperative title, one sentence on why, a recommended example and one to avoid. Rules 1–9 are
+Kumo rules that hold for Tabler as they are, 10–14 are Kumo rules adapted to Tabler's
+choices, 15–24 are Tabler's own. Counts are the state of `preview/pages` and `shared/` on
+`v2-dev`, 2026-09-08; they show that a rule is not yet met everywhere, so a review reports new
+violations, not the backlog.
+
+A review cites a rule by number: "rule 16 — buttons go in `btn-list`".
+
+### 1. Use 14px for content text
+
+Body copy, buttons, table cells and form values are 14px. 16px and above belong to headings.
+
+```html
+
+API tokens
+Production token expires in 30 days.
+
+
+API tokens
+Production token expires in 30 days.
+```
+
+### 2. Always sentence case headings
+
+Only the first word and product names take a capital. Title Case reads as marketing, not UI.
+This covers all UI text: headings, card and page titles, subheaders, buttons, menu items and
+table headers.
+Today: "Top Pages", "Payment Method", "Edit Profile" and about ten more card titles in preview.
+
+```html
+
+Recent requests
+Cloudflare Workers usage
+
+
+Recent Requests
+Recent requests
+```
+
+### 3. Never use `fw-bold`
+
+Headings are semibold (600) and emphasised inline text is medium (500). 700 is not on the
+weight scale. Today: 12 × `fw-bold` against 25 × `fw-medium` and 8 × `fw-semibold`.
+
+```html
+
+Account settings
+This action is required.
+
+
+Account settings
+This action is required.
+```
+
+### 4. Never change letter spacing
+
+`letter-spacing` is 0 everywhere. The only tracked text is `.subheader`, which the framework
+styles itself.
+
+```html
+
+
+Worker metrics
+
+
+Worker metrics
+Worker metrics
+```
+
+### 5. Put related text closer together
+
+A title and its description sit 8px apart; the group sits 16px from the next one. Equal
+spacing everywhere hides the grouping.
+
+```html
+
+
+
Web analytics
+
Measure site traffic without changing your code.
+
+Configure
+
+
+Web analytics
+Measure site traffic without changing your code.
+Configure
+```
+
+### 6. Reduce the font size of inline monospaced text
+
+Monospace glyphs look larger than the sans at the same size. `code` is `0.857em` in the
+framework, so use the element rather than a styled span.
+
+```html
+
+Edit wrangler.toml to continue.
+
+
+Edit wrangler.toml to continue.
+```
+
+### 7. Align icons with the first line of text
+
+An icon next to multi-line text aligns with the first line, not the middle of the block.
+Inline icons are 16px (`icon-inline`) so they match the x-height.
+
+```html
+
+
+
+
API token permissions cannot be changed after creation. Create a new token instead.
+
+Runs on Workers.
+
+
+
+
+
API token permissions cannot be changed after creation. Create a new token instead.
+
+```
+
+### 8. Use concentric border radii
+
+When one rounded box sits inside another with 8px or less between them, the inner radius is
+the outer radius minus the gap. The framework does this for card headers and dropdown items;
+hand-written nesting must too.
+
+```html
+
+
+
+
+
+```
+
+### 9. Use a border to separate sticky elements
+
+A sticky table head or toolbar gets a 1px border on the edge that meets the content. A
+shadow there reads as a floating surface, which it is not. Today: 6 × `sticky-top` in preview.
+
+```html
+
+
+
+
+…
+```
+
+### 10. Hover feedback within 150ms
+
+Kumo forbids colour transitions on hover. Tabler keeps them, but at `0.15s` only, because
+that is fast enough to feel instant. Never put `--transition-base` (0.2s, `all`) on a hover.
+An opacity reveal (actions that appear on hover) is not a colour change and uses
+`$transition-time` (0.3s).
+
+```scss
+// Recommended
+.my-item:hover { background: $hover-bg; }
+.my-item { transition: background-color 0.15s ease-in-out; }
+
+// Avoid
+.my-item { transition: all 0.2s ease-in-out; }
+```
+
+### 11. Flat surfaces get a border plus the `xs` shadow, nothing more
+
+Kumo says never combine a border with a shadow. Tabler's card is exactly that pair, on
+purpose: the border draws the edge, the 1px `xs` shadow lifts it off the gray page. Anything
+stronger belongs to floating surfaces.
+
+```html
+
+…
+
+
+
+…
+…
+```
+
+### 12. Never nest a card inside a card body
+
+Two borders and two paddings in a row look like a mistake. Kumo's rule is "never stack";
+Tabler has `.card-stacked` for the deliberate stack, so the rule is about nesting. A list in a
+card is `card-list-group`, a table is `card-table`; both keep the card's edge and padding.
+
+```html
+
+
+
+
+
+```
+
+### 13. Modals live in the DOM and open with `data-bs-toggle`
+
+Kumo's "never conditionally render dialogs" becomes: never inject a modal with a script or
+toggle it with `display`. The framework owns the fade, the backdrop and the focus trap.
+
+```html
+
+Edit
+…
+
+
+Edit
+```
+
+### 14. Maintain content size during collapse animations
+
+A collapsing element animates `height` only. If the content inside reflows to the new
+width mid-animation, text jumps. Fix the inner width, not the animation.
+
+```html
+
+
+
+
+
+```
+
+### 15. Only one filled button per view
+
+`btn-primary` marks the single main action. Everything else is the plain `btn`, so the eye
+finds the primary without reading.
+
+```html
+
+
+
+
+
+```
+
+### 16. Buttons go in `btn-list`, badges in `badge-list`, avatars in `avatar-list`
+
+The wrappers own the gap and the wrapping. Hand-written `me-2` breaks at the last item and
+in RTL. Today: 57 × `me-2` against 5 × `btn-list`.
+
+```html
+
+
+
+
+Export
+Create
+```
+
+### 17. Use `text-secondary`, never `text-muted`
+
+One class for secondary text. `text-muted` is a Bootstrap alias kept for compatibility.
+Today: 254 × `text-secondary`, 18 × `text-muted`. `` still emits
+`text-muted` and is documented that way; it is not a finding until the prop is remapped.
+
+```html
+
+Last deployed 4 minutes ago
+
+
+Last deployed 4 minutes ago
+```
+
+### 18. No inline `style` for sizes
+
+Sizes come from utilities or a component token, so they follow the scale and the theme.
+Today: 75 inline `style` attributes in preview, among them 8 × `height: 8rem` and
+`width: 350px`. The exception is SVG illustration fills, which are data, not layout.
+
+```html
+
+
+…
+
+
+
+…
+```
+
+### 19. Vertical rhythm is `mb-3` inside a card and `row-cards` between cards
+
+One spacing step per level. Mixing `mb-3`, `mb-4` and `mt-3` in the same block gives uneven
+gaps nobody chose. Today: 142 × `mb-3`, 76 × `mb-4`, 31 × `mt-3`, 31 × `mt-4`.
+
+```html
+
+
+
+
+
+```
+
+### 20. Colour means state
+
+A coloured badge, dot or text says success, warning or danger. Colour used for decoration
+makes real states invisible. Categorical colour (mail labels, calendar categories, tags) is
+allowed when every item in the set gets one, as a dot or a soft `-lt` badge, never solid,
+and the state colours are not reused for categories.
+
+```html
+
+Active
+Failed
+Draft
+
+
+Marketing
+Sales
+```
+
+### 21. Icons inherit colour inside controls, `gray-400` elsewhere
+
+A button's icon is the button's text colour. A stand-alone icon is `--icon-color`.
+Colouring an icon separately makes it read as a second control.
+
+```html
+
+ Create
+
+
+
+ Create
+```
+
+### 22. Uppercase only in `.subheader` and list-group headers
+
+The framework uppercases `.subheader`, `.list-group-header`, avatar initials, ribbons and the
+pagination "page" label. Nothing else. Today: 9 × `text-uppercase` in preview to review.
+
+```html
+
+
+
+
+
+Total revenue
+Status |
+```
+
+### 23. Empty states use `.empty`
+
+The component gives the icon, title, hint and action the same spacing on every page. A
+hand-written "No data" paragraph does not.
+
+```html
+
+
+ Clear filters
+
+
+
+No data
+```
+
+### 24. Every `light-dark()` pair is checked in both modes before merge
+
+A new colour token is not done when light mode looks right. Capture the page with
+`screenshots/` (both modes come out of one run) or toggle `data-bs-theme="dark"` in the
+preview and compare.
+
+```scss
+// Recommended
+--note-bg: light-dark(var(--yellow-lt), color-mix(in oklab, var(--yellow) 15%, transparent));
+
+// Avoid
+--note-bg: #fff7dd;
+```