rastrillo / fiesta Public

Clone
git clone https://amadan.net/rastrillo/fiesta

Plain git — no account needed to clone.

Download

Download this file

1---
2name: Fiesta
3description: A quiet, correct interface baseline built to be taken over.
4colors:
5 bg: "#fbfbfc"
6 surface: "#ffffff"
7 surface-2: "#f3f3f5"
8 line: "#e6e6ea"
9 line-strong: "#868691"
10 text: "#18181b"
11 text-muted: "#52525c"
12 text-faint: "#6b6b75"
13 accent: "#1f56d6"
14 accent-strong: "#1844ac"
15 accent-soft: "#ecf1ff"
16 on-accent: "#ffffff"
17 accent-fill: "#2257df"
18 on-accent-fill: "#ffffff"
19 danger-fill: "#c62f2f"
20 control-border: "#d0d4dc"
21 tone-neutral-fg: "#374151"
22 tone-neutral-bg: "#eef0f2"
23 tone-positive-fg: "#166534"
24 tone-positive-bg: "#dcfce7"
25 tone-warning-fg: "#854d0e"
26 tone-warning-bg: "#fef3c7"
27 tone-negative-fg: "#b91c1c"
28 tone-negative-bg: "#fee2e2"
29typography:
30 display:
31 fontFamily: "system-ui, -apple-system, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif"
32 fontSize: "2.1rem"
33 fontWeight: 650
34 lineHeight: 1.1
35 headline:
36 fontFamily: "system-ui, -apple-system, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif"
37 fontSize: "1.25rem"
38 fontWeight: 600
39 lineHeight: 1.25
40 letterSpacing: "-0.012em"
41 title:
42 fontFamily: "system-ui, -apple-system, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif"
43 fontSize: "0.875rem"
44 fontWeight: 600
45 lineHeight: 1.4
46 body:
47 fontFamily: "system-ui, -apple-system, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif"
48 fontSize: "0.875rem"
49 fontWeight: 400
50 lineHeight: 1.6
51 label:
52 fontFamily: "system-ui, -apple-system, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif"
53 fontSize: "0.71875rem"
54 fontWeight: 650
55 letterSpacing: "0"
56 textTransform: "none"
57 field:
58 fontFamily: "system-ui, -apple-system, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif"
59 fontSize: "1.0625rem"
60 fontWeight: 550
61 stat:
62 fontFamily: "system-ui, -apple-system, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif"
63 fontSize: "1.5rem"
64 fontWeight: 650
65 lineHeight: 1.1
66 mono:
67 fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"
68 fontSize: "0.78125rem"
69rounded:
70 bar: "2px"
71 sm: "4px"
72 md: "6px"
73 pill: "999px"
74spacing:
75 "1": "0.25rem"
76 "2": "0.5rem"
77 "3": "0.75rem"
78 "4": "1rem"
79 "5": "1.5rem"
80 "6": "2.5rem"
81components:
82 button:
83 backgroundColor: "{colors.surface}"
84 textColor: "{colors.text}"
85 typography: "{typography.title}"
86 rounded: "{rounded.sm}"
87 padding: "0.45rem 0.75rem"
88 button-primary:
89 backgroundColor: "{colors.accent-fill}"
90 textColor: "{colors.on-accent-fill}"
91 typography: "{typography.title}"
92 rounded: "{rounded.sm}"
93 padding: "0.45rem 0.75rem"
94 button-primary-hover:
95 backgroundColor: "#1d4dc8"
96 textColor: "{colors.on-accent-fill}"
97 input:
98 backgroundColor: "{colors.surface}"
99 textColor: "{colors.text}"
100 rounded: "{rounded.sm}"
101 padding: "0.45rem 0.6rem"
102 card:
103 backgroundColor: "{colors.surface}"
104 textColor: "{colors.text}"
105 rounded: "{rounded.md}"
106 padding: "1.1rem 1.25rem"
107 status-pill:
108 backgroundColor: "{colors.tone-neutral-bg}"
109 textColor: "{colors.tone-neutral-fg}"
110 rounded: "{rounded.pill}"
111 padding: "0.15rem 0.5rem"
112---
113
114# Design System: Fiesta
115
116<!--
117 Scope note, because this system has three themes and two colour schemes.
118 The frontmatter carries the `day` theme's LIGHT values, because
119 fiesta.ThemeNames()[0] is `day` and it is the theme fiesta.css carries.
120 The normative source in the repository is one custom property per colour
121 holding a light-dark() pair, so a single-value frontmatter slot cannot
122 hold it; .impeccable/design.json carries both halves per token under
123 extensions.colorMeta. Do not treat a hex here as the only value.
124 Source of truth: css/tokens.css (structure) and css/themes/*.css (colour,
125 type family, shape).
126-->
127
128## Overview
129
130**Creative North Star: "A Default With a Point of View"**
131
132This is the interface an application has before anyone has styled it,
133and it should look like someone chose it. `day` is near-greyscale with
134hairline borders, one cobalt for anything that acts, crisp corners, and
135natural depth: light comes from above, cards rest on the page, controls
136press in and slowly lift. It aims to read as professional, in the family
137of the tools people already trust, and to be recognisably this system's.
138
139Remixability is still the thesis. Every colour, radius, shadow,
140highlight and strength resolves through a custom property, and the three
141shipped themes prove the axes work rather than offering a menu of three
142answers: `day` turns depth on, `plain` and `signal` turn it off and
143render as they always did. An app that wants a fourth writes one `:root`
144block. Files are delivered once and app-owned from that moment: edit
145them, or delete what you do not use.
146
147The system is deliberately under-designed at the component level so an
148app's own character can land on top without a fight. It is explicitly
149not the SaaS gradient template — no purple-to-blue washes, glassmorphism,
150oversized rounded cards or decorative blobs — and explicitly not a
151vendor-branded admin kit that stamps someone else's identity on an app
152and resists removal. Both hand you an identity you did not choose, which
153is the one thing this system exists not to do.
154
155**Key Characteristics:**
156
157- Small, dense type: a 14px base with 12.5px and 11.5px steps below it.
158- Near-flat surfaces separated by 1px hairlines; depth is a theme axis.
159- One accent, carrying action and position; status carries its own
160 four-tone palette.
161- Every colour is a `light-dark()` pair, so light and dark are one
162 declaration rather than two stylesheets.
163- The CSS floor is Chrome 123, Safari 17.5, Firefox 121, set by `light-dark()`; anything at or below it is used with no fallback.
164- Works with JavaScript off. Scripts are enhancement and each is
165 deletable on its own.
166
167## Colors
168
169A restrained near-neutral palette with a single working blue, held to
170WCAG 2.2 AA by a contrast gate over documented pairs in every theme and
171both schemes.
172
173### Primary
174
175- **Interface Blue** (`#1f56d6` light, `#7aa2ff` dark): the accent, a
176 cobalt. It marks what you can act on and where you are: focus rings,
177 links, an active tab, the current menu item. It belongs to the chrome,
178 not to the content.
179- **Accent Fill** (`#2257df` light, `#2f61e8` dark): the primary
180 button's fill, with a hover fill of its own (`#1d4dc8` light,
181 `#2956d0` dark) and a white label (`on-accent-fill`). A filled control
182 never darkens by filter: every state is a colour, gated.
183- **Danger Fill** (`#c62f2f` light, `#c43232` dark): the danger button's
184 fill, the same way, with a white label.
185- **Interface Blue Deep** (`#1844ac` light, `#a3c0ff` dark): kept for a
186 theme written before the fills; `tokens.css` reads it only as the
187 primary hover's fallback.
188- **Interface Blue Wash** (`#ecf1ff` light, `#16234a` dark): the tint
189 behind a current menu item, and the fallback for the hover and press
190 tints in a theme that declares none. Carries no text of its own beyond
191 the accent and the muted greys, both gated against it.
192
193### Neutral
194
195- **Page** (`#fbfbfc` light, `#0c0d0f` dark): the page ground, one step
196 below the surface, so a card reads as resting on it.
197- **Surface** (`#ffffff` light, `#141518` dark): cards, panels, inputs
198 and menus.
199- **Surface Sunken** (`#f3f3f5` light, `#18191d` dark): the recessed
200 step, for table header rows, the second tone in a stack and a hovered
201 ghost button.
202- **Hairline** (`#e6e6ea` light, `#26282e` dark): every divider, card
203 edge and table rule.
204- **Control Edge** (`#868691` light, `#71737c` dark): the border of a
205 field and a select. Deliberately much darker
206 than the hairline, and held at 3:1 against its backgrounds because it
207 is a control boundary (WCAG 1.4.11), not decoration.
208- **Control Border** (`#d0d4dc` light, `#343843` dark): a plain button's
209 edge, softer than the field border on purpose. The label identifies
210 the button, so the edge is decoration under 1.4.11 and carries no 3:1
211 floor.
212- **Ink** (`#18181b` light, `#ededf0` dark): body text.
213- **Ink Muted** (`#52525c` light, `#a1a1aa` dark): secondary text,
214 subheads, cell values that are not the row's name.
215- **Ink Faint** (`#6b6b75` light, `#8c8c96` dark): hints, captions,
216 counts. Still gated at 4.5:1; "faint" is a role, not a licence to
217 drop below the floor.
218
219### Tertiary
220
221The four status tones, each a foreground/background pair used together:
222**Neutral** (`#374151` on `#eef0f2`), **Positive** (`#166534` on
223`#dcfce7`), **Warning** (`#854d0e` on `#fef3c7`), **Negative**
224(`#b91c1c` on `#fee2e2`). They exist so status never borrows the accent.
225
226### Named Rules
227
228**The Gated Pair Rule.** A colour that carries text owes 4.5:1 against
229the surface it actually sits on, and a control boundary owes 3:1 — and
230owes it in every theme and both schemes, in the contrast gate, before it
231ships. Adding a colour means adding its pair to that gate. A tone chosen
232against a tinted ground says nothing about the same tone on a plain card.
233
234**The Two-Signal Rule.** State is never colour alone. A status pill
235renders its label, a meter prints its fraction as text beside the bar,
236and a stat's change carries its own `+` or `−` sign. Colour is the
237second signal, never the first.
238
239**The Light-Dark Rule.** Every colour is declared once as
240`light-dark(<light>, <dark>)` under `color-scheme: light dark`. There is
241no dark stylesheet and no second palette to keep in step; an explicit
242theme choice re-resolves the same declarations by setting
243`color-scheme`.
244
245## Typography
246
247**Display / Body Font:** the platform UI stack — `system-ui`,
248`-apple-system`, `"Segoe UI"`, `Roboto`, `"Helvetica Neue"`, `Arial`,
249`sans-serif`. `signal` leads the same stack with `"Helvetica Neue"`.
250**Mono Font:** `ui-monospace, SFMono-Regular, Menlo, Consolas,
251monospace`, on the `rst-mono` utility. It is a real second family and
252not a weight of the first — reference codes, identifiers and machine
253values are set in it so they read as things to be copied rather than
254read.
255
256**Character:** the type has no personality of its own on purpose. The
257family is a theme axis, and the shipped default is whatever the reader's
258OS considers correct — which is the right answer for an application
259frame and the wrong answer for a brand, so an app that wants a voice
260sets one property.
261
262### Hierarchy
263
264- **Display** (650, 2.1rem, 1.1): the lead reading in a stat band, and
265 nothing else. The only genuinely large type in the system.
266- **Headline** (600, 1.25rem, 1.25, −0.012em in `day`; 1.375rem and
267 −0.015em in `plain` and `signal`): a screen's `<h1>` in the page
268 header. Capped at 1.25rem under 34rem.
269- **Field** (550, 1.0625rem = 17px, inherited 1.5): the form's headline
270 input — a title, a name — and the only control that leaves the body
271 step. Its label stays at Label size, so the emphasis is carried by
272 the reader's own words rather than by the form's furniture.
273- **Title** (600, 0.875rem, 1.4): section headings inside a card head,
274 button labels, a row's name. A section `<h2>` separates by weight, not
275 size: body-sized in `plain` and `signal`, a step below it (0.8125rem)
276 in `day`.
277- **Body** (400, 0.875rem = 14px, 1.6): everything else. Subheads cap at
278 44rem.
279- **Label** (650, 0.71875rem = 11.5px): the eyebrow over a value, such
280 as stat labels, table column heads and the nav rail's section names.
281 Sentence case with no tracking in `day`; uppercase at +0.06em in
282 `plain` and `signal`.
283
284### Steps outside the roles
285
286Four literal sizes appear in `tokens.css` outside the roles above, and
287they are recorded rather than tidied away — two have since become roles,
288which is what recording them was for:
289
290- **1.5rem** — a stat's number when it is not the lead. A role now
291 (`stat`), because a dashboard uses it as one.
292- **1.25rem**: the headline's cap under 34rem, and `day`'s headline at
293 every width. A step of `headline` rather than a size of its own.
294- **1.0625rem** — a role now (`field`, and `--rst-fs-lg`), because a
295 form's headline input reads as one. The large person avatar's initials
296 and the shell's brand land on the same value and stay literals: they
297 are sized to fit a circle, so the match is a coincidence rather than a
298 shared step, and tying them to the type scale would move them the next
299 time the scale moves.
300- **0.6875rem** — the ordinary person avatar's initials.
301
302That last one is decoration sized to fit a circle, not type a reader
303parses, which is why it is not a role.
304
305### Named Rules
306
307**The Small-Base Rule.** The base is 14px, not 16px, and every step is
308declared in `rem` so the whole scale tracks a reader who has raised
309their browser default. Do not convert any of it to `px`.
310
311**The Weight-Not-Size Rule.** Hierarchy inside a screen is carried by
312weight and colour before size. A card's `<h2>` is body-sized at 600;
313reaching for a larger size to make a section feel important is how this
314system loses its density.
315
316The one exception is a form's headline field (the Field role above),
317and it is an exception because of what is being sized. Everywhere else
318the rule governs *chrome* — a heading the system wrote, labelling
319content underneath it — and chrome has no business growing. A title
320input is the opposite case: the large thing is what the reader typed,
321which is the only content on a form that is not furniture. The
322exception is deliberately narrow, one control on one form; a second
323large field on the same screen is the density loss this rule is about.
324
325**The Grouped Number Rule.** Every number a person reads as a quantity is
326grouped for their locale — `54,173` in English, `54.173` in German,
327`٥٤٬١٧٣` in Arabic. Never a bare run of digits. Grouping is not "add a
328comma": the separator, the group size and the digits themselves all
329change with the locale, and a number's readability is the whole reason
330the rule exists — `54173` is counted, `54,173` is read.
331
332The exception is exact and small: **an identifier is not a quantity.**
333Reference codes, order numbers, years, versions and port numbers are
334labels that happen to be made of digits, and grouping one changes what it
335appears to be. Order 4471 is not order 4,471.
336
337The rule is met. `{{number $L .Count}}` groups a quantity and `{{money $L .Amount .Currency}}` writes money, both on the server, in the reader's language, from CLDR data. In the browser, `Intl` is at most an enhancement.
338
339Money has two rules of its own. The currency always comes from the data, never from the reader: an amount is stored as a whole number of its currency's smallest unit, with its ISO 4217 code beside it, so a price in euros is in euros on every page. And every digit of that smallest unit is shown: 1.5 dinars is `KWD 1.500`, never `KWD 1.5`.
340
341Each language's formatting data is a pack an app links only when it offers that language, so a language costs nothing until then. Templates are built once, and the localizer in each page's data chooses the language, so one request's language never reaches another's page.
342
343## Layout
344
345A single centred content column, capped at **64rem** with `1.5rem 1rem
3462.5rem` of padding, holding every screen. The column is markup an app
347emits (`rst-page`); no partial brings its own.
348
349The column ends with a footer row (`rst-page-foot`) for the language and colour-scheme menus: muted, under a hairline, with the menus at the inline end. The page header stays the first thing on screen.
350
351**Spacing** is a six-step rem scale — 0.25 / 0.5 / 0.75 / 1 / 1.5 /
3522.5rem — and screens stack vertically in reading order: page header,
353then section head plus card, then the next. Horizontal arrangement is
354reserved for the idioms that ship it (card heads, field rows, toolbars,
355list-row cells, segmented tabs, stat bands).
356
357**Breakpoints.** 800px is the system's real line: `rst-m-hide` columns
358fold away, the sidebar rail collapses into a disclosure, and the topbar
359takes its narrow layout. 34rem (544px) stacks the page header's title
360and action. **320px is a written commitment** — the layout reflows to it
361without a horizontal scrollbar.
362
363**Density** is high by intent: 14px base, 0.45rem control padding, rows
364that fit a working screenful. The floor on that density is WCAG 2.2's
365Target Size (2.5.8) — pagination chips measure about 30px, row-action
366pills about 27px and buttons about 34px on their smaller axis, all clear
367of the 24px minimum. Do not shrink them without re-measuring.
368
369The scrollbar gutter is reserved whether or not there is a scrollbar in
370it (`--rst-scrollbar-gutter`), so a short page and a long one are the
371same width and moving between them does not slide the layout sideways.
372
373## Elevation & Depth
374
375Depth is a theme axis, and `day` turns it on. The light comes from
376above: a 1px highlight along a raised edge (`--rst-depth-hl`), a short
377contact shadow where an object meets the page (`--rst-depth-contact`),
378soft ambient light around it (`--rst-depth-ambient`), a face shade for
379filled controls (`--rst-depth-face`, 0.04 in `day`), and a 1px drop for
380a pressed button (`--rst-press-drop`). Buttons are a touch proud of the
381page and press in; cards rest on it; menus float, with tight elevation;
382fields sit slightly into it. `plain` and `signal` set every one flat,
383and a theme written before the axis is read through fallbacks that are
384today's values.
385
386### Shadow Vocabulary (the `day` theme)
387
388- **Pop** (`0 2px 4px light-dark(rgba(16,24,40,0.06), rgba(0,0,0,0.35)),
389 0 8px 16px -4px light-dark(rgba(16,24,40,0.1), rgba(0,0,0,0.5))`): an
390 open menu, a dropdown panel, a modal: surfaces that float over the
391 page. Real elevation, kept tight.
392- **Knob** (`0 1px 2px light-dark(rgba(16,24,40,0.18), rgba(0,0,0,0.4))`):
393 the moving part of a control, such as a switch's knob.
394- **Lift** (`0 1px 2px light-dark(rgba(16,24,40,0.08), rgba(0,0,0,0.3))`):
395 the barely-there separation on a raised strip.
396- **Overlay** (`light-dark(rgba(0,0,0,0.45), rgba(0,0,0,0.6))`): the
397 modal scrim. Deeper in dark on purpose: black reads lighter against a
398 dark backdrop and needs more alpha to still dim.
399
400## The light
401
402`light.js` moves the light. The shadows' light comes from the hour: the
403left in the morning, overhead at noon, the right in the evening, so
404shadows lean away from it by up to 1.5px and the top highlight leans
405toward it. With a mouse, a button catches a sheen where the pointer is
406(white on a filled button, a faint accent tint on a plain one in light
407mode), and a card's rim brightens near the pointer over about a second
408and fades when it leaves. On a phone a pressed button shows the press,
409not the sheen, and a tapped card catches the light briefly. The whole
410page's light can follow the pointer instead: set `FOLLOW` to `true` at
411the top of the file. It is off because every write of it restyles the
412page.
413
414It does not run under reduced motion, does nothing under forced colours,
415reads no sensor, and never captures the pointer. For the still design,
416leave `light.js` out; nothing else asks for it.
417
418## Shapes
419
420Soft rectangles, and the exact softness is a theme axis: `day` uses 6px
421on cards and 4px on controls, `plain` 4px and 3px, `signal` 4px and 2px.
422A pill radius (999px) is reserved for status pills and count chips —
423things that are read as tokens rather than operated.
424
425A 2px radius is the thin-bar step, and it is not on the theme axis: the
426capacity meter and a running job's progress bar both use it, at a height
427of 4px where a themed 8px corner would swallow the bar whole.
428
429Borders do most of the structural work: 1px hairlines between and around
430surfaces, and a distinctly darker 1px edge on anything operable. Focus
431is a 2px accent outline offset 2px, never a border swap, so focus never
432changes an element's size.
433
434**Known gap, recorded rather than smoothed over.** `tokens.css` also
435hard-codes 9px, 7px, 4px, 10px and 14px radii in a handful of places —
436the combobox and date-picker lists, and a few small chrome pieces — that
437do not resolve through `var(--rst-radius*)`. A theme that changes its
438corner language does not change those. It is a real limit on the
439remixability this system claims, it predates this record, and it is not
440something to fix by editing one component in passing.
441
442**The Derived Rule Rule.** The page header's underline is not authored
443per theme: it is `color-mix(in oklab, var(--rst-accent) N%, var(--rst-line))`,
444mixed at 45% in `signal`, and left as the plain hairline in `plain` and
445in `day`, which keeps its cobalt for things that act. A theme that
446changes its accent gets a matching rule with no second value to keep in
447step. It is decorative and carries no contrast floor.
448
449## Components
450
451### Hover and press
452
453Hover is quiet and press is clear. A plain button's hover is a light
454grey face and a darker border; a filled button's is a deeper fill; a
455ghost's is a surface tint. Rows, menu items and sidebar items lift: a 5%
456accent tint (9% in dark), a highlight along the top edge, no sheen. The
457current sidebar item is a selected thumb, and hovering another is a
458half-raised preview of it. These tints and lifts apply only on a
459device that can hover, so a tap leaves no tint behind; the phone index
460rows keep their tap tint on purpose.
461
462A press is instant and slow to leave. The button drops 1px, loses its
463highlight and outer shadow for an inset one, switches its sheen off and
464darkens, most under the pointer; on release the darkness lifts over
465about 0.7s, the spot under the finger last. A row pressed, or its link
466tapped, takes a stronger tint and sinks. Under reduced motion nothing
467moves and nothing fades.
468
469### Buttons
470
471- **Shape:** crisp (`{rounded.sm}`, 4px in `day`).
472- **Default:** control face, control-border edge (softer than the field
473 border on purpose), Ink label at 600 and 12.5px, `0.45rem 0.75rem`
474 padding. Hover: a light grey face and a darker border.
475- **Primary:** the accent fill with a 4% top-to-bottom shade and a white
476 label; hover deepens the fill.
477- **Danger:** the danger fill, the same way.
478- **Ghost:** no fill, muted label; hover a surface tint, with the label
479 darkening to Ink rather than picking up the accent. The accent means
480 "this goes somewhere new", and a ghost Cancel does not.
481- **Press:** every variant sinks and darkens; see Hover and press.
482- **Focus:** 2px accent outline, 2px offset.
483
484### Cards / Containers
485
486- **Corner Style:** `{rounded.md}` (6px in `day`).
487- **Background:** Surface, on a Page ground one step below it, with the
488 hairline painted as the outer stop of the rim gradient. To recolour a
489 card, set `--rst-surface` on it, which also recolours the surfaces
490 inside it; a `background` of your own paints over the hairline and a
491 `background-color` sits under the face.
492- **Shadow Strategy:** at rest, a hairline and two ambient layers lit
493 from above; the rim catches the light near the pointer. Menus float
494 with real but tight elevation.
495- **Internal Padding:** `1.1rem 1.25rem`. A card head (`rst-box-head`)
496 sits *outside* the card, holding an `<h2>` and one compact button.
497- **Note:** list cards and data grids hold rows only and are unpadded by
498 design; prose, forms and links go in a padded box.
499
500### Inputs / Fields
501
502- **Style:** Surface fill, 1px Control Edge (`--rst-line-strong`)
503 border, a 1px inset shadow so the field sits into the page,
504 `{rounded.sm}`, `0.45rem 0.6rem` padding, full width by default.
505- **Focus:** the accent border, a 3px accent halo, and the 2px outline
506 offset 2px.
507- **Error:** the message is text, and the field is described by it via
508 `aria-describedby`; the required marker uses the negative tone.
509
510### Navigation
511
512The rail sits on the page background. The current item is a surface
513thumb with a top highlight, a hairline ring and a contact shadow, marked
514by `aria-current` in the markup; a hovered item is the thumb's
515half-raised preview. Below 800px the rail collapses into a
516`<details>` disclosure with a menu icon. Menus are
517`<details name="rst-menus">`, so opening one closes the rest with no
518script; `fiesta.js` adds outside-click and Escape as enhancement.
519
520**The Row Rule.** A dropdown never sits on a line of its own. Put it in a row, beside what it acts on: a page header, a section head, a list's toolbar, a bar, or the footer row. A menu above a page's title pushes the title down and reads as the page's first action.
521
522### Status Pill
523
524A pill-radius tinted chip carrying a foreground/background tone pair and
525**always its label**. The leading dot is decoration on top of the word,
526never a substitute for it.
527
528### Stat Band (signature)
529
530The instrument strip a dashboard opens with: a row of stat cells in one
531card, divided by inline-start hairlines, wrapping rather than squeezing
532so any number of cells works. One cell may be marked `lead`, which is
533the same component at 2.1rem instead of 1.5rem — there is no separate
534headline component. Numbers are `tabular-nums` so a polled figure does
535not jitter its neighbours. A change carries its own sign in the text and
536a tone the caller supplies; the tone is never derived from the sign,
537because a falling number is good about as often as it is bad.
538
539## Do's and Don'ts
540
541### Do:
542
543- **Do** change the system by setting custom properties. Every colour,
544 radius, shadow and type family resolves through one; a fourth theme is
545 a single `:root` block.
546- **Do** declare colours as `light-dark(<light>, <dark>)` pairs under
547 `color-scheme: light dark`, so light and dark stay one declaration.
548- **Do** add any new colour that carries text to the contrast gate, at
549 4.5:1 against the surface it actually sits on, in all three themes and
550 both schemes.
551- **Do** give state a second signal besides colour — a label, a sign, a
552 number as text.
553- **Do** group the digits of any number a person reads as a quantity, for
554 their locale — and never group an identifier, a year or a version.
555- **Do** keep type in `rem` and the base at 14px, so the scale tracks a
556 reader's own browser setting.
557- **Do** use logical properties (`border-inline-start`,
558 `margin-block-end`) so borders and dividers land correctly in Arabic.
559- **Do** let the scriptless rendering be the real one; add script only as
560 enhancement that can be deleted.
561
562### Don't:
563
564- **Don't** reach for the SaaS gradient template — purple-to-blue
565 washes, glassmorphism, oversized rounded cards, decorative blobs.
566- **Don't** stamp a vendor kit's identity onto an app. This system is
567 under-designed on purpose so the app's own character can land on top.
568- **Don't** use the accent as decoration or mood. It marks what you can
569 act on and where you are; status has its own four tones.
570- **Don't** float a resting surface. A card rests on the page with the
571 theme's ambient light; a pop shadow means something has left the page
572 plane.
573- **Don't** make a section feel important by making it bigger. Weight and
574 colour carry hierarchy before size; a card's `<h2>` is body-sized.
575- **Don't** shrink control padding without re-measuring against WCAG
576 2.2's 24px target-size minimum.
577- **Don't** compose a heading, a paragraph and a button side by side in a
578 flex row. Screens stack vertically; horizontal arrangement is reserved
579 for the idioms that ship it.
580- **Don't** put a dropdown on a line of its own, or above a page's title. Put it in a row beside what it acts on.
581