rastrillo / fiesta Public
--- 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. - 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. - 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 (
#1f56d6light,#7aa2ffdark): 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 (
#2257dflight,#2f61e8dark): the primary button's fill, with a hover fill of its own (#1d4dc8light,#2956d0dark) and a white label (on-accent-fill). A filled control never darkens by filter: every state is a colour, gated. - Danger Fill (
#c62f2flight,#c43232dark): the danger button's fill, the same way, with a white label. - Interface Blue Deep (
#1844aclight,#a3c0ffdark): kept for a theme written before the fills;tokens.cssreads it only as the primary hover's fallback. - Interface Blue Wash (
#ecf1fflight,#16234adark): 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 (
#fbfbfclight,#0c0d0fdark): the page ground, one step below the surface, so a card reads as resting on it. - Surface (
#fffffflight,#141518dark): cards, panels, inputs and menus. - Surface Sunken (
#f3f3f5light,#18191ddark): the recessed step, for table header rows, the second tone in a stack and a hovered ghost button. - Hairline (
#e6e6ealight,#26282edark): every divider, card edge and table rule. - Control Edge (
#868691light,#71737cdark): 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 (
#d0d4dclight,#343843dark): 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 (
#18181blight,#ededf0dark): body text. - Ink Muted (
#52525clight,#a1a1aadark): secondary text, subheads, cell values that are not the row's name. - Ink Faint (
#6b6b75light,#8c8c96dark): 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 inplainandsignal): 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 inplainandsignal, a step below it (0.8125rem) inday. - 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 inplainandsignal.
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 ofheadlinerather 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.
The 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.
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 inday). - Default: control face, control-border edge (softer than the field border on purpose), Ink label at 600 and 12.5px,
0.45rem 0.75rempadding. 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 inday). - 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-surfaceon it, which also recolours the surfaces inside it; abackgroundof your own paints over the hairline and abackground-colorsits 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.6rempadding, 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.
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.
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
:rootblock. - Do declare colours as
light-dark(<light>, <dark>)pairs undercolor-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
remand 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.
- 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.