/* rastrillo/ui — design tokens and the component classes the shipped partials use (see ui_test.go's TestAllPartialsAreDefined for the current, authoritative list — deliberately not counted here, so this comment cannot go stale as the set grows). rastrillo new writes this file once into your app's static/ directory. From then on it is yours: edit it, replace it, delete rules you do not want. Nothing regenerates it, and rastrillo.Serve never serves it — your app's own static handler does. Every class, attribute and custom property is prefixed rst- — except the single .icon rule, which styles the class rastrillo.Icon emits — so dropping this file into an app with existing CSS has exactly one known collision surface. There are no imports, no remote assets and no webfont: this stylesheet fetches nothing, from anywhere, ever. That is a test, not a wish (ui_test.go, TestTokensCSSIsSelfContained) — which is also why this comment cannot spell those constructs out literally. TWO SPELLINGS, ON PURPOSE, FOR ONE RELEASE. Every component rule below names its class and its attribute twin: `.rst-btn, [rst-btn]`, `.rst-btn--primary, [rst-btn~="primary"]`, `.rst-callout__body, [rst-callout-body]`. The attribute spelling is the vocabulary this library is moving to (design spec §6-v3); the class spelling is what every app and every shipped partial writes today. Pairing them here first is what makes the move non-breaking in both directions — this file is written into your static/ once and frozen there, while the partials upgrade with the module, so an app that has not refreshed its copy must keep styling either way. The grammar, so a rule you add follows it: the KIND is a bare attribute; a VARIANT is a token in that attribute's value, matched with ~= so `rst-btn="primary compact"` composes the way a class list does; a PART is its own flat attribute (`rst-callout-body`), never a value — the value slot means variant on every attribute or the grammar is unreadable; and rst-tone carries the four tones that data-tone carries today. Both members of a pair weigh exactly the same: a class and an attribute selector are both (0,1,0), so pairing changes no cascade anywhere. ui_test.go's TestEveryClassSelectorHasAnAttributeTwin holds all of that, including the specificity, and the browser drive proves the two spellings compute identical styles. Utilities keep the class spelling and are not paired: rst-sr-only, rst-mono, rst-m-hide, rst-grow, rst-nm, rst-danger, rst-cell-mut. They are not kinds — they are cross-cutting styling, which is what class is for, and they stay class after the move. Colour, the type family and shape are not here: they live in themes/day.css (or whichever theme was scaffolded), which rastrillo new writes beside this file as static/theme.css. This file is structure — layout, spacing, the type scale, and the component classes, every one of them painted with var(--rst-*). Swap the theme file and the app changes palette, corner and depth; nothing here is touched. Radius and shadow used to live in the scale block below. They moved to the themes in v2, because a theme that can only change colour cannot change how an app feels: day is soft (8px/6px, wide low-alpha shadows), plain is nearly square (4px/3px, one hairline layer) and signal is milled (4px/2px, short dense shadows). tokens.css still paints with var(--rst-radius*) and var(--rst-shadow-*) everywhere; it just no longer decides what they are. Padding values are load-bearing for SC 2.5.8 Target Size (AA, new in WCAG 2.2): pagination chips measure about 30px, row action pills about 27px, and the button sizes about 28px (sm), 34px (default) and 44px (lg) on their smaller axis, all clear of the 24px minimum. Do not shrink them without re-measuring; the button block shows the arithmetic. Type sizes are rem, not px, so the whole scale tracks a reader who has raised their browser's default font size. Spacing is rem for the same reason. Anything you add here should follow suit. ──────────────────────────────────────────────────────────────────── */ /* ── Scale: type and spacing. Theme-independent. Radius is not here — it is part of the theme's shape axis; see themes/. ────────────── */ :root { --rst-fs-lg: 1.0625rem; /* 17px — the one step above base */ --rst-fs-base: 0.875rem; /* 14px at the browser default */ --rst-fs-sm: 0.78125rem; /* 12.5px */ --rst-fs-xs: 0.71875rem; /* 11.5px */ --rst-sp-1: 0.25rem; --rst-sp-2: 0.5rem; --rst-sp-3: 0.75rem; --rst-sp-4: 1rem; --rst-sp-5: 1.5rem; --rst-sp-6: 2.5rem; /* The tap floor on small or touch screens: 44px at the default root, and it tracks a reader who raised their default size. Declared here so an app's own CSS can use it; only rules inside the touch query below read it, so desktop density never sees it. */ --rst-tap: 2.75rem; /* The list grid's kebab column. A variable rather than the literal 32px the samples used to write, so the column can grow to a 44px kebab on touch screens (the touch block redefines it). An app that keeps a literal 32px still works: the kebab is justify-self: end, so it overflows 12px toward the inline start, into the grid's 0.85rem gap, without reaching the cell beside it. */ --rst-col-menu: 32px; /* The scrollbar's gutter, reserved whether or not there is a scrollbar in it, so a short page and a long one are the same width and clicking between them does not slide the layout sideways. A token rather than a class for three reasons: the opt-out is one line in your own stylesheet (--rst-scrollbar-gutter: auto), it layers like every other token, and custom properties are untouched by the markup migration, so this survives it without an edit. What it costs, chosen knowingly: a page too short to scroll now reserves the strip as well, so there is a thin empty band at the trailing edge where there was none. That is the price of never shifting. both-edges would double the band to keep centred content exactly centred, and was not taken: this system's pages are already a max-width column inside a wider ground, so the asymmetry does not land where it would be seen. Do not over-claim what it fixes. macOS overlay scrollbars take no layout space at all, so on a default Mac there was never anything to shift. This is real on Windows and Linux, on a Mac set to "always show scrollbars", and inside the design gallery's own preview iframes. Chrome 94+, Firefox 97+, Safari 18.2+; an older engine ignores the declaration and lands on the behaviour it has today. */ --rst-scrollbar-gutter: stable; } /* ── Small or touch screens: the type scale moves up one step ─────── One query decides "a phone, a tablet, or a window too narrow to read at desktop density", and every touch-only rule in this file sits in it. pointer: coarse is the PRIMARY pointer, so a touchscreen laptop driven by its trackpad keeps desktop density; any-pointer would switch a mouse user's whole interface because a touchscreen exists. max-width: 40rem catches a narrow desktop window and the design gallery's 390px Mobile tab, a fine pointer showing what a phone shows (a rem in a media query is the browser's initial size, not the root's). The shells' 800px is a different axis: between 640 and 800px under a mouse is a split window, which gets the narrow LAYOUT and keeps mouse DENSITY. The four tokens move and nothing else: the root size, the rem spacing and every px geometry stay, and controls grow because their text grows and because the touch block at the end of this file gives them a minimum box. Display sizes (page titles, stat numbers, avatar initials) stay literal on purpose, so a 22px title sits over 16px body text. ─────────────────────────────────────────────────────── */ @media (pointer: coarse), (max-width: 40rem) { :root { --rst-fs-lg: 1.1875rem; /* 19px */ --rst-fs-base: 1rem; /* 16px */ --rst-fs-sm: 0.875rem; /* 14px */ --rst-fs-xs: 0.8125rem; /* 13px */ } } /* ── Base. Three rules, so a freshly scaffolded app looks finished before it has any CSS of its own. Delete them if your app owns its own base styles. ──────────────────────────────────────────── */ /* The gutter is declared on the root element, which is where the viewport's scrollbar lives. It also fixes a second instance of the same bug that nobody had attributed to the scrollbar: the modal's scroll lock below (body:has(.rst-backdrop) { overflow: hidden }) takes the scrollbar away the instant a modal opens, and without a reserved gutter the whole page slides sideways while the reader watches. One declaration, both cases. */ html { scrollbar-gutter: var(--rst-scrollbar-gutter); } body { margin: 0; background: var(--rst-bg); color: var(--rst-text); font-family: var(--rst-font); font-size: var(--rst-fs-base); line-height: 1.5; } /* The content column every screen sits in. Your page markup emits this; no partial does. 64rem, widened from 52rem after the v2 page went live: 52rem is a reading measure, and this column holds list grids and side-by-side form fields, which a reading measure squeezes. */ .rst-page, [rst-page] { box-sizing: border-box; margin: 0 auto; max-width: 64rem; padding: var(--rst-sp-5) var(--rst-sp-4) var(--rst-sp-6); } /* rastrillo.Icon emits class="icon" with no width or height, so the icon sizes from here and tracks the text beside it. */ .icon { block-size: 1em; flex: none; inline-size: 1em; } /* Focus is visible everywhere inside the app column (.rst-page) and inside any component rendered outside one. :where() contributes no specificity, so any rule below can override it. */ :where(.rst-page, .rst-page-header, .rst-list, .rst-lbar, .rst-search, .rst-empty, .rst-pagination) :focus-visible, :where([rst-page], [rst-page-header], [rst-list], [rst-lbar], [rst-search], [rst-empty], [rst-pagination]) :focus-visible { outline: 2px solid var(--rst-accent); outline-offset: 2px; } /* ── Buttons: page-header's primary action, empty-state's CTA. ────── */ /* border-box because rst-btn goes on as often as on