Components, looks, options and generators
Design tokens made a theme’s values data, and Styles let an administrator lay values of their own over them. Four things are built on that ground, and this chapter is about them: component tokens, which let a style change one part of the kit without moving everything that shares a foundation; looks, which let one block differ from the style; options and variants, the choices of markup a theme leaves to a style; and generators, which make a whole style, or a part of one, from a few answers. SPECS.md §20 (“Tokens”, “Options and variants”, “Generators”) and §10 (“Looks”) are the terse reference; the administrator’s side is the user book’s Styles and Blocks.
The words
Section titled “The words”The track gave a dozen words a precise meaning. They are used that way everywhere — in the code, in SPECS.md, in these books and in the panel’s English.
| Word | What it is | Where it lives |
|---|---|---|
| Token | A CSS custom property with a typed value: --color-accent, --radius-box, --button-padding-x |
themes/<name>/tokens.json, read by Hilms\Theme\Tokens |
| Foundation token | One of a theme’s basic decisions: a colour, the spacing unit, a type size, a rounding, a face, a shadow, a duration, a layout measure. 70 in the default theme | the file’s groups without a tier, or "tier": "foundation"; the 62 a theme with no parent must have are Contract\FoundationTokens |
| Component token | One decision of one part of the kit, named --<component>-<property>, which follows a foundation until a style changes it: --button-radius follows --radius-control. 95 in the default theme |
groups marked "tier": "component"; all 95 are Contract\ComponentTokens |
| Theme | The code package: Blade views, a build, a manifest, a token file. A developer’s | themes/<name> |
| Style | Data: the values one theme’s tokens take instead of their own, and the options and variants it picks. An administrator’s | a row of styles; a built-in style is a file of the theme, themes/<name>/styles/<slug>.json, synced into a read-only row |
| Option | A choice of markup for a part every page shares, which no token reaches: header.layout is start, centred or split |
declared in theme.json under options, picked by a style |
| Variant | A choice of markup for one block type: a callout is soft, bordered or bold |
declared in theme.json under variants, picked by a style for every block of the type, and by a block’s look for itself |
| Look | One block’s own choices over the style — its surface, padding, corners, width, alignment and variant — always among the style’s own options, never a free value | the block’s look key, Hilms\Blocks\Looks |
| Contract | What a theme answers for. There are six: the views a theme draws and the variables each receives (ViewContract, from every module’s Theme\ContractViews, CoreViews and AccessViews), the shell (Shell), the accessibility rules (Accessibility), the contrast pairs (ContrastPairs), and the foundation and component tokens (FoundationTokens, ComponentTokens) |
app-modules/theme/src/Contract |
| Generator | Code that writes a style’s tokens from a few seeds: a brand colour, a tint, a body size and a ratio, a density, a shape, the faces | Hilms\Theme\Styles\Generators |
“Preview” means five different things, and a sentence that says only “the preview” is ambiguous:
- A style’s preview — the site drawn in a style for the one administrator who pressed Preview on it (
Styles\StylePreview, the session keystyles.preview, the banner). - A block preview — a block drawn on the canvas of a panel editor: its view receives
$preview = trueand sits inside a.hilms-block-previewwrapper. - The preview build — the stylesheet the panel draws block previews with, the manifest’s
previewentry (css/preview.cssin the default theme), linked bytheme()->previewStylesheets(). - A draft page’s preview — an unpublished page read by somebody who may edit it, its view receiving
$preview = trueand drawing a notice. - A course’s Preview action — on the courses list in the panel, it only opens the public course page in a new tab.
The design catalogue (/design) is none of these: it is a developer’s gallery of every contract view drawn from samples.
Two names changed during the track because they collided with others: the foundation contract was Contract\Tokens and is Contract\FoundationTokens, and Block::option(), which reads an enum out of a block’s data, is Block::enumOf(), since “option” now means a theme’s option.
Component tokens
Section titled “Component tokens”A foundation is shared by design: --radius-box rounds every card, panel, alert and box-like block at once. That is right for a theme and too coarse for a style — an administrator who wants square buttons must not square every field with them. A component token is the seam: the button draws with --button-radius, which says {radius.control} in the theme’s file, so the button moves with the foundation until a style gives it a value of its own, and then only the button moves.
Naming and shape
Section titled “Naming and shape”--<component>-<property>, outside Tailwind’s namespaces (--button-radius, never--radius-button), so a component token makes no utility of its own and the kit asks for it by name. The parts: buttons, form fields, cards, badges, chips, alerts, callouts, the box-like blocks, headings, links, the header, the footer, the rail, drop-down panels, icon buttons and the progress bar.SPECS.md§20 lists them all.- A tier. A group in
tokens.jsonsays"$extensions": {"hilms": {"tier": "component"}}, and every token inside it inherits that (TokenTier). A document — an export, a style’s stored overrides — writes the tier on every component token it holds, so a file cut down to a few tokens still says what each one is. - A component token follows something. Either an alias to a foundation —
"$value": "{radius.control}"— which staysvar(--radius-control)all the way to the browser, or a length counted in steps of the spacing unit:"$value": {"value": 1, "unit": "rem"}, "$extensions": {"hilms": {"steps": 4}}is writtencalc(var(--spacing) * 4)and follows the density of whatever style it is drawn in.$valuekeeps the length the count comes to where the file was written, which is what any other tool reads; the count wins in HiLMS (Values\StepsValue, a whole or a half step from 0 to 64). Two counts are equal whatever unit each was written against (Values\Equality::same()), which is what the import and the generators compare them by. - There are no text-size component tokens. An arbitrary size utility (
text-(length:--button-size)) would drop the line height that the named size (text-sm) sets with it; a part that must be larger takes another named size.
How the kit draws them
Section titled “How the kit draws them”Every atom of the theme’s kit and every part of the shell draws each such property through its token, with Tailwind’s arbitrary forms. They read differently from what their names suggest, and every one below was checked in the build:
| Property | Write | Not |
|---|---|---|
| a colour | bg-(--button-surface), text-(--link-ink), ring-(--field-border) |
— |
| a length on a side | border-l-(length:--callout-rule-width) |
border-l-(--x), which is a colour |
| a size | text-(length:--x) (avoid: no line height) |
text-(--x), which is a colour |
| a weight | font-(--button-weight) |
— |
| a face | font-(family-name:--heading-font) |
font-(--x), which is a weight |
| letter spacing | tracking-(--heading-tracking) |
— |
| a shadow | shadow-(--card-shadow) |
shadow-box: Tailwind writes the value of a named shadow into the utility itself, so no style could ever change it |
The last row was a real bug: before the component tokens, the kit drew shadow-box, and a style changing --shadow-box changed nothing anyone could see. ThemeStyleGuardTest now fails shadow-box, shadow-raised and shadow-control anywhere in a theme or a generator stub, and holds a list, per atom, of the utilities it drew with before tokens, none of which may come back.
The typography plugin’s reading text is bound in prose.css: --tw-prose-links reads --link-ink, the headings --heading-ink.
Adding a component token
Section titled “Adding a component token”- Put it in the component’s group of
tokens.json, as an alias to the foundation it follows or a count of steps; give it a$description. - Add it to
Contract\ComponentTokenswith its type and a one-line meaning:ThemeTokensTestholds the default theme to the contract both ways, andhilms:theme:checkmakes a theme with no parent answer for it. - Label it in
theme::styles.tokens.<key>in both languages; the Components tab shows it under its component’s section. - A colour drawn on top of another goes into
Contract\ContrastPairswith the ratio it needs, or nothing judges it — neither the suite, nor the editor’s advice, nor the generators. bin/artisan hilms:theme:tokens, draw it in the atom,npm run build.- Prove nothing moved:
npm run visualagainst a baseline taken before the change saysIdentical.A component token that follows its foundation draws exactly what the foundation drew.
A style is site-wide. A look is the exception one block makes: this callout padded more, that quote centred, this video narrower than the text. It never holds a value of its own. Every choice is one of the style’s own options — a surface the theme defines, a step of its spacing, one of its radii — so a block with a look still follows the style, only from another token.
Which blocks, and which controls
Section titled “Which blocks, and which controls”A block declares what it takes through looks(), a list of Hilms\Blocks\Looks\LookControl:
| Control | Choices | Written as |
|---|---|---|
| Surface | none, card, well, accent tint | --look-surface, --look-shadow, --look-border |
| Padding | none, the page gutter, the space between blocks, the space between sections | --look-padding |
| Corners | square, the control radius, the box radius | --look-radius |
| Width | narrow, text, page, wide, all the room | --look-width |
| Alignment | start, centre, end | --look-margin-start, --look-margin-end |
Only the box-like blocks take any: the grid cell (surface, padding, corners), the callout (padding and corners — its background is its kind), the question (padding, corners, width, alignment — its options draw status colours on its background), the quote and the accordion (all five), the hero (surface, padding, corners), the image (corners and alignment — its own width setting wins), the code listing (surface, padding, corners), and the embed and the video (corners, width, alignment). A heading and rich text keep the style’s typography and take none. A surface is offered only where ContrastPairs judges the text, the secondary text, the headings and the links drawn on it, and BlockLookTest holds every offered surface to that.
Where a look lives
Section titled “Where a look lives”Under one reserved key of the block’s data:
{"type": "quote", "data": {"text": "…", "look": {"follow": false, "padding": "block", "align": "centre", "variant": "centred"}}}followis the Follow the style switch. It is on until somebody turns it off, and a look without it follows the style: a block written before it had a look, or by a seeder, a pattern or an assistant, holds no switch. That is why the field isFilament\FollowStyleToggleand not Filament’sToggle, which casts a missing value to off before any hook can tell it from one turned off.Look::followsStyle($switch)is the one rule for reading it.- Every other key is optional and only read while the switch is off. Turning it on again hands the look back whole, so no choice is kept behind it.
fields()is a block’ssettings()followed byLookFields— the switch and one optional select per control, after the Variant select when the theme gives the type variants — and is what every builder and the side panel draw;settingKeys()names the look wheneverLookFieldsdraws one, so a refused look is marked on the canvas. Spreadfields(), neversettings()alone, or the look disappears from that place.- A look is validated by the editor’s own schema like every other field (and so by
BlockValidator), kept byBlockCopy, stored as it is bySyncBlocksand left alone by the MCP tools. No control is namedasset,type,dataoruuid, which those read.
Drawing it
Section titled “Drawing it”Looks\Look::of($block, $data) reads the look (an unknown value is the style’s), and $look->attributes() writes one style attribute on the block’s root with a --look-* property per declared control. A control nobody chose is written initial: a custom property is inherited, and a callout inside a padded cell must not take the cell’s padding. The view then reads every property behind a fallback that is what the block drew before it had a look:
<blockquote {{ $look->attributes() }} data-variant="{{ $variant }}" @class([ 'my-block ms-(--look-margin-start,0) me-(--look-margin-end,0) max-w-(--look-width,100%) rounded-(--look-radius,var(--quote-radius)) bg-(--look-surface,transparent) shadow-(--look-shadow,0_0_#0000) ring-1 ring-(--look-border,transparent) …', …])>So a block that follows the style draws exactly as it did, and because a look only ever points at a token, a person’s prefers-contrast promotion still reaches it — the cascade check (npm run cascade) holds a block with a look to that. Two details matter: inside a shadow, “nothing” is 0 0 #0000, never none, which a composed shadow cannot read; and the one style attribute a block view may carry is the look’s — the front-end policy allows style-src-attr, and nothing else in a block view writes one.
The question draws through Livewire on the site, as the quiz does: its look reaches both render paths, because the Livewire view receives the same $look.
Making a block with a look
Section titled “Making a block with a look”bin/artisan hilms:make-block Notice --looks=surface,padding,roundingwrites looks() in the class and the matching var() utilities, with neutral fallbacks, on the view’s root, which always carries $look->attributes(). Asked for the name at the prompt, it offers the controls as a multi-select. It then says, in two lines, that the block draws nothing of its own behind its look yet, and that a default look of its own comes with a token group of its own — the next section.
A block’s own default look
Section titled “A block’s own default look”A scaffolded block falls back to neutral values (transparent, 0), and so does the grid cell, which draws nothing of its own until a look says so. Every other built-in box-like block falls back to its own component tokens — rounded-(--look-radius,var(--quote-radius)) — so the look a block has while it follows the style is a style’s to change, on the Components tab, for every block of its type. To give a new block that:
- A token group in the theme’s
tokens.json, named after the block type and marked"tier": "component": its corners (--notice-radius, an alias to the radius it draws today), its room (--notice-padding, steps), and a surface only if the block draws one by default (--notice-surface). Carry what the look changes and nothing more. - The contract: the group in
Contract\ComponentTokens, with each token’s type and meaning. - Labels:
theme::styles.groups.noticeandtheme::styles.tokens.notice-*, in both languages. - Pairs: every ink the block draws on its own surface — its text, secondary text, headings, links, whatever it draws — in
Contract\ContrastPairs, for both schemes. A pair nothing draws is a false warning in the editor, so tailor the list to the block. - Fallbacks: in the view, each
--look-*property falls back to the block’s own token, never to the foundation the token follows:rounded-(--look-radius,var(--notice-radius)), notvar(--radius-box). - The guard: add the view and its group to
BLOCK_TOKEN_GROUPSinThemeStyleGuardTest. It fails a box-like block that stops drawing a token of its group, and one that falls back to the corner, surface or spacing foundation behind a look, which would take the block’s look out of the Styles editor again. hilms:theme:tokens,npm run build, andnpm run visual: following the style, the block must draw exactly what it drew.
There are no tokens shared by every box-like block (no --block-radius that all follow), which is why hilms:make-block has no --tokens option: a scaffolded block would have nothing to follow. If such base controls ever exist, the option makes sense then.
Options and variants
Section titled “Options and variants”Some choices are markup, and no token reaches them: whether the header puts its logo in the middle, whether a course card stands its cover beside the words, whether a callout is a soft tint or an outline. A theme names the ones it leaves to a style in its manifest:
{ "options": { "header.layout": {"values": ["start", "centred", "split"], "default": "start"}, "course-card.layout": {"values": ["stacked", "horizontal"], "default": "stacked"}, "course-card.category": {"values": ["shown", "hidden"], "default": "shown"}, "footer.layout": {"values": ["centred", "start", "columns"], "default": "centred"} }, "variants": { "callout": {"values": ["soft", "bordered", "bold"], "default": "soft"}, "quote": {"values": ["bar", "centred", "large"], "default": "bar"}, "hero": {"values": ["centred", "start"], "default": "centred"} }}Options are <part>.<choice>; variants are keyed by block type. Values are lowercase words. Options\ThemeOptions::of($chain) merges a chain root first, the way tokens are merged: the root declares each with its values, a child may add values and name another default, and never take a value away. A default the merged values do not hold throws, and hilms:theme:check reports it under Options.
Reading them
Section titled “Reading them”theme()->option('header.layout'); // 'centred' — the style's pick, else the theme's defaulttheme()->variant('callout'); // the style's pick for every callout, else the defaulttheme()->variants('callout'); // ['soft', 'bordered', 'bold']Each answers null for what the theme does not declare and never throws — an error page draws the header too. The style’s picks are read once per request and kept on it, because a listing asks once per card. They are the previewed style’s for the person previewing and the active one’s for everybody else, and in the panel the active one’s, because the block previews are drawn with the active style’s sheet. theme()->force(Choices) is the design catalogue’s door and nothing else’s.
A style’s picks
Section titled “A style’s picks”A style stores its picks in styles.options and styles.variants, only where a pick differs from the theme’s default (Style::choose()), and they travel in the ActiveStyle snapshot beside the hash, so a warm page still costs no query. The editor’s Layout tab has a select per option (“Parts of the page”) and per block type (“Blocks”), labelled by Filament\Support\OptionLabels from theme::styles.options, option_values, variants and variant_values; a value a child theme adds and nobody labelled shows its manifest word. A stored pick the theme no longer offers draws the default. The export writes every option and variant with the value the style draws; the import keeps what the theme offers and names what it passed over.
A block’s variant
Section titled “A block’s variant”A block whose type has variants offers them first in its look: Variant, after the switch, whether or not the block takes any other control. $look->variant() answers the block’s own when its look chose one the theme offers, else the style’s, else the theme’s default, and null for a type without variants. A stored variant the theme does not offer is refused by BlockValidator like any value the select would not take.
Drawing one
Section titled “Drawing one”A view honouring an option or a variant:
- switches on the value with whole literal classes per value — Tailwind finds class names by scanning files, so a class built from parts is never compiled — and treats any value it does not know as the default, since a child theme may add a value without drawing it;
- writes
data-layout(an option) ordata-variant(a variant) on its root; - draws the default exactly as it drew before the option existed —
npm run visualproves it; - keeps the DOM order equal to the visual order, so focus never jumps back across a bar laid out differently, and keeps a header layout to one row of exactly
--header-height; - is declared with
ContractView::honouring(options: [...], variants: [...])in its module’sTheme\ContractViews(every block view honours its own type’s variants by itself).
That declaration is what makes every value tested: ViewRenderer::cases() answers one set of picks per value beside the default, the catalogue lists them under the view, the split export draws each as its own document (<kind>/<slug>.<option>-<value>.<mode>.html, choices in its manifest entry), so npm run a11y and npm run visual see every one, ViewContractTest holds each to toBeAccessibleDocument, and hilms:theme:check reads the shell once per header and footer layout.
Adding one
Section titled “Adding one”- Declare it in
theme.jsonwith its values and default. - Draw it as above, and declare it with
honouring(). - Label it:
theme::styles.options.<name>andoption_values.<name>.<value>(a variant:variants.<type>,variant_values.<type>.<value>), in both languages. - A variant that puts inks on a new surface brings its pairs: the bold callout’s every ink is
--color-surfaceon the kind’s colour, andContrastPairsholds it there. npm run visualandnpm run a11y. The run ofnpm run visuallists each new case as “no baseline” and so does not sayIdentical.; what matters is that nothing else is listed.npm run a11ychecks every value.
hilms:make-block --variants=soft,bold does the block’s half: it declares variants.<type> in the target theme’s manifest (the first value is the default; it refuses fewer than two, a value that is not a lowercase word, one listed twice, and a type that already has variants, before writing anything), gives every view the switch, data-variant and one class string per variant to fill in, and the test a case drawing each one. It names the label keys rather than writing into the module’s language files, and the panel shows the words capitalised meanwhile.
Generators
Section titled “Generators”Writing seventy foundations by hand is a designer’s afternoon. The generators write them from five answers — a brand colour, a tint for the greys, the faces, a shape and a density — and every value they write meets the contrast contract by construction. They all work over the active theme’s tokens and write foundations only: the component tokens follow by alias.
The palette
Section titled “The palette”PaletteGenerator writes the 21 semantic colours, in both schemes, from a brand colour and a NeutralTint (plain, warm — the default theme’s hue 80 at chroma 0.006 — cool, or toward the brand). Each colour starts where the default theme has it: its lightness, and a chroma in proportion to the seed it follows. The accent starts from the brand’s own lightness in light, held between 0.25 and 0.6 — a navy brand stays navy wherever contrast allows — and from the lighter of 0.77 and the brand’s in dark; its hover sits 0.07 darker in light and 0.08 lighter in dark. The status hues are fixed (info 237, success 167, warning 75, danger 30), so a red brand never makes the danger colour ambiguous.
Then the colours are solved in dependency order — the surfaces, the border, the tints, the inks, the strong border, the accent and its hover, the statuses, what is written on the accent, the focus ring — each against everything already chosen that a pair of ContrastPairs draws it with, followed through the aliases of the component tokens. Support\ContrastSolver does one colour: only its lightness moves, away from what it is drawn with, by bisection, to the nearest lightness that meets every ratio plus a margin of 0.1. The hue never moves. The chroma is the most a screen can show at each lightness: Oklch::maxChroma() bisects the chroma alone, so a colour is mapped into sRGB without its hue or lightness shifting. A solved colour is stored with its lightness and hue rounded, and its chroma fitted again under what the rounded colour can show, because rounding alone could push it off the screen.
Two facts found on the way are worth knowing. The default theme’s own accent, oklch(45% 0.15 248), is beyond sRGB — the chroma stops at 0.121 at that lightness and hue — and every browser clips it; the generator writes the mapped colour. And starting every accent at the default’s lightness turned a navy brand light blue, which is why the accent starts from the brand. The HiLMS WCAG seeds give back the theme’s own lightness for every colour, and the suite holds 216 seeds — 24 hues, three chromas, three tints — to every pair in both schemes and every colour on screen.
Type, room, corners and faces
Section titled “Type, room, corners and faces”- Type (
TypeScaleGenerator):--text-xsto--text-4xlfrom a body size (0.875–1.125 rem) and a ratio (1.125–1.333), each step the body size times the ratio to the power of its distance from it, never below 0.75 rem; each keeps the line height the default theme gives it. However steep the scale, a page reflows at 320 px: a heading (wrap-break-wordon<x-ui.heading>) and reading text (overflow-wrap: break-wordin.prose) break a word longer than the line rather than push the page sideways. - Room (
Density: compact, comfortable, airy): the spacing unit at 0.225, 0.25 or 0.28 rem, and the space between blocks, between sections and along the edge in proportion, rounded to a sixteenth of a rem. Every component counting its room in steps follows. - Corners (
Shape: sharp, soft, round): the box, control and badge radii together. - Faces: the body face and, optionally, the headings’ face, from
Styles\FontChoices— the families the theme’s chain builds, the fonts on the Fonts page (Styles) and the system stacks — the same list the editor’s font fields offer.
The one door
Section titled “The one door”StyleGenerator answers each part as the theme’s tokens holding new values, and style(StyleSeeds) all of them. Nothing it answers is written into a style directly: every generated value enters a style through Styles\StyleOverrides::keep(), the door the import uses too. It lays the values over the theme token by token, a colour as a whole pair, and drops a value equal to what the theme holds — colours compared as colours, at the precision of the coarser way of writing them: OKLCH at the editor’s precision (the hue of a grey ignored), and within one step of eight bits a channel when either side is written in sRGB. So a generated style stores only what differs, exactly like one made by hand.
Where they are used
Section titled “Where they are used”- New style from scratch (
StyleFromScratchActionon the Styles list): a five-step wizard — the brand colour, the tint of the greys, the faces, shape and room, a name — whose last step shows the contrast advice of what it would make.Actions\GenerateStylecreates the style, never active, and its editor opens. - Generate… on four sections of the Foundations tab (
GenerateSectionAction): Colours, Text sizes, Spacing and Corners each fill their own fields from their own seeds throughStyleTokens::formValues(), empty a field whose value equals the theme’s, leave every following component token following, and save nothing — the administrator reviews, then saves. It works on the create page too. hilms:make-theme acme --brand="#7a1f5c" --neutral=warm --fonts=serif,sans --shape=soft --density=comfortablegenerates over the new theme’s own chain once its scaffold is on disk and writes what differs into its built-in style,styles/acme.json— never intotokens.json, which stays the theme’s — naming the seeds in the file’s description and reporting the audit’s verdict. A value it cannot read refuses the command before anything is written; when the theme’s name itself is asked at the prompt, it asks for each seed too.
What a theme without a build cannot do
Section titled “What a theme without a build cannot do”A child theme may skip a build of its own ("build": "none"): its tokens.json becomes a plain public/tokens.css laid over its parent’s build, and its Blade overrides its parent’s views. That is enough to restyle a site, and it has three limits worth knowing before choosing it:
- No new utilities. Markup it adds can use only the classes its parent’s build already emits: Tailwind compiled the parent from the parent’s files, and nothing scans the child’s. A class it needs that the parent never used draws nothing.
--standalonegives a child a build of its own, which compiles its parent’s sheet and its own;hilms:make-theme’s closing lines say so. - No proof.
hilms:design:cascadeandnpm run visualread compiled CSS from a build; a build-less child has none of its own, so neither the precedence of its sheet nor “nothing moved” can be proven for it the way it is for a theme with a build. - A child states what it changes. A child changing a colour whose dark value its parent changes must give its own dark value too, and may not turn a token into another kind;
ThemeTokensrefuses such a file, because a light override without its dark would show in dark mode at the child’s weight, and a type change would hand the parent’s utilities a value they cannot read.
A child with a build imports its parent’s tokens as a reference (@import … reference), so its bundle never declares the parent’s variables again — they would pin the parent’s values until the child was rebuilt.
- A look never sets a token, only points at one. Inline custom properties on a block’s root set
--look-*only. A semantic token set there would be declared on the block itself, and a value declared on an element beats whatever it would inherit — the root’s!importantprefers-contrastpromotion included. initial, not nothing, for a control nobody chose: custom properties inherit.- Literal classes per value.
bg-(--look-surface,var(--callout-surface))must be written whole in the view; a string built from parts compiles nothing. - A fallback is the block’s own token, never the foundation it follows, or the guard fails and the block’s look leaves the Styles editor.
json_decodeturns250.0into250, and MySQL reorders JSON keys: compare stored picks and looks withtoEqual, andStyle::choose()leaves a stored pick in another key order alone.- A generated value has one door. Writing a generated token into a style any other way than
StyleOverrides::keep()stores values equal to the theme’s, which then stop following it. - The solver moves lightness only. A colour that cannot meet its pairs at any lightness of its hue and chroma is a seed problem, not a solver problem; the suite’s seed sweep is where such a seed shows up.
HiLMS is MIT-licensed. No replicants were harmed in the writing of these books.