Design rules
Each rule has a reason. If the reason doesn’t apply in a given case, the rule doesn’t either. If you break a rule, say why in the PR.
Adapted from Cloudflare’s Kumo design guidance (skills/kumo-design) and shaped for multi-client admin work.
Colour
Use semantic tokens only. Write var(--ad-text-subtle) / text-subtle, never #6b7280 / text-gray-500.
Why: a raw colour has to be edited for every client and every mode. A token is edited once, in the brand file. check-raw-colors.mjs enforces this rule.
Never write a dark: variant. Why: each token already holds both values via light-dark(). A second set of classes drifts out of sync.
Brand is for action and selection, not decoration. Primary buttons, focus rings, links, the selected nav item’s icon, selected rows and chart series 1 use the brand. Headings, backgrounds and borders do not. Why: a client’s brand colour can be anything. If it also tints large surfaces, a loud brand makes the whole admin loud.
Status hues are fixed. Danger is always red and success always green, whatever the brand. Why: a red brand must not make “Delete” look like an ordinary button. If a brand sits close to danger red, raise it before shipping.
Type
Content text is 14px. Body, buttons, inputs, table data. 16px and above is only for headings. Why: admin screens are dense and scanned. One content size keeps tables and forms aligned.
Headings are sentence case and semibold (600). Nothing is bold (700). Use font-weight: 500 for inline emphasis.
Why: Title Case and heavy weights make dense screens shout.
Don’t change letter-spacing. Why: the font designer already tuned it. Tracking tweaks are the first thing to look off when a client swaps the font.
The kit loads no font files; a project that adds a web font loads it itself. Set --ad-font-sans in the brand file, use font-display: swap (or optional), and preload only the regular weight’s WOFF2. Why: without font-display, text can stay invisible while the font loads, and preloading every weight costs more than it saves. Pages should read fine on the system fonts that follow it in the stack.
Inline code is 0.9em. Why: monospace runs optically larger than the sans next to it.
Numbers in columns use tabular-nums and right-align (.adm-num). Why: digits line up, so values can be compared at a glance.
Space and shape
Related text sits closer together than the blocks around it. Use .adm-stack (6px) for a heading + description and .adm-group (24px) between blocks.
Why: proximity is how people see grouping. Equal gaps everywhere make a form read as one long list.
Vertical padding is smaller than horizontal around text (cards: 16px × 20px). Why: line-height already adds space above and below the glyphs.
Nested corners are concentric: inner radius = outer radius − padding. Why: identical radii look pinched when the gap between them is small. .adm-segmented and .adm-menu__item use this rule.
Elevated surfaces use a ring plus a shadow, never a border plus a shadow. Why: a border adds a hard edge on top of the shadow’s soft one. A 1px ring reads as part of the shadow.
Don’t nest cards. Inside a card, separate content with a heading or a hairline. Why: a card inside a card reads as two separate objects and loses the hierarchy.
Sticky elements get a bottom border (topbar, table header). Why: without one, scrolled content seems to slide under nothing.
Motion
Hover colour changes are instant. No transition on colour. Why: animated hover states make a tool feel slow. Motion is for things that appear: dialogs, toasts, the mobile sidebar.
Leaving is quicker than arriving. Things arrive in --ad-duration (200 ms) with --ad-ease-out and leave in --ad-duration-fast (120 ms) with --ad-ease-in. Why: once someone has dismissed something they’ve stopped looking at it; a slow exit only holds them up.
Animate opacity, translate and scale, never size or position properties. Why: those are drawn without laying out the page again, so they stay smooth on a slow laptop.
Keep dialogs in the DOM and toggle open, never render them conditionally. Why: conditional rendering kills the exit animation and the browser’s focus return.
Respect prefers-reduced-motion. components.css does this globally.
Writing
Name things the way the user sees them: “Notifications”, not “webhook config”. Buttons say what happens: “Delete segment”, not “OK”. A toast confirms the outcome: “Segment deleted”. An error says what went wrong and how to fix it: “Mailchimp rejected the API key. Generate a new key in Mailchimp, then paste it under Connectors.” No apologies, no “Something went wrong”. Confirmations say what will NOT happen too: “Members stay in Salesforce; nothing is removed there.” Why: destructive dialogs in data tools are scary mainly because the scope is unclear.