rastrillo / fiesta Public

DESIGN.md and design.json, from rastrillo at SEED, in Fiesta's words

The rest of Task 2 of the Fiesta part-1 plan: the two design documents,
which waited on copy review because they are read by people.

Both files are rastrillo's at SEED, e14b745a7c08d0efa3bf221a8b6a5e75de15c46f,
with exactly the seven edits Paul approved unchanged (copy-review
result: approve, nothing edited). In DESIGN.md: the frontmatter name
and the title say Fiesta; the scope note cites fiesta.ThemeNames()[0]
and the theme fiesta.css carries rather than ui.ThemeNames and
`rastrillo new`; the source of truth is css/tokens.css and
css/themes/*.css rather than ui/; the light section's opt-out is
"leave light.js out", since Fiesta has no vendoredIsMine or
`rastrillo doctor`; and the navigation section credits fiesta.js, the
file Task 4 writes, with the menu dismiss rastrillo.js provides. In
design.json only the title changes. A diff against SEED shows those
lines and no other, and neither file mentions rastrillo or ui/ now.

copy-review/ is ignored because the review's scratch sits in the
worktree and is not part of the design system.
Human

Prompt

Finish Task 2: copy DESIGN.md and .impeccable/design.json from SEED and apply the seven copy edits Paul approved; ignore copy-review/.

From this commit's own message. The prompt ledger has no entries for it — which is what a squashed, rebased or amended commit looks like, and what every commit looks like in a repo without a ledger.

Summary

Adds DESIGN.md and .impeccable/design.json, rastrillo's at e14b745 with seven approved edits, and ignores copy-review/.
Agent notes
The copy-review result is in copy-review/result.json in the worktree, now ignored. .claude/ stays untracked.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 0e9be0fce15039bcb6d8776a24606a7e33a1609d parent 6510f90
3 files changed, +1139 −0
  • .gitignore +1 −0
  • .impeccable/design.json +564 −0
  • DESIGN.md +574 −0
diff --git a/.gitignore b/.gitignore
index 30bcfa4..a442105 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
.build/
+copy-review/
diff --git a/.impeccable/design.json b/.impeccable/design.json
new file mode 100644
index 0000000..c7e5625
--- /dev/null
+++ b/.impeccable/design.json
@@ -0,0 +1,564 @@
+{
+ "schemaVersion": 2,
+ "generatedAt": "2026-09-02T22:42:36Z",
+ "title": "Design System: Fiesta",
+ "extensions": {
+ "colorMeta": {
+ "bg": {
+ "role": "neutral",
+ "displayName": "Page",
+ "canonical": "light-dark(#fbfbfc, #0c0d0f)",
+ "tonalRamp": [
+ "#0b0d10",
+ "#111418",
+ "#1a1f26",
+ "#2a3038",
+ "#4b5563",
+ "#9aa2ad",
+ "#d7dbe0",
+ "#ffffff"
+ ]
+ },
+ "surface": {
+ "role": "neutral",
+ "displayName": "Surface",
+ "canonical": "light-dark(#ffffff, #141518)",
+ "tonalRamp": [
+ "#0e1116",
+ "#141820",
+ "#1a1f26",
+ "#2a3038",
+ "#525a66",
+ "#9aa2ad",
+ "#e3e6ea",
+ "#ffffff"
+ ]
+ },
+ "surface-2": {
+ "role": "neutral",
+ "displayName": "Surface Sunken",
+ "canonical": "light-dark(#f3f3f5, #18191d)",
+ "tonalRamp": [
+ "#0b0e12",
+ "#15191f",
+ "#1f242b",
+ "#2f353d",
+ "#565d67",
+ "#9ba2ab",
+ "#dfe2e5",
+ "#f6f7f8"
+ ]
+ },
+ "line": {
+ "role": "neutral",
+ "displayName": "Hairline",
+ "canonical": "light-dark(#e6e6ea, #26282e)",
+ "tonalRamp": [
+ "#14171c",
+ "#2a3038",
+ "#3b424c",
+ "#59616c",
+ "#7f8792",
+ "#adb3bc",
+ "#e5e7eb",
+ "#f7f8f9"
+ ]
+ },
+ "line-strong": {
+ "role": "neutral",
+ "displayName": "Control Edge",
+ "canonical": "light-dark(#868691, #71737c)",
+ "tonalRamp": [
+ "#181b20",
+ "#2b3038",
+ "#414852",
+ "#5b636e",
+ "#7f8792",
+ "#a5abb4",
+ "#cbcfd5",
+ "#eceef0"
+ ]
+ },
+ "text": {
+ "role": "neutral",
+ "displayName": "Ink",
+ "canonical": "light-dark(#18181b, #ededf0)",
+ "tonalRamp": [
+ "#0a0c0e",
+ "#1a1d21",
+ "#2e333a",
+ "#4b5563",
+ "#727a86",
+ "#a3aab4",
+ "#cfd4d9",
+ "#e8eaed"
+ ]
+ },
+ "text-muted": {
+ "role": "neutral",
+ "displayName": "Ink Muted",
+ "canonical": "light-dark(#52525c, #a1a1aa)",
+ "tonalRamp": [
+ "#12161b",
+ "#232a33",
+ "#374151",
+ "#4b5563",
+ "#6b7382",
+ "#949ca8",
+ "#c3c9d1",
+ "#e6e9ed"
+ ]
+ },
+ "text-faint": {
+ "role": "neutral",
+ "displayName": "Ink Faint",
+ "canonical": "light-dark(#6b6b75, #8c8c96)",
+ "tonalRamp": [
+ "#151920",
+ "#262c34",
+ "#3c434e",
+ "#535a66",
+ "#656c78",
+ "#98a1ac",
+ "#c6cbd2",
+ "#e9ebee"
+ ]
+ },
+ "accent": {
+ "role": "primary",
+ "displayName": "Interface Blue",
+ "canonical": "light-dark(#1f56d6, #7aa2ff)",
+ "tonalRamp": [
+ "#0a1b3f",
+ "#102a63",
+ "#173d8f",
+ "#1a4fc0",
+ "#2464e0",
+ "#5b8bee",
+ "#7ba7ff",
+ "#eef3fe"
+ ]
+ },
+ "accent-strong": {
+ "role": "primary",
+ "displayName": "Interface Blue Deep",
+ "canonical": "light-dark(#1844ac, #a3c0ff)",
+ "tonalRamp": [
+ "#08152f",
+ "#0e2354",
+ "#143a86",
+ "#1a4fc0",
+ "#3a6ddc",
+ "#6e97f2",
+ "#a3c0ff",
+ "#e4ecff"
+ ]
+ },
+ "accent-soft": {
+ "role": "primary",
+ "displayName": "Interface Blue Wash",
+ "canonical": "light-dark(#ecf1ff, #16234a)",
+ "tonalRamp": [
+ "#0c1526",
+ "#17233a",
+ "#22334f",
+ "#33496b",
+ "#5d7cae",
+ "#93a9cf",
+ "#cbd9f4",
+ "#eef3fe"
+ ]
+ },
+ "on-accent": {
+ "role": "primary",
+ "displayName": "On Accent",
+ "canonical": "light-dark(#ffffff, #0c0d0f)",
+ "tonalRamp": [
+ "#0b1220",
+ "#16203a",
+ "#293c5a",
+ "#475874",
+ "#7386a3",
+ "#a6b4c9",
+ "#d3dae5",
+ "#ffffff"
+ ]
+ },
+ "accent-fill": {
+ "role": "primary",
+ "displayName": "Accent Fill",
+ "canonical": "light-dark(#2257df, #2f61e8)"
+ },
+ "on-accent-fill": {
+ "role": "primary",
+ "displayName": "On Accent Fill",
+ "canonical": "#ffffff"
+ },
+ "danger-fill": {
+ "role": "primary",
+ "displayName": "Danger Fill",
+ "canonical": "light-dark(#c62f2f, #c43232)"
+ },
+ "control-border": {
+ "role": "neutral",
+ "displayName": "Control Border",
+ "canonical": "light-dark(#d0d4dc, #343843)"
+ },
+ "tone-neutral-fg": {
+ "role": "tertiary",
+ "displayName": "Neutral Tone Ink",
+ "canonical": "light-dark(#374151, #c3c9d1)",
+ "tonalRamp": [
+ "#0f1318",
+ "#1c222a",
+ "#293240",
+ "#374151",
+ "#5b6675",
+ "#8b95a3",
+ "#c3c9d1",
+ "#eef0f2"
+ ]
+ },
+ "tone-neutral-bg": {
+ "role": "tertiary",
+ "displayName": "Neutral Tone Ground",
+ "canonical": "light-dark(#eef0f2, #262c34)",
+ "tonalRamp": [
+ "#12151a",
+ "#262c34",
+ "#39414c",
+ "#525b68",
+ "#78818e",
+ "#a6adb7",
+ "#d2d7dc",
+ "#eef0f2"
+ ]
+ },
+ "tone-positive-fg": {
+ "role": "tertiary",
+ "displayName": "Positive Tone Ink",
+ "canonical": "light-dark(#166534, #6ee7a8)",
+ "tonalRamp": [
+ "#04170c",
+ "#0a2e18",
+ "#0f4724",
+ "#166534",
+ "#22874a",
+ "#3fb673",
+ "#6ee7a8",
+ "#dcfce7"
+ ]
+ },
+ "tone-positive-bg": {
+ "role": "tertiary",
+ "displayName": "Positive Tone Ground",
+ "canonical": "light-dark(#dcfce7, #10281c)",
+ "tonalRamp": [
+ "#06150e",
+ "#10281c",
+ "#1b432e",
+ "#2a6647",
+ "#3f8f65",
+ "#6bbb8e",
+ "#a6dcbf",
+ "#dcfce7"
+ ]
+ },
+ "tone-warning-fg": {
+ "role": "tertiary",
+ "displayName": "Warning Tone Ink",
+ "canonical": "light-dark(#854d0e, #f0c46a)",
+ "tonalRamp": [
+ "#1c1104",
+ "#38230a",
+ "#573710",
+ "#854d0e",
+ "#b3741f",
+ "#d69f3f",
+ "#f0c46a",
+ "#fef3c7"
+ ]
+ },
+ "tone-warning-bg": {
+ "role": "tertiary",
+ "displayName": "Warning Tone Ground",
+ "canonical": "light-dark(#fef3c7, #2c2110)",
+ "tonalRamp": [
+ "#150f07",
+ "#2c2110",
+ "#46351a",
+ "#665029",
+ "#8f733f",
+ "#bda067",
+ "#e0cb9b",
+ "#fef3c7"
+ ]
+ },
+ "tone-negative-fg": {
+ "role": "tertiary",
+ "displayName": "Negative Tone Ink",
+ "canonical": "light-dark(#b91c1c, #f79aa0)",
+ "tonalRamp": [
+ "#210607",
+ "#420b0c",
+ "#6d1414",
+ "#b91c1c",
+ "#d64a48",
+ "#ea7377",
+ "#f79aa0",
+ "#fee2e2"
+ ]
+ },
+ "tone-negative-bg": {
+ "role": "tertiary",
+ "displayName": "Negative Tone Ground",
+ "canonical": "light-dark(#fee2e2, #331819)",
+ "tonalRamp": [
+ "#190b0c",
+ "#331819",
+ "#502527",
+ "#73393b",
+ "#9c5c5e",
+ "#c58a8c",
+ "#e6b8ba",
+ "#fee2e2"
+ ]
+ }
+ },
+ "typographyMeta": {
+ "display": {
+ "displayName": "Display",
+ "purpose": "The lead reading in a stat band, and nothing else. The only genuinely large type in the system."
+ },
+ "headline": {
+ "displayName": "Headline",
+ "purpose": "A screen's h1 in the page header. 1.25rem in day, 1.375rem in plain and signal; capped at 1.25rem under 34rem."
+ },
+ "field": {
+ "displayName": "Field",
+ "purpose": "The form's headline input — a title, a name. The only control that leaves the body step, and the one exception to the Weight-Not-Size Rule, because what grows is the reader's own words rather than chrome."
+ },
+ "title": {
+ "displayName": "Title",
+ "purpose": "Section headings in a card head, button labels, a row's name. Body-sized; separates by weight."
+ },
+ "body": {
+ "displayName": "Body",
+ "purpose": "Everything else. 14px base. Subheads cap at 44rem."
+ },
+ "label": {
+ "displayName": "Label",
+ "purpose": "The eyebrow over a value: stat labels, column heads, rail section names. Sentence case in day, uppercase in plain and signal."
+ },
+ "stat": {
+ "displayName": "Stat",
+ "purpose": "A stat's number when it is not the lead. A role because a dashboard uses it as one."
+ },
+ "mono": {
+ "displayName": "Mono",
+ "purpose": "Reference codes, identifiers and machine values, on the rst-mono utility. A real second family, so they read as things to copy rather than to read."
+ }
+ },
+ "themes": [
+ {
+ "name": "day",
+ "role": "default",
+ "font": "system UI stack",
+ "radius": "6px / 4px",
+ "accent": "light-dark(#1f56d6, #7aa2ff)",
+ "headerRule": "var(--rst-line)",
+ "character": "Crisp and lit from above: near-greyscale neutrals, cobalt for anything that acts, natural depth, shadows lit by the hour and a sheen under the pointer."
+ },
+ {
+ "name": "plain",
+ "role": "alternate",
+ "font": "system UI stack",
+ "radius": "4px / 3px",
+ "accent": "light-dark(#111111, #f2f2f2)",
+ "headerRule": "var(--rst-line)",
+ "character": "Nearly square, near-black accent, one hairline shadow layer."
+ },
+ {
+ "name": "signal",
+ "role": "alternate",
+ "font": "Helvetica Neue stack",
+ "radius": "4px / 2px",
+ "accent": "light-dark(#1a56ff, #6d90ff)",
+ "headerRule": "color-mix(in oklab, var(--rst-accent) 45%, var(--rst-line))",
+ "character": "Milled: short dense shadows, electric blue, tightest corners."
+ }
+ ],
+ "shadows": [
+ {
+ "name": "pop",
+ "value": "0 2px 4px light-dark(rgba(16, 24, 40, 0.06), rgba(0, 0, 0, 0.35)), 0 8px 16px -4px light-dark(rgba(16, 24, 40, 0.1), rgba(0, 0, 0, 0.5))",
+ "purpose": "Surfaces that float over the page: an open menu, a dropdown panel, a modal. Real elevation, kept tight. Theme-owned; signal uses 0 4px 10px and plain 0 1px 3px."
+ },
+ {
+ "name": "knob",
+ "value": "0 1px 2px light-dark(rgba(16, 24, 40, 0.18), rgba(0, 0, 0, 0.4))",
+ "purpose": "The moving part of a control, such as a switch's knob."
+ },
+ {
+ "name": "lift",
+ "value": "0 1px 2px light-dark(rgba(16, 24, 40, 0.08), rgba(0, 0, 0, 0.3))",
+ "purpose": "Barely-there separation on a raised strip."
+ },
+ {
+ "name": "overlay",
+ "value": "light-dark(rgba(0, 0, 0, 0.45), rgba(0, 0, 0, 0.6))",
+ "purpose": "The modal scrim. Deeper in dark because black reads lighter against a dark backdrop."
+ }
+ ],
+ "motion": [
+ {
+ "name": "reduced-motion",
+ "value": "@media (prefers-reduced-motion: reduce)",
+ "purpose": "Honoured throughout; animated affordances fall back to a static treatment rather than disappearing."
+ }
+ ],
+ "breakpoints": [
+ {
+ "name": "reflow-floor",
+ "value": "320px"
+ },
+ {
+ "name": "header-stack",
+ "value": "34rem"
+ },
+ {
+ "name": "rail-collapse",
+ "value": "800px"
+ },
+ {
+ "name": "page-column-cap",
+ "value": "64rem"
+ }
+ ]
+ },
+ "components": [
+ {
+ "name": "Primary Button",
+ "kind": "button",
+ "refersTo": "button-primary",
+ "description": "The one action a screen most wants taken. One per screen head.",
+ "html": "<a class=\"ds-btn ds-btn-primary\" href=\"#\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M5 12h14\"/><path d=\"M12 5v14\"/></svg>Write a post</a>",
+ "css": ".ds-btn { align-items: center; background: var(--rst-surface, #ffffff); border: 1px solid var(--rst-line-strong, #7f8792); border-radius: var(--rst-radius-sm, 6px); color: var(--rst-text, #1a1d21); cursor: pointer; display: inline-flex; font-family: inherit; font-size: 0.78125rem; font-weight: 600; gap: 0.5rem; line-height: 1.4; padding: 0.45rem 0.75rem; text-decoration: none; } .ds-btn svg { block-size: 1em; inline-size: 1em; flex: none; } .ds-btn-primary { background: var(--rst-accent, #2464e0); border-color: var(--rst-accent, #2464e0); color: var(--rst-on-accent, #ffffff); } .ds-btn-primary:hover { background: var(--rst-accent-strong, #1a4fc0); border-color: var(--rst-accent-strong, #1a4fc0); } .ds-btn-primary:focus-visible { outline: 2px solid var(--rst-accent, #2464e0); outline-offset: 2px; }"
+ },
+ {
+ "name": "Default Button",
+ "kind": "button",
+ "refersTo": "button",
+ "description": "Every other action. Hover moves the border and label to the accent rather than filling.",
+ "html": "<a class=\"ds-btn ds-btn-default\" href=\"#\">Edit</a>",
+ "css": ".ds-btn-default { align-items: center; background: var(--rst-surface, #ffffff); border: 1px solid var(--rst-line-strong, #7f8792); border-radius: var(--rst-radius-sm, 6px); color: var(--rst-text, #1a1d21); cursor: pointer; display: inline-flex; font-family: inherit; font-size: 0.78125rem; font-weight: 600; gap: 0.5rem; line-height: 1.4; padding: 0.45rem 0.75rem; text-decoration: none; } .ds-btn-default:hover { border-color: var(--rst-accent, #2464e0); color: var(--rst-accent, #2464e0); } .ds-btn-default:focus-visible { outline: 2px solid var(--rst-accent, #2464e0); outline-offset: 2px; }"
+ },
+ {
+ "name": "Text Input",
+ "kind": "input",
+ "refersTo": "input",
+ "description": "A labelled field with its hint. Focus is an offset outline, never a border swap, so focus never resizes the control.",
+ "html": "<div class=\"ds-field\"><label class=\"ds-field-label\" for=\"ds-title\">Title</label><input class=\"ds-input\" id=\"ds-title\" type=\"text\" value=\"Release notes for v0.24\" aria-describedby=\"ds-title-help\"><small class=\"ds-field-help\" id=\"ds-title-help\">Shown at the top of the post.</small></div>",
+ "css": ".ds-field { display: block; margin: 0 0 1rem; } .ds-field-label { color: var(--rst-text, #1a1d21); display: block; font-size: 0.78125rem; font-weight: 600; margin: 0 0 0.2rem; } .ds-input { background: var(--rst-surface, #ffffff); border: 1px solid var(--rst-line-strong, #7f8792); border-radius: var(--rst-radius-sm, 6px); box-sizing: border-box; color: var(--rst-text, #1a1d21); font: inherit; font-size: 0.875rem; padding: 0.45rem 0.6rem; width: 100%; } .ds-input:focus-visible { outline: 2px solid var(--rst-accent, #2464e0); outline-offset: 2px; } .ds-field-help { color: var(--rst-text-muted, #4b5563); display: block; font-size: 0.71875rem; margin: 0.45rem 0 0; }"
+ },
+ {
+ "name": "Status Pill",
+ "kind": "chip",
+ "refersTo": "status-pill",
+ "description": "A record's status. Always renders its label; the dot is decoration on top of the word, never a substitute.",
+ "html": "<span class=\"ds-pill ds-pill-warning\">Waiting</span> <span class=\"ds-pill ds-pill-positive\">Resolved</span> <span class=\"ds-pill\">Open</span>",
+ "css": ".ds-pill { align-items: center; background: var(--rst-tone-neutral-bg, #eef0f2); border-radius: 999px; color: var(--rst-tone-neutral-fg, #374151); display: inline-flex; font-size: 0.71875rem; font-weight: 600; gap: 0.35rem; padding: 0.15rem 0.5rem; } .ds-pill::before { background: currentColor; border-radius: 50%; block-size: 0.4em; content: \"\"; inline-size: 0.4em; } .ds-pill-positive { background: var(--rst-tone-positive-bg, #dcfce7); color: var(--rst-tone-positive-fg, #166534); } .ds-pill-warning { background: var(--rst-tone-warning-bg, #fef3c7); color: var(--rst-tone-warning-fg, #854d0e); } .ds-pill-negative { background: var(--rst-tone-negative-bg, #fee2e2); color: var(--rst-tone-negative-fg, #b91c1c); }"
+ },
+ {
+ "name": "Card",
+ "kind": "card",
+ "refersTo": "card",
+ "description": "The padded section card, with its heading sitting outside it.",
+ "html": "<div class=\"ds-box-head\"><h2>Payout</h2><a class=\"ds-btn-default\" href=\"#\">Edit</a></div><section class=\"ds-box\"><p>Everything on a screen sits inside boxes.</p><div class=\"ds-box-foot\">Last updated 2 hours ago</div></section>",
+ "css": ".ds-box { background: var(--rst-surface, #ffffff); border: 1px solid var(--rst-line, #e5e7eb); border-radius: var(--rst-radius, 8px); color: var(--rst-text, #1a1d21); font-size: 0.875rem; line-height: 1.6; margin: 0; padding: 1.1rem 1.25rem; } .ds-box > :first-child { margin-top: 0; } .ds-box > :last-child { margin-bottom: 0; } .ds-box-head { align-items: baseline; display: flex; gap: 1rem; justify-content: space-between; margin: 0 0 0.5rem; } .ds-box-head h2 { color: var(--rst-text, #1a1d21); font-size: 0.875rem; font-weight: 600; margin: 0; } .ds-box-foot { border-top: 1px solid var(--rst-line, #e5e7eb); color: var(--rst-text-muted, #4b5563); font-size: 0.78125rem; margin-top: 1rem; padding-top: 0.75rem; }"
+ },
+ {
+ "name": "Navigation Rail",
+ "kind": "nav",
+ "refersTo": null,
+ "description": "The sidebar shell's rail. The current item is marked by three signals at once — tint, colour and weight — plus aria-current in the markup.",
+ "html": "<nav class=\"ds-nav\" aria-label=\"Main\"><a href=\"#\" aria-current=\"true\">Dashboard</a><a href=\"#\">Requests</a><a href=\"#\">Settings</a></nav>",
+ "css": ".ds-nav { display: flex; flex-direction: column; gap: 0.1rem; inline-size: 220px; padding: 0.5rem; } .ds-nav a { border-radius: var(--rst-radius-sm, 6px); color: var(--rst-text-muted, #4b5563); display: block; font-size: 0.875rem; padding: 0.4rem 0.6rem; text-decoration: none; } .ds-nav a:hover { background: var(--rst-surface-2, #f6f7f8); color: var(--rst-text, #1a1d21); } .ds-nav a[aria-current] { background: var(--rst-accent-soft, #eef3fe); color: var(--rst-accent, #2464e0); font-weight: 600; } .ds-nav a:focus-visible { outline: 2px solid var(--rst-accent, #2464e0); outline-offset: 2px; }"
+ },
+ {
+ "name": "Stat Band",
+ "kind": "custom",
+ "refersTo": null,
+ "description": "The instrument strip a dashboard opens with. One cell may be the lead; the change carries its own sign, and its tone is supplied rather than derived.",
+ "html": "<div class=\"ds-stats\"><div class=\"ds-stat ds-stat-lead\"><span class=\"ds-stat-label\">Revenue this month</span><span class=\"ds-stat-num\">&euro;48,210</span><span class=\"ds-stat-delta ds-stat-up\">+12%</span><span class=\"ds-stat-note\">vs. last month</span></div><div class=\"ds-stat\"><span class=\"ds-stat-label\">Orders today</span><span class=\"ds-stat-num\">318</span></div><div class=\"ds-stat\"><span class=\"ds-stat-label\">Average basket</span><span class=\"ds-stat-num\">&euro;41.20</span><span class=\"ds-stat-delta ds-stat-down\">&minus;3%</span></div></div>",
+ "css": ".ds-stats { background: var(--rst-surface, #ffffff); border: 1px solid var(--rst-line, #e5e7eb); border-radius: var(--rst-radius, 8px); display: flex; flex-wrap: wrap; } .ds-stat { border-inline-start: 1px solid var(--rst-line, #e5e7eb); display: flex; flex: 1 1 9rem; flex-direction: column; gap: 0.15rem; min-inline-size: 0; padding: 1rem; } .ds-stat:first-child { border-inline-start: 0; } .ds-stat-lead { flex-basis: 14rem; } .ds-stat-label { color: var(--rst-text-muted, #4b5563); font-size: 0.71875rem; font-weight: 650; letter-spacing: 0.06em; text-transform: uppercase; } .ds-stat-num { color: var(--rst-text, #1a1d21); font-size: 1.5rem; font-variant-numeric: tabular-nums; font-weight: 650; line-height: 1.1; } .ds-stat-lead .ds-stat-num { font-size: 2.1rem; } .ds-stat-delta { font-size: 0.78125rem; font-variant-numeric: tabular-nums; font-weight: 600; } .ds-stat-up { color: var(--rst-tone-positive-fg, #166534); } .ds-stat-down { color: var(--rst-tone-negative-fg, #b91c1c); } .ds-stat-note { color: var(--rst-text-faint, #656c78); font-size: 0.71875rem; }"
+ }
+ ],
+ "narrative": {
+ "northStar": "A Default With a Point of View",
+ "overview": "This is the interface an application has before anyone has styled it, and it should look like someone chose it. day is near-greyscale with hairline borders, one cobalt for anything that acts, crisp corners, and natural depth: light comes from above, cards rest on the page, controls press in and slowly lift. It aims to read as professional, in the family of the tools people already trust, and to be recognisably this system's.\n\nRemixability is still the thesis. Every colour, radius, shadow, highlight and strength resolves through a custom property, and the three shipped themes prove the axes work rather than offering a menu of three answers: day turns depth on, plain and signal turn it off and render as they always did. An app that wants a fourth writes one :root block. Files are delivered once and app-owned from that moment: edit them, or delete what you do not use.\n\nThe system is deliberately under-designed at the component level so an app's own character can land on top without a fight. It is explicitly not the SaaS gradient template — no purple-to-blue washes, glassmorphism, oversized rounded cards or decorative blobs — and explicitly not a vendor-branded admin kit that stamps someone else's identity on an app and resists removal. Both hand you an identity you did not choose, which is the one thing this system exists not to do.",
+ "keyCharacteristics": [
+ "Small, dense type: a 14px base with 12.5px and 11.5px steps below it.",
+ "Near-flat surfaces separated by 1px hairlines; depth is a theme axis.",
+ "One accent, carrying action and position; status carries its own four-tone palette.",
+ "Every colour is a light-dark() pair, so light and dark are one declaration rather than two stylesheets.",
+ "Works with JavaScript off. Scripts are enhancement and each is deletable on its own."
+ ],
+ "rules": [
+ {
+ "name": "The Gated Pair Rule",
+ "body": "A colour that carries text owes 4.5:1 against the surface it actually sits on, and a control boundary owes 3:1 — and owes it in every theme and both schemes, in the contrast gate, before it ships. Adding a colour means adding its pair to that gate. A tone chosen against a tinted ground says nothing about the same tone on a plain card.",
+ "section": "colors"
+ },
+ {
+ "name": "The Two-Signal Rule",
+ "body": "State is never colour alone. A status pill renders its label, a meter prints its fraction as text beside the bar, and a stat's change carries its own + or − sign. Colour is the second signal, never the first.",
+ "section": "colors"
+ },
+ {
+ "name": "The Light-Dark Rule",
+ "body": "Every colour is declared once as light-dark(<light>, <dark>) under color-scheme: light dark. There is no dark stylesheet and no second palette to keep in step; an explicit theme choice re-resolves the same declarations by setting color-scheme.",
+ "section": "colors"
+ },
+ {
+ "name": "The Small-Base Rule",
+ "body": "The base is 14px, not 16px, and every step is declared in rem so the whole scale tracks a reader who has raised their browser default. Do not convert any of it to px.",
+ "section": "typography"
+ },
+ {
+ "name": "The Weight-Not-Size Rule",
+ "body": "Hierarchy inside a screen is carried by weight and colour before size. A card's h2 is body-sized at 600; reaching for a larger size to make a section feel important is how this system loses its density.",
+ "section": "typography"
+ },
+ {
+ "name": "The Derived Rule Rule",
+ "body": "The page header's underline is not authored per theme: it is color-mix(in oklab, var(--rst-accent) N%, var(--rst-line)), mixed at 45% in signal, and left as the plain hairline in plain and in day, which keeps its cobalt for things that act. A theme that changes its accent gets a matching rule with no second value to keep in step. It is decorative and carries no contrast floor.",
+ "section": "shapes"
+ },
+ {
+ "name": "The Grouped Number Rule",
+ "body": "Every number a person reads as a quantity is grouped for their locale — 54,173 in English, 54.173 in German, ٥٤٬١٧٣ in Arabic. Never a bare run of digits. The exception is exact: an identifier is not a quantity. Reference codes, order numbers, years, versions and port numbers are labels made of digits, and grouping one changes what it appears to be. Ruled 2026-09-02 and not yet met: form.FormatCents writes $1284.50 today.",
+ "section": "typography"
+ }
+ ],
+ "dos": [
+ "Do change the system by setting custom properties. Every colour, radius, shadow and type family resolves through one; a fourth theme is a single :root block.",
+ "Do declare colours as light-dark(<light>, <dark>) pairs under color-scheme: light dark, so light and dark stay one declaration.",
+ "Do add any new colour that carries text to the contrast gate, at 4.5:1 against the surface it actually sits on, in all three themes and both schemes.",
+ "Do give state a second signal besides colour — a label, a sign, a number as text.",
+ "Do keep type in rem and the base at 14px, so the scale tracks a reader's own browser setting.",
+ "Do use logical properties (border-inline-start, margin-block-end) so borders and dividers land correctly in Arabic.",
+ "Do let the scriptless rendering be the real one; add script only as enhancement that can be deleted.",
+ "Do group the digits of any number a person reads as a quantity, for their locale — and never group an identifier, a year or a version."
+ ],
+ "donts": [
+ "Don't reach for the SaaS gradient template — purple-to-blue washes, glassmorphism, oversized rounded cards, decorative blobs.",
+ "Don't stamp a vendor kit's identity onto an app. This system is under-designed on purpose so the app's own character can land on top.",
+ "Don't use the accent as decoration or mood. It marks what you can act on and where you are; status has its own four tones.",
+ "Don't float a resting surface. A card rests on the page with the theme's ambient light; a pop shadow means something has left the page plane.",
+ "Don't make a section feel important by making it bigger. Weight and colour carry hierarchy before size; a card's h2 is body-sized.",
+ "Don't shrink control padding without re-measuring against WCAG 2.2's 24px target-size minimum.",
+ "Don't compose a heading, a paragraph and a button side by side in a flex row. Screens stack vertically; horizontal arrangement is reserved for the idioms that ship it."
+ ]
+ }
+}
diff --git a/DESIGN.md b/DESIGN.md
new file mode 100644
index 0000000..2e5d21e
--- /dev/null
+++ b/DESIGN.md
@@ -0,0 +1,574 @@
+---
+name: Fiesta
+description: A quiet, correct interface baseline built to be taken over.
+colors:
+ bg: "#fbfbfc"
+ surface: "#ffffff"
+ surface-2: "#f3f3f5"
+ line: "#e6e6ea"
+ line-strong: "#868691"
+ text: "#18181b"
+ text-muted: "#52525c"
+ text-faint: "#6b6b75"
+ accent: "#1f56d6"
+ accent-strong: "#1844ac"
+ accent-soft: "#ecf1ff"
+ on-accent: "#ffffff"
+ accent-fill: "#2257df"
+ on-accent-fill: "#ffffff"
+ danger-fill: "#c62f2f"
+ control-border: "#d0d4dc"
+ tone-neutral-fg: "#374151"
+ tone-neutral-bg: "#eef0f2"
+ tone-positive-fg: "#166534"
+ tone-positive-bg: "#dcfce7"
+ tone-warning-fg: "#854d0e"
+ tone-warning-bg: "#fef3c7"
+ tone-negative-fg: "#b91c1c"
+ tone-negative-bg: "#fee2e2"
+typography:
+ display:
+ fontFamily: "system-ui, -apple-system, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif"
+ fontSize: "2.1rem"
+ fontWeight: 650
+ lineHeight: 1.1
+ headline:
+ fontFamily: "system-ui, -apple-system, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif"
+ fontSize: "1.25rem"
+ fontWeight: 600
+ lineHeight: 1.25
+ letterSpacing: "-0.012em"
+ title:
+ fontFamily: "system-ui, -apple-system, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif"
+ fontSize: "0.875rem"
+ fontWeight: 600
+ lineHeight: 1.4
+ body:
+ fontFamily: "system-ui, -apple-system, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif"
+ fontSize: "0.875rem"
+ fontWeight: 400
+ lineHeight: 1.6
+ label:
+ fontFamily: "system-ui, -apple-system, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif"
+ fontSize: "0.71875rem"
+ fontWeight: 650
+ letterSpacing: "0"
+ textTransform: "none"
+ field:
+ fontFamily: "system-ui, -apple-system, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif"
+ fontSize: "1.0625rem"
+ fontWeight: 550
+ stat:
+ fontFamily: "system-ui, -apple-system, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif"
+ fontSize: "1.5rem"
+ fontWeight: 650
+ lineHeight: 1.1
+ mono:
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace"
+ fontSize: "0.78125rem"
+rounded:
+ bar: "2px"
+ sm: "4px"
+ md: "6px"
+ pill: "999px"
+spacing:
+ "1": "0.25rem"
+ "2": "0.5rem"
+ "3": "0.75rem"
+ "4": "1rem"
+ "5": "1.5rem"
+ "6": "2.5rem"
+components:
+ button:
+ backgroundColor: "{colors.surface}"
+ textColor: "{colors.text}"
+ typography: "{typography.title}"
+ rounded: "{rounded.sm}"
+ padding: "0.45rem 0.75rem"
+ button-primary:
+ backgroundColor: "{colors.accent-fill}"
+ textColor: "{colors.on-accent-fill}"
+ typography: "{typography.title}"
+ rounded: "{rounded.sm}"
+ padding: "0.45rem 0.75rem"
+ button-primary-hover:
+ backgroundColor: "#1d4dc8"
+ textColor: "{colors.on-accent-fill}"
+ input:
+ backgroundColor: "{colors.surface}"
+ textColor: "{colors.text}"
+ rounded: "{rounded.sm}"
+ padding: "0.45rem 0.6rem"
+ card:
+ backgroundColor: "{colors.surface}"
+ textColor: "{colors.text}"
+ rounded: "{rounded.md}"
+ padding: "1.1rem 1.25rem"
+ status-pill:
+ backgroundColor: "{colors.tone-neutral-bg}"
+ textColor: "{colors.tone-neutral-fg}"
+ rounded: "{rounded.pill}"
+ padding: "0.15rem 0.5rem"
+---
+
+# Design System: Fiesta
+
+<!--
+ Scope note, because this system has three themes and two colour schemes.
+ The frontmatter carries the `day` theme's LIGHT values, because
+ fiesta.ThemeNames()[0] is `day` and it is the theme fiesta.css carries.
+ The normative source in the repository is one custom property per colour
+ holding a light-dark() pair, so a single-value frontmatter slot cannot
+ hold it; .impeccable/design.json carries both halves per token under
+ extensions.colorMeta. Do not treat a hex here as the only value.
+ Source of truth: css/tokens.css (structure) and css/themes/*.css (colour,
+ type family, shape).
+-->
+
+## Overview
+
+**Creative North Star: "A Default With a Point of View"**
+
+This is the interface an application has before anyone has styled it,
+and it should look like someone chose it. `day` is near-greyscale with
+hairline borders, one cobalt for anything that acts, crisp corners, and
+natural depth: light comes from above, cards rest on the page, controls
+press in and slowly lift. It aims to read as professional, in the family
+of the tools people already trust, and to be recognisably this system's.
+
+Remixability is still the thesis. Every colour, radius, shadow,
+highlight and strength resolves through a custom property, and the three
+shipped themes prove the axes work rather than offering a menu of three
+answers: `day` turns depth on, `plain` and `signal` turn it off and
+render as they always did. An app that wants a fourth writes one `:root`
+block. Files are delivered once and app-owned from that moment: edit
+them, or delete what you do not use.
+
+The system is deliberately under-designed at the component level so an
+app's own character can land on top without a fight. It is explicitly
+not the SaaS gradient template — no purple-to-blue washes, glassmorphism,
+oversized rounded cards or decorative blobs — and explicitly not a
+vendor-branded admin kit that stamps someone else's identity on an app
+and resists removal. Both hand you an identity you did not choose, which
+is the one thing this system exists not to do.
+
+**Key Characteristics:**
+
+- Small, dense type: a 14px base with 12.5px and 11.5px steps below it.
+- Near-flat surfaces separated by 1px hairlines; depth is a theme axis.
+- One accent, carrying action and position; status carries its own
+ four-tone palette.
+- Every colour is a `light-dark()` pair, so light and dark are one
+ declaration rather than two stylesheets.
+- Works with JavaScript off. Scripts are enhancement and each is
+ deletable on its own.
+
+## Colors
+
+A restrained near-neutral palette with a single working blue, held to
+WCAG 2.2 AA by a contrast gate over documented pairs in every theme and
+both schemes.
+
+### Primary
+
+- **Interface Blue** (`#1f56d6` light, `#7aa2ff` dark): the accent, a
+ cobalt. It marks what you can act on and where you are: focus rings,
+ links, an active tab, the current menu item. It belongs to the chrome,
+ not to the content.
+- **Accent Fill** (`#2257df` light, `#2f61e8` dark): the primary
+ button's fill, with a hover fill of its own (`#1d4dc8` light,
+ `#2956d0` dark) and a white label (`on-accent-fill`). A filled control
+ never darkens by filter: every state is a colour, gated.
+- **Danger Fill** (`#c62f2f` light, `#c43232` dark): the danger button's
+ fill, the same way, with a white label.
+- **Interface Blue Deep** (`#1844ac` light, `#a3c0ff` dark): kept for a
+ theme written before the fills; `tokens.css` reads it only as the
+ primary hover's fallback.
+- **Interface Blue Wash** (`#ecf1ff` light, `#16234a` dark): the tint
+ behind a current menu item, and the fallback for the hover and press
+ tints in a theme that declares none. Carries no text of its own beyond
+ the accent and the muted greys, both gated against it.
+
+### Neutral
+
+- **Page** (`#fbfbfc` light, `#0c0d0f` dark): the page ground, one step
+ below the surface, so a card reads as resting on it.
+- **Surface** (`#ffffff` light, `#141518` dark): cards, panels, inputs
+ and menus.
+- **Surface Sunken** (`#f3f3f5` light, `#18191d` dark): the recessed
+ step, for table header rows, the second tone in a stack and a hovered
+ ghost button.
+- **Hairline** (`#e6e6ea` light, `#26282e` dark): every divider, card
+ edge and table rule.
+- **Control Edge** (`#868691` light, `#71737c` dark): the border of a
+ field and a select. Deliberately much darker
+ than the hairline, and held at 3:1 against its backgrounds because it
+ is a control boundary (WCAG 1.4.11), not decoration.
+- **Control Border** (`#d0d4dc` light, `#343843` dark): a plain button's
+ edge, softer than the field border on purpose. The label identifies
+ the button, so the edge is decoration under 1.4.11 and carries no 3:1
+ floor.
+- **Ink** (`#18181b` light, `#ededf0` dark): body text.
+- **Ink Muted** (`#52525c` light, `#a1a1aa` dark): secondary text,
+ subheads, cell values that are not the row's name.
+- **Ink Faint** (`#6b6b75` light, `#8c8c96` dark): hints, captions,
+ counts. Still gated at 4.5:1; "faint" is a role, not a licence to
+ drop below the floor.
+
+### Tertiary
+
+The four status tones, each a foreground/background pair used together:
+**Neutral** (`#374151` on `#eef0f2`), **Positive** (`#166534` on
+`#dcfce7`), **Warning** (`#854d0e` on `#fef3c7`), **Negative**
+(`#b91c1c` on `#fee2e2`). They exist so status never borrows the accent.
+
+### Named Rules
+
+**The Gated Pair Rule.** A colour that carries text owes 4.5:1 against
+the surface it actually sits on, and a control boundary owes 3:1 — and
+owes it in every theme and both schemes, in the contrast gate, before it
+ships. Adding a colour means adding its pair to that gate. A tone chosen
+against a tinted ground says nothing about the same tone on a plain card.
+
+**The Two-Signal Rule.** State is never colour alone. A status pill
+renders its label, a meter prints its fraction as text beside the bar,
+and a stat's change carries its own `+` or `−` sign. Colour is the
+second signal, never the first.
+
+**The Light-Dark Rule.** Every colour is declared once as
+`light-dark(<light>, <dark>)` under `color-scheme: light dark`. There is
+no dark stylesheet and no second palette to keep in step; an explicit
+theme choice re-resolves the same declarations by setting
+`color-scheme`.
+
+## Typography
+
+**Display / Body Font:** the platform UI stack — `system-ui`,
+`-apple-system`, `"Segoe UI"`, `Roboto`, `"Helvetica Neue"`, `Arial`,
+`sans-serif`. `signal` leads the same stack with `"Helvetica Neue"`.
+**Mono Font:** `ui-monospace, SFMono-Regular, Menlo, Consolas,
+monospace`, on the `rst-mono` utility. It is a real second family and
+not a weight of the first — reference codes, identifiers and machine
+values are set in it so they read as things to be copied rather than
+read.
+
+**Character:** the type has no personality of its own on purpose. The
+family is a theme axis, and the shipped default is whatever the reader's
+OS considers correct — which is the right answer for an application
+frame and the wrong answer for a brand, so an app that wants a voice
+sets one property.
+
+### Hierarchy
+
+- **Display** (650, 2.1rem, 1.1): the lead reading in a stat band, and
+ nothing else. The only genuinely large type in the system.
+- **Headline** (600, 1.25rem, 1.25, −0.012em in `day`; 1.375rem and
+ −0.015em in `plain` and `signal`): a screen's `<h1>` in the page
+ header. Capped at 1.25rem under 34rem.
+- **Field** (550, 1.0625rem = 17px, inherited 1.5): the form's headline
+ input — a title, a name — and the only control that leaves the body
+ step. Its label stays at Label size, so the emphasis is carried by
+ the reader's own words rather than by the form's furniture.
+- **Title** (600, 0.875rem, 1.4): section headings inside a card head,
+ button labels, a row's name. A section `<h2>` separates by weight, not
+ size: body-sized in `plain` and `signal`, a step below it (0.8125rem)
+ in `day`.
+- **Body** (400, 0.875rem = 14px, 1.6): everything else. Subheads cap at
+ 44rem.
+- **Label** (650, 0.71875rem = 11.5px): the eyebrow over a value, such
+ as stat labels, table column heads and the nav rail's section names.
+ Sentence case with no tracking in `day`; uppercase at +0.06em in
+ `plain` and `signal`.
+
+### Steps outside the roles
+
+Four literal sizes appear in `tokens.css` outside the roles above, and
+they are recorded rather than tidied away — two have since become roles,
+which is what recording them was for:
+
+- **1.5rem** — a stat's number when it is not the lead. A role now
+ (`stat`), because a dashboard uses it as one.
+- **1.25rem**: the headline's cap under 34rem, and `day`'s headline at
+ every width. A step of `headline` rather than a size of its own.
+- **1.0625rem** — a role now (`field`, and `--rst-fs-lg`), because a
+ form's headline input reads as one. The large person avatar's initials
+ and the shell's brand land on the same value and stay literals: they
+ are sized to fit a circle, so the match is a coincidence rather than a
+ shared step, and tying them to the type scale would move them the next
+ time the scale moves.
+- **0.6875rem** — the ordinary person avatar's initials.
+
+That last one is decoration sized to fit a circle, not type a reader
+parses, which is why it is not a role.
+
+### Named Rules
+
+**The Small-Base Rule.** The base is 14px, not 16px, and every step is
+declared in `rem` so the whole scale tracks a reader who has raised
+their browser default. Do not convert any of it to `px`.
+
+**The Weight-Not-Size Rule.** Hierarchy inside a screen is carried by
+weight and colour before size. A card's `<h2>` is body-sized at 600;
+reaching for a larger size to make a section feel important is how this
+system loses its density.
+
+The one exception is a form's headline field (the Field role above),
+and it is an exception because of what is being sized. Everywhere else
+the rule governs *chrome* — a heading the system wrote, labelling
+content underneath it — and chrome has no business growing. A title
+input is the opposite case: the large thing is what the reader typed,
+which is the only content on a form that is not furniture. The
+exception is deliberately narrow, one control on one form; a second
+large field on the same screen is the density loss this rule is about.
+
+**The Grouped Number Rule.** Every number a person reads as a quantity is
+grouped for their locale — `54,173` in English, `54.173` in German,
+`٥٤٬١٧٣` in Arabic. Never a bare run of digits. Grouping is not "add a
+comma": the separator, the group size and the digits themselves all
+change with the locale, and a number's readability is the whole reason
+the rule exists — `54173` is counted, `54,173` is read.
+
+The exception is exact and small: **an identifier is not a quantity.**
+Reference codes, order numbers, years, versions and port numbers are
+labels that happen to be made of digits, and grouping one changes what it
+appears to be. Order 4471 is not order 4,471.
+
+The 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.
+
+Money 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`.
+
+Each 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.
+
+## Layout
+
+A single centred content column, capped at **64rem** with `1.5rem 1rem
+2.5rem` of padding, holding every screen. The column is markup an app
+emits (`rst-page`); no partial brings its own.
+
+**Spacing** is a six-step rem scale — 0.25 / 0.5 / 0.75 / 1 / 1.5 /
+2.5rem — and screens stack vertically in reading order: page header,
+then section head plus card, then the next. Horizontal arrangement is
+reserved for the idioms that ship it (card heads, field rows, toolbars,
+list-row cells, segmented tabs, stat bands).
+
+**Breakpoints.** 800px is the system's real line: `rst-m-hide` columns
+fold away, the sidebar rail collapses into a disclosure, and the topbar
+takes its narrow layout. 34rem (544px) stacks the page header's title
+and action. **320px is a written commitment** — the layout reflows to it
+without a horizontal scrollbar.
+
+**Density** is high by intent: 14px base, 0.45rem control padding, rows
+that fit a working screenful. The floor on that density is WCAG 2.2's
+Target Size (2.5.8) — pagination chips measure about 30px, row-action
+pills about 27px and buttons about 34px on their smaller axis, all clear
+of the 24px minimum. Do not shrink them without re-measuring.
+
+The scrollbar gutter is reserved whether or not there is a scrollbar in
+it (`--rst-scrollbar-gutter`), so a short page and a long one are the
+same width and moving between them does not slide the layout sideways.
+
+## Elevation & Depth
+
+Depth is a theme axis, and `day` turns it on. The light comes from
+above: a 1px highlight along a raised edge (`--rst-depth-hl`), a short
+contact shadow where an object meets the page (`--rst-depth-contact`),
+soft ambient light around it (`--rst-depth-ambient`), a face shade for
+filled controls (`--rst-depth-face`, 0.04 in `day`), and a 1px drop for
+a pressed button (`--rst-press-drop`). Buttons are a touch proud of the
+page and press in; cards rest on it; menus float, with tight elevation;
+fields sit slightly into it. `plain` and `signal` set every one flat,
+and a theme written before the axis is read through fallbacks that are
+today's values.
+
+### Shadow Vocabulary (the `day` theme)
+
+- **Pop** (`0 2px 4px light-dark(rgba(16,24,40,0.06), rgba(0,0,0,0.35)),
+ 0 8px 16px -4px light-dark(rgba(16,24,40,0.1), rgba(0,0,0,0.5))`): an
+ open menu, a dropdown panel, a modal: surfaces that float over the
+ page. Real elevation, kept tight.
+- **Knob** (`0 1px 2px light-dark(rgba(16,24,40,0.18), rgba(0,0,0,0.4))`):
+ the moving part of a control, such as a switch's knob.
+- **Lift** (`0 1px 2px light-dark(rgba(16,24,40,0.08), rgba(0,0,0,0.3))`):
+ the barely-there separation on a raised strip.
+- **Overlay** (`light-dark(rgba(0,0,0,0.45), rgba(0,0,0,0.6))`): the
+ modal scrim. Deeper in dark on purpose: black reads lighter against a
+ dark backdrop and needs more alpha to still dim.
+
+## The light
+
+`light.js` moves the light. The shadows' light comes from the hour: the
+left in the morning, overhead at noon, the right in the evening, so
+shadows lean away from it by up to 1.5px and the top highlight leans
+toward it. With a mouse, a button catches a sheen where the pointer is
+(white on a filled button, a faint accent tint on a plain one in light
+mode), and a card's rim brightens near the pointer over about a second
+and fades when it leaves. On a phone a pressed button shows the press,
+not the sheen, and a tapped card catches the light briefly. The whole
+page's light can follow the pointer instead: set `FOLLOW` to `true` at
+the top of the file. It is off because every write of it restyles the
+page.
+
+It does not run under reduced motion, does nothing under forced colours,
+reads no sensor, and never captures the pointer. For the still design,
+leave `light.js` out; nothing else asks for it.
+
+## Shapes
+
+Soft rectangles, and the exact softness is a theme axis: `day` uses 6px
+on cards and 4px on controls, `plain` 4px and 3px, `signal` 4px and 2px.
+A pill radius (999px) is reserved for status pills and count chips —
+things that are read as tokens rather than operated.
+
+A 2px radius is the thin-bar step, and it is not on the theme axis: the
+capacity meter and a running job's progress bar both use it, at a height
+of 4px where a themed 8px corner would swallow the bar whole.
+
+Borders do most of the structural work: 1px hairlines between and around
+surfaces, and a distinctly darker 1px edge on anything operable. Focus
+is a 2px accent outline offset 2px, never a border swap, so focus never
+changes an element's size.
+
+**Known gap, recorded rather than smoothed over.** `tokens.css` also
+hard-codes 9px, 7px, 4px, 10px and 14px radii in a handful of places —
+the combobox and date-picker lists, and a few small chrome pieces — that
+do not resolve through `var(--rst-radius*)`. A theme that changes its
+corner language does not change those. It is a real limit on the
+remixability this system claims, it predates this record, and it is not
+something to fix by editing one component in passing.
+
+**The Derived Rule Rule.** The page header's underline is not authored
+per theme: it is `color-mix(in oklab, var(--rst-accent) N%, var(--rst-line))`,
+mixed at 45% in `signal`, and left as the plain hairline in `plain` and
+in `day`, which keeps its cobalt for things that act. A theme that
+changes its accent gets a matching rule with no second value to keep in
+step. It is decorative and carries no contrast floor.
+
+## Components
+
+### Hover and press
+
+Hover is quiet and press is clear. A plain button's hover is a light
+grey face and a darker border; a filled button's is a deeper fill; a
+ghost's is a surface tint. Rows, menu items and sidebar items lift: a 5%
+accent tint (9% in dark), a highlight along the top edge, no sheen. The
+current sidebar item is a selected thumb, and hovering another is a
+half-raised preview of it. These tints and lifts apply only on a
+device that can hover, so a tap leaves no tint behind; the phone index
+rows keep their tap tint on purpose.
+
+A press is instant and slow to leave. The button drops 1px, loses its
+highlight and outer shadow for an inset one, switches its sheen off and
+darkens, most under the pointer; on release the darkness lifts over
+about 0.7s, the spot under the finger last. A row pressed, or its link
+tapped, takes a stronger tint and sinks. Under reduced motion nothing
+moves and nothing fades.
+
+### Buttons
+
+- **Shape:** crisp (`{rounded.sm}`, 4px in `day`).
+- **Default:** control face, control-border edge (softer than the field
+ border on purpose), Ink label at 600 and 12.5px, `0.45rem 0.75rem`
+ padding. Hover: a light grey face and a darker border.
+- **Primary:** the accent fill with a 4% top-to-bottom shade and a white
+ label; hover deepens the fill.
+- **Danger:** the danger fill, the same way.
+- **Ghost:** no fill, muted label; hover a surface tint, with the label
+ darkening to Ink rather than picking up the accent. The accent means
+ "this goes somewhere new", and a ghost Cancel does not.
+- **Press:** every variant sinks and darkens; see Hover and press.
+- **Focus:** 2px accent outline, 2px offset.
+
+### Cards / Containers
+
+- **Corner Style:** `{rounded.md}` (6px in `day`).
+- **Background:** Surface, on a Page ground one step below it, with the
+ hairline painted as the outer stop of the rim gradient. To recolour a
+ card, set `--rst-surface` on it, which also recolours the surfaces
+ inside it; a `background` of your own paints over the hairline and a
+ `background-color` sits under the face.
+- **Shadow Strategy:** at rest, a hairline and two ambient layers lit
+ from above; the rim catches the light near the pointer. Menus float
+ with real but tight elevation.
+- **Internal Padding:** `1.1rem 1.25rem`. A card head (`rst-box-head`)
+ sits *outside* the card, holding an `<h2>` and one compact button.
+- **Note:** list cards and data grids hold rows only and are unpadded by
+ design; prose, forms and links go in a padded box.
+
+### Inputs / Fields
+
+- **Style:** Surface fill, 1px Control Edge (`--rst-line-strong`)
+ border, a 1px inset shadow so the field sits into the page,
+ `{rounded.sm}`, `0.45rem 0.6rem` padding, full width by default.
+- **Focus:** the accent border, a 3px accent halo, and the 2px outline
+ offset 2px.
+- **Error:** the message is text, and the field is described by it via
+ `aria-describedby`; the required marker uses the negative tone.
+
+### Navigation
+
+The rail sits on the page background. The current item is a surface
+thumb with a top highlight, a hairline ring and a contact shadow, marked
+by `aria-current` in the markup; a hovered item is the thumb's
+half-raised preview. Below 800px the rail collapses into a
+`<details>` disclosure with a menu icon. Menus are
+`<details name="rst-menus">`, so opening one closes the rest with no
+script; `fiesta.js` adds outside-click and Escape as enhancement.
+
+### Status Pill
+
+A pill-radius tinted chip carrying a foreground/background tone pair and
+**always its label**. The leading dot is decoration on top of the word,
+never a substitute for it.
+
+### Stat Band (signature)
+
+The instrument strip a dashboard opens with: a row of stat cells in one
+card, divided by inline-start hairlines, wrapping rather than squeezing
+so any number of cells works. One cell may be marked `lead`, which is
+the same component at 2.1rem instead of 1.5rem — there is no separate
+headline component. Numbers are `tabular-nums` so a polled figure does
+not jitter its neighbours. A change carries its own sign in the text and
+a tone the caller supplies; the tone is never derived from the sign,
+because a falling number is good about as often as it is bad.
+
+## Do's and Don'ts
+
+### Do:
+
+- **Do** change the system by setting custom properties. Every colour,
+ radius, shadow and type family resolves through one; a fourth theme is
+ a single `:root` block.
+- **Do** declare colours as `light-dark(<light>, <dark>)` pairs under
+ `color-scheme: light dark`, so light and dark stay one declaration.
+- **Do** add any new colour that carries text to the contrast gate, at
+ 4.5:1 against the surface it actually sits on, in all three themes and
+ both schemes.
+- **Do** give state a second signal besides colour — a label, a sign, a
+ number as text.
+- **Do** group the digits of any number a person reads as a quantity, for
+ their locale — and never group an identifier, a year or a version.
+- **Do** keep type in `rem` and the base at 14px, so the scale tracks a
+ reader's own browser setting.
+- **Do** use logical properties (`border-inline-start`,
+ `margin-block-end`) so borders and dividers land correctly in Arabic.
+- **Do** let the scriptless rendering be the real one; add script only as
+ enhancement that can be deleted.
+
+### Don't:
+
+- **Don't** reach for the SaaS gradient template — purple-to-blue
+ washes, glassmorphism, oversized rounded cards, decorative blobs.
+- **Don't** stamp a vendor kit's identity onto an app. This system is
+ under-designed on purpose so the app's own character can land on top.
+- **Don't** use the accent as decoration or mood. It marks what you can
+ act on and where you are; status has its own four tones.
+- **Don't** float a resting surface. A card rests on the page with the
+ theme's ambient light; a pop shadow means something has left the page
+ plane.
+- **Don't** make a section feel important by making it bigger. Weight and
+ colour carry hierarchy before size; a card's `<h2>` is body-sized.
+- **Don't** shrink control padding without re-measuring against WCAG
+ 2.2's 24px target-size minimum.
+- **Don't** compose a heading, a paragraph and a button side by side in a
+ flex row. Screens stack vertically; horizontal arrangement is reserved
+ for the idioms that ship it.