@fusion.dev/helix-docs 0.104.0 → 0.105.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/content/accessibility.mdx +92 -71
  2. package/content/angular.mdx +26 -24
  3. package/content/browser-support.mdx +61 -58
  4. package/content/building-on-the-headless-tier.mdx +100 -74
  5. package/content/charting.mdx +98 -79
  6. package/content/coding-agents.mdx +87 -69
  7. package/content/color-modes.mdx +40 -37
  8. package/content/color.mdx +67 -54
  9. package/content/composition.mdx +85 -83
  10. package/content/elevation-and-material.mdx +32 -29
  11. package/content/events.mdx +77 -65
  12. package/content/forms.mdx +69 -58
  13. package/content/iconography.mdx +75 -71
  14. package/content/installation.mdx +77 -68
  15. package/content/intent-reference.mdx +59 -54
  16. package/content/introduction.mdx +25 -23
  17. package/content/localization.mdx +122 -82
  18. package/content/maps.mdx +222 -0
  19. package/content/metrics.mdx +294 -297
  20. package/content/motion.mdx +44 -36
  21. package/content/other-frameworks.mdx +33 -31
  22. package/content/overlays.mdx +72 -54
  23. package/content/overriding-a-component.mdx +85 -74
  24. package/content/react.mdx +72 -64
  25. package/content/recipes.mdx +64 -47
  26. package/content/routing.mdx +100 -93
  27. package/content/sandbox.mdx +39 -39
  28. package/content/server-rendering.mdx +95 -84
  29. package/content/shape-and-borders.mdx +44 -37
  30. package/content/size-and-density.mdx +51 -46
  31. package/content/space-and-size.mdx +41 -34
  32. package/content/state-and-control.mdx +63 -51
  33. package/content/surfaces-and-separation.mdx +55 -53
  34. package/content/svelte.mdx +20 -19
  35. package/content/theme-builder.mdx +42 -31
  36. package/content/theming.mdx +58 -51
  37. package/content/tiers.mdx +69 -61
  38. package/content/tokens.mdx +72 -53
  39. package/content/typography.mdx +47 -47
  40. package/content/using-helix-with-an-agent.mdx +147 -115
  41. package/content/using-the-elements.mdx +55 -50
  42. package/content/versioning.mdx +42 -35
  43. package/content/visual-axes.mdx +168 -146
  44. package/content/vue.mdx +24 -24
  45. package/content/without-a-bundler.mdx +40 -23
  46. package/dist/pages.generated.js +49 -43
  47. package/dist/sources.generated.js +3337 -2680
  48. package/package.json +3 -3
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: Every surface here is readable by a machine, and where each one is.
2
+ description: The files an AI agent can read to write Helix code, and where to find each one.
3
3
  order: 4
4
4
  section: Getting started
5
5
  title: Coding agents
@@ -7,9 +7,9 @@ title: Coding agents
7
7
 
8
8
  # Coding agents
9
9
 
10
- An agent writing Helix code should not have to read a rendered website. Three
11
- surfaces give it the library as text, all generated from the sources this site
12
- renders from.
10
+ We publish Helix as text for agents that write Helix code, so an agent does not
11
+ need to read the rendered website. Three sources give it the library as text.
12
+ We generate all three from the same sources as this site.
13
13
 
14
14
  ## The manifest
15
15
 
@@ -18,43 +18,54 @@ https://fusion.dev/helix/custom-elements.json
18
18
  @fusion.dev/helix/custom-elements.json
19
19
  ```
20
20
 
21
- A URL and a package path, the same file either way. **Both exist because a
22
- specifier is reachable from a `node_modules` and from nowhere else**, and an
23
- agent that found the library through `llms.txt` has this site and no checkout.
21
+ The URL and the package path serve the same file. A package path works only
22
+ from a `node_modules` folder. An agent that found Helix through `llms.txt` can
23
+ reach this site and has no checkout, so it needs the URL.
24
24
 
25
- Every element, every property with its literal type union, every event with its
26
- class, every slot, every part, every custom property. It is what the framework
27
- typings, the reference documentation and the site's component pages are all
28
- generated from, so it cannot disagree with them.
25
+ The manifest lists:
29
26
 
30
- **A generated manifest is what makes hallucinating a component impossible to
31
- sustain.** The vocabulary is the file: a tag that is not in it does not exist,
32
- and a prop value outside a declared union is a value the component will reject.
27
+ - every element
28
+ - every property, with its literal type union
29
+ - every event, with its class
30
+ - every slot
31
+ - every part
32
+ - every custom property
33
33
 
34
- Literal unions rather than named types are enforced at analysis time for exactly
35
- this reason: `'ghost' | 'outlined' | 'solid' | 'subtle'` tells a reader what to
36
- write, where `ButtonEmphasis` tells them to go and look somewhere else.
34
+ We generate the framework typings, the reference documentation, and the site's
35
+ component pages from the manifest, so they always match it.
37
36
 
38
- ## Markdown for every family
37
+ **If a tag is not in the manifest, it does not exist.** An agent can check every
38
+ tag and value it writes against the file, so an invented component is caught. A
39
+ prop value outside a declared union is invalid.
40
+
41
+ For the same reason, the analyzer requires literal unions for property types.
42
+ `'ghost' | 'outlined' | 'solid' | 'subtle'` shows you what to write. A named
43
+ type such as `ButtonEmphasis` sends you somewhere else to look.
44
+
45
+ ## Markdown and JSON for each component family
39
46
 
40
47
  ```
41
48
  /components/<family>.md
42
49
  /components/<family>.json
43
50
  ```
44
51
 
45
- Static files. The `.md` carries the family's metadata, every element's
46
- description and API tables, and **the examples**, which the HTML page
47
- deliberately does not: that page answers what a component can do and the sandbox
48
- beside it answers what it looks like, and a reference that started collecting
49
- pictures would stop being the one place you can trust to be complete. An agent
50
- has no sandbox to be sent to, and to it an example is text rather than a
51
- picture.
52
+ These are static files. The `.md` file has:
53
+
54
+ - the family's metadata
55
+ - every element's description and API tables
56
+ - the examples
57
+
58
+ The HTML page leaves the examples out on purpose. The page lists what a
59
+ component can do, and the sandbox beside it shows what the component looks
60
+ like. Keeping pictures out of the reference keeps it the one place you can trust
61
+ to be complete. An agent cannot use the sandbox, and it reads an example as
62
+ text, so the `.md` file includes the examples.
52
63
 
53
- The `.json` is the same family as structured data.
64
+ The `.json` file has the same family as structured data.
54
65
 
55
- ## What is enforced
66
+ ## Rules the build checks
56
67
 
57
- These are the difference between code that looks right and code that passes:
68
+ Code that looks right can still fail these checks:
58
69
 
59
70
  | Rule | Check |
60
71
  |---|---|
@@ -63,11 +74,11 @@ These are the difference between code that looks right and code that passes:
63
74
  | A documented CSS hook must be read | `helix component --only hooks` |
64
75
  | A visual property draws from its axis | `helix component`'s `axes` |
65
76
  | A form control styles with `:disabled` | `helix component --only disabled` |
66
- | A component that animates answers reduced motion | `helix component --only motion` |
77
+ | A component that animates supports reduced motion | `helix component --only motion` |
67
78
  | No component reads a primitive token | `helix component`'s `tokens` |
68
79
 
69
- If you are generating Helix code, the first two are the ones that produce
70
- working-looking output that is wrong.
80
+ If you generate Helix code, watch the first two rules. Breaking them produces
81
+ output that looks like it works and is wrong.
71
82
 
72
83
  ## The index
73
84
 
@@ -75,57 +86,64 @@ working-looking output that is wrong.
75
86
  /llms.txt
76
87
  ```
77
88
 
78
- Every page on the site, grouped, each one linked at its `.md` and annotated with
79
- a sentence. Generated from the same route registry the sitemap comes from, so a
80
- page cannot exist without appearing here and cannot appear here without
81
- existing.
89
+ `llms.txt` lists every page on the site, in groups. Each entry links to the
90
+ page's `.md` file and has a one-sentence summary. We generate it from the same
91
+ route registry as the sitemap. Every page appears in it, and every entry in it
92
+ is a page that exists.
82
93
 
83
- Start there rather than crawling: one request, and it names every page on the
84
- site plus the two machine-readable surfaces above. No count here, because a
85
- number typed into prose is a number that goes stale.
94
+ Start with `llms.txt`. In one request, it names every page on the site and the
95
+ two machine-readable sources above. This guide gives no page count, because a
96
+ number typed into prose goes stale.
86
97
 
87
- ## From a component's page
98
+ ## Links on each component page
88
99
 
89
- Every family page carries a menu beside its title with **View Markdown**, **Copy
90
- Markdown**, and links that hand the document's URL to Claude or to Cursor. The
91
- URL rather than the contents, because the URL stays current and a pasted copy
92
- starts going stale the moment it is pasted.
100
+ Each family page has a menu beside its title with:
93
101
 
94
- ## The one thing a document cannot answer
102
+ - **View Markdown**
103
+ - **Copy Markdown**
104
+ - links that send the document's URL to Claude or to Cursor
105
+
106
+ The links send the URL, because the URL stays current. A pasted copy starts to
107
+ go stale as soon as you paste it.
108
+
109
+ ## Compute a theme with `helix mcp`
95
110
 
96
111
  ```sh
97
112
  helix mcp
98
113
  ```
99
114
 
100
- The three surfaces above are closed sets: every tag, every token, every example,
101
- all published and all fetchable. What none of them holds is what *your* brand
102
- color derives to, because the input is a color and the domain is infinite, so the
103
- answer has to be computed when it is asked.
115
+ The three sources above are fixed sets. Every tag, token, and example is
116
+ published, and an agent can fetch it. None of them has the theme for *your*
117
+ brand color. The input is a color, and there are infinitely many colors, so
118
+ Helix has to compute the theme when an agent requests it.
119
+
120
+ `helix mcp` runs an MCP server over stdio with two tools:
121
+
122
+ - `helix_audit_theme` measures every foreground against the background it is
123
+ painted on. It reports each pair with its ratio and target.
124
+ - `helix_derive_theme` returns the CSS.
104
125
 
105
- `helix mcp` serves that over stdio as an MCP server, with two tools:
106
- `helix_audit_theme`, which measures every foreground against the ground it is
107
- really painted on and reports each pair with its ratio and target, and
108
- `helix_derive_theme`, which returns the CSS. Both take the same JSON a
109
- `helix.theme.json` holds.
126
+ Both take the same JSON that a `helix.theme.json` file holds.
110
127
 
111
- **It refuses to hand back a palette that fails its own targets**, the way
112
- `helix theme build` does. Pasting one into a stylesheet is worse than reading a
113
- warning above it, and an agent does not read warnings.
128
+ **`helix mcp` does not return a palette that fails its own targets**, the same
129
+ as `helix theme build`. A failing palette pasted into a stylesheet is worse than
130
+ a warning above it, and an agent does not read warnings.
114
131
 
115
- **You run it and nobody hosts it.** A server we operated would be one more
116
- service standing between you and your own tokens; this is a subcommand of a
117
- binary you installed, talking to a client you chose, with no network in it.
132
+ You run `helix mcp` yourself, and we do not host it. A server we operated would
133
+ be one more service between you and your own tokens. `helix mcp` is a
134
+ subcommand of a binary you installed. It talks to a client you chose and makes
135
+ no network requests.
118
136
 
119
- ## What is not here
137
+ ## What we do not publish for agents
120
138
 
121
- **`llms-full.txt`, or any other whole-library download.** Concatenating every
122
- page into one file is out of date the moment anything changes, where the links
123
- in `llms.txt` are individually cacheable and individually current.
139
+ **`llms-full.txt`, or any other whole-library download.** A single file with
140
+ every page goes out of date as soon as anything changes. Each link in `llms.txt`
141
+ can be cached on its own and stays current on its own.
124
142
 
125
- What such a file would be for is covered better by the two surfaces already
126
- here. `llms.txt` is the index, one request, and each family's `.md` is the
127
- document. Fetch the families you are using.
143
+ The two sources already here cover what such a file would be for. `llms.txt` is
144
+ the index, in one request, and each family's `.md` file is its document. Fetch
145
+ the families you use.
128
146
 
129
- **A search index.** The site has one for humans; there is nothing an agent can
130
- query yet, and `llms.txt` plus the documents it names is a smaller thing to read
131
- than most search APIs are to learn.
147
+ **A search index.** The site has a search index for people. There is nothing an
148
+ agent can query yet. Reading `llms.txt` and the documents it names takes less
149
+ work than learning most search APIs.
@@ -1,15 +1,16 @@
1
1
  ---
2
- description: One attribute, two modes, and no token restated to honor it.
2
+ description: Switch between light and dark mode with one attribute. Each token declares both values once.
3
3
  section: Foundations
4
4
  title: Color modes
5
5
  ---
6
6
 
7
7
  # Color modes
8
8
 
9
- Light and dark are one attribute and no duplicated tokens. There is no second
10
- stylesheet, no `.dark` class to toggle, and nothing to remember to update twice.
9
+ One attribute switches between light and dark mode. Each token declares its
10
+ light and dark values together. You do not need a second stylesheet, a `.dark`
11
+ class, or a second copy of any value.
11
12
 
12
- ## How it works
13
+ ## How color modes work
13
14
 
14
15
  ```css
15
16
  :root {
@@ -18,21 +19,20 @@ stylesheet, no `.dark` class to toggle, and nothing to remember to update twice.
18
19
  }
19
20
  ```
20
21
 
21
- Every token is written once, with both halves in a `light-dark()`. Which half
22
- resolves is decided by `color-scheme` and nothing else, so the mode is a single
23
- inherited property and no token is restated to honor it. That is 156
24
- declarations across the shipped stylesheets, 71 of them in the token layer,
25
- expressing two modes without a second copy of anything.
22
+ Each token is written once, with its light and dark values in `light-dark()`.
23
+ The `color-scheme` property alone decides which value applies. `color-scheme`
24
+ inherits, so one property sets the mode for an element and everything inside
25
+ it. The shipped stylesheets use `light-dark()` in 156 declarations, 71 of them
26
+ in the token layer.
26
27
 
27
- ## Setting the mode
28
+ ## Set the mode
28
29
 
29
30
  ```html
30
31
  <html data-color-mode="dark">
31
32
  ```
32
33
 
33
- `[data-color-mode="light"]` and `[data-color-mode="dark"]` set `color-scheme`,
34
- and `color-scheme` inherits, so the attribute works **on any element** rather
35
- than only on the root:
34
+ `[data-color-mode="light"]` and `[data-color-mode="dark"]` set `color-scheme`.
35
+ Because `color-scheme` inherits, **the attribute works on any element**:
36
36
 
37
37
  ```html
38
38
  <div data-color-mode="light">
@@ -40,24 +40,24 @@ than only on the root:
40
40
  </div>
41
41
  ```
42
42
 
43
- That is the whole mechanism, and it is why the theme builder can show you a
44
- light and a dark rendering of the same component side by side without rendering
45
- two documents.
43
+ The theme builder uses the same attribute to show a component in light and dark
44
+ mode side by side in one document.
46
45
 
47
- ## Following the system
46
+ ## Follow the operating system
48
47
 
49
- Unset, `:root` declares `color-scheme: light dark`, which means the operating
50
- system decides. That is the default and it is usually right.
48
+ When you do not set a mode, `:root` declares `color-scheme: light dark`, and the
49
+ operating system decides the mode. This is the default, and it is usually the
50
+ right choice.
51
51
 
52
- A `prefers-color-scheme` media query still exists in the token layer for one
53
- narrow case rather than being the mechanism: a value that has to be *derived*
54
- from the mode, rather than chosen between two, cannot be expressed as
55
- `light-dark()` and has to ask which mode is current.
52
+ The token layer uses a `prefers-color-scheme` media query in one case.
53
+ `light-dark()` chooses between two values. A value that has to be *computed*
54
+ from the mode cannot use `light-dark()`, so a media query checks which mode is
55
+ current.
56
56
 
57
- ## The one component that inverts
57
+ ## Toast uses the opposite mode
58
58
 
59
- `Toast` renders in the opposite mode to the page it is on, which is why it is
60
- the exception written into the base layer:
59
+ `Toast` renders in the opposite mode to the page it is on. The base layer sets
60
+ this with one rule:
61
61
 
62
62
  ```css
63
63
  helix-toast:not([data-color-mode]) {
@@ -65,14 +65,13 @@ helix-toast:not([data-color-mode]) {
65
65
  }
66
66
  ```
67
67
 
68
- A toast is chrome over the page rather than part of it, and inverting is how it
69
- reads as separate without a border. Opt out by putting `data-color-mode` on it,
70
- which then wins, because the rule only applies to a toast that has not been
71
- told.
68
+ A toast sits over the page. The opposite mode sets it apart from the page
69
+ without a border. To choose a toast's mode yourself, set `data-color-mode` on
70
+ it. The rule applies only to a toast without that attribute.
72
71
 
73
- ## What this means for your own CSS
72
+ ## Colors in your own CSS
74
73
 
75
- Write `light-dark()` for your own colors and set nothing else:
74
+ Write your own colors with `light-dark()`. You do not need anything else:
76
75
 
77
76
  ```css
78
77
  .callout {
@@ -80,8 +79,12 @@ Write `light-dark()` for your own colors and set nothing else:
80
79
  }
81
80
  ```
82
81
 
83
- It will follow the page, follow a subtree that was told otherwise, and follow
84
- the system when nothing has said. Do not write a `prefers-color-scheme` query
85
- for it, and do not key off the `data-color-mode` attribute: both stop working
86
- the moment a subtree overrides the mode, which is the case this design exists to
87
- make cheap.
82
+ The color then follows:
83
+
84
+ - The page's mode
85
+ - The mode of a subtree that sets its own
86
+ - The operating system, when nothing sets a mode
87
+
88
+ Do not write a `prefers-color-scheme` query for it, and do not select on the
89
+ `data-color-mode` attribute. Both stop working when a subtree sets its own mode,
90
+ and this design exists to make that easy.
package/content/color.mdx CHANGED
@@ -1,81 +1,95 @@
1
1
  ---
2
- description: The roles, the ramps, and why the palette is solved rather than picked.
2
+ description: The color roles, why Helix uses OKLCH, and how the engine meets contrast targets.
3
3
  section: Foundations
4
4
  title: Color
5
5
  ---
6
6
 
7
7
  # Color
8
8
 
9
- Seventy-four of the library's 185 tokens are color, which is most of what a
10
- theme is. They are roles rather than values: nothing in Helix is named after
11
- what it looks like, because a name like `--brand-blue-500` stops being true the
12
- moment anybody rethemes.
9
+ Seventy-four of the library's 185 tokens are colors, and they make up most of a
10
+ theme. Each color token is named for its role. No token is named for how it
11
+ looks, because a name like `--brand-blue-500` becomes wrong as soon as you change
12
+ the theme.
13
13
 
14
- ## The roles
14
+ ## Color roles
15
15
 
16
- **Grounds**, which things sit on. `background` is the page itself, and the
17
- surfaces above it are a ladder rather than a set:
16
+ ### Grounds
17
+
18
+ Grounds are the colors that content sits on. `background` is the page itself.
19
+ The surfaces above it form an ordered ladder:
18
20
 
19
21
  [//]: # (tokens:surface-ladder)
20
22
 
21
23
  | Token | Sample | For |
22
24
  | --- | --- | --- |
23
- | `--helix-color-background` | <span data-token-swatch style={{ background: 'var(--helix-color-background)' }} /> | The page. Told apart by nothing, because it is the ground. |
24
- | `--helix-color-surface` | <span data-token-swatch style={{ background: 'var(--helix-color-surface)' }} /> | A plane on the page: a card, or a panel you own. Told apart by a fill, an edge or a lift, whichever the policy leads with. |
25
- | `--helix-color-surface-raised` | <span data-token-swatch style={{ background: 'var(--helix-color-surface-raised)' }} /> | Lifted while still lying on the page: a selected tab, a hovered row. Told apart by its fill, which goes down the ramp rather than up. |
26
- | `--helix-color-surface-overlay` | <span data-token-swatch style={{ background: 'var(--helix-color-surface-overlay)' }} /> | Over the page rather than on it: a menu, a popover, a dialog. Told apart by its shadow and by what it covers. |
27
- | `--helix-color-surface-sunken` | <span data-token-swatch style={{ background: 'var(--helix-color-surface-sunken)' }} /> | A well a value goes into: an input, a track, a groove. Told apart by its fill, inward. |
25
+ | `--helix-color-background` | <span data-token-swatch style={{ background: 'var(--helix-color-background)' }} /> | The page background. Every other surface sits on it. |
26
+ | `--helix-color-surface` | <span data-token-swatch style={{ background: 'var(--helix-color-surface)' }} /> | A surface on the page, such as a card or a panel. The separation policy sets it apart with a fill, a border, or a shadow. |
27
+ | `--helix-color-surface-raised` | <span data-token-swatch style={{ background: 'var(--helix-color-surface-raised)' }} /> | A raised surface on the page, such as a selected tab or a hovered row. Its fill is one step down the ramp. |
28
+ | `--helix-color-surface-overlay` | <span data-token-swatch style={{ background: 'var(--helix-color-surface-overlay)' }} /> | A surface over the page, such as a menu, a popover, or a dialog. Its shadow sets it apart. |
29
+ | `--helix-color-surface-sunken` | <span data-token-swatch style={{ background: 'var(--helix-color-surface-sunken)' }} /> | A recessed surface that holds a value, such as an input, a track, or a groove. Its fill sets it apart. |
28
30
 
29
31
  [//]: # (/tokens)
30
32
 
31
- Which one to reach for is a question about the geometry rather than about the
32
- shade, and it is the thing people most often get wrong. See [Surfaces and
33
+ Choose a surface by where the element sits relative to the page. This is the
34
+ choice people most often get wrong. See [Surfaces and
33
35
  separation](./surfaces-and-separation).
34
36
 
35
- **Inks**, which go on grounds. `foreground`, then `foreground-muted` and
36
- `foreground-subtle` for text that recedes, and `foreground-on-accent` for text
37
- that sits on a filled accent rather than on a surface.
37
+ ### Inks
38
+
39
+ Inks are the colors that go on grounds. The base ink is `foreground`.
40
+
41
+ - `foreground-muted` and `foreground-subtle` are for text that recedes.
42
+ - `foreground-on-accent` is for text on a filled accent color.
43
+
44
+ ### Intent
45
+
46
+ Intent colors show what something means.
47
+
48
+ - `primary` and `accent` are for emphasis. Each has `-hover`, `-active`, and
49
+ `-subtle` variants.
50
+ - `info`, `success`, `warning`, and `danger` are for status. Each has a
51
+ `-surface` and a `-surface-hover` variant for the tinted box that holds the
52
+ message.
53
+
54
+ ### Series
55
+
56
+ Helix has twenty-four series colors for charts. They are ordered, so a chart
57
+ with three lines uses the first three. We chose the colors so that the first
58
+ ones in the list stay easy to tell apart, however many a chart uses.
59
+
60
+ ### Syntax
38
61
 
39
- **Intent**, which says what something means. `primary` and `accent` for
40
- emphasis, each with `-hover`, `-active` and `-subtle`; `info`, `success`,
41
- `warning` and `danger` for status, each with a `-surface` and a `-surface-hover`
42
- for the tinted box the message sits in.
62
+ The eight syntax roles for code are `callable`, `comment`, `constant`,
63
+ `invalid`, `keyword`, `name`, `string`, and `tag`. A full editor theme has more
64
+ roles. Helix has fewer because code in documentation only needs to be read.
43
65
 
44
- **Series**, twenty-four of them, for charts. Ordered rather than arbitrary: a
45
- chart with three lines uses the first three, and they are chosen to stay
46
- distinguishable in that order rather than to look good as a full set.
66
+ ## Why colors use OKLCH
47
67
 
48
- **Syntax**, eight roles for code: `callable`, `comment`, `constant`, `invalid`,
49
- `keyword`, `name`, `string`, `tag`. Deliberately fewer than a full editor theme,
50
- because a code block in documentation is read rather than edited.
68
+ Every Helix color is written in OKLCH. In sRGB, equal numeric steps do not look
69
+ equal, which makes ramps and contrast math unreliable. A ramp with even numeric
70
+ steps looks uneven. A contrast solver that works in sRGB cannot converge on a
71
+ target that it can measure.
51
72
 
52
- ## OKLCH
73
+ OKLCH lets the theme engine *solve* for a palette from your brand color. See
74
+ [Deriving a theme](./theming).
53
75
 
54
- Every color is authored in OKLCH. Equal numeric steps in sRGB are not equal
55
- perceptual steps, which makes both ramp generation and contrast arithmetic
56
- unreliable: a ramp that is even by the numbers looks uneven, and a contrast
57
- solver working in the wrong space cannot converge on a target it can measure.
76
+ ## How the engine meets contrast targets
58
77
 
59
- Concretely, this is what lets the theme engine take a brand color and *solve*
60
- for a palette rather than have someone pick one. See [Deriving a
61
- theme](./theming).
78
+ The engine measures each ink against the ground it will be painted on. It then
79
+ searches for the lightness that meets the target. Every pair it produces meets
80
+ the target.
62
81
 
63
- ## Contrast is a guarantee
82
+ The engine can solve only the pairs it knows about, so we check two things:
64
83
 
65
- The engine measures every ink against the ground it will really be painted on
66
- and searches for the lightness that meets the target, rather than proposing a
67
- value and checking it afterwards. Every pairing it produces meets the target by
68
- construction.
84
+ - We compare the engine's surface model with what the components paint.
85
+ - We measure every pair on a rendered page.
69
86
 
70
- That guarantee is only as good as the list of pairs somebody thought of, so two
71
- things are checked rather than one. The engine's declared surface model is held
72
- against what components actually paint, and then every pairing is measured on a
73
- laid-out page, which is the only way to see the ones no stylesheet states: just
74
- eleven rules in the library set an ink and a ground together, where 165 set only
75
- an ink and 112 only a ground, and a pairing the cascade assembled is still a
76
- pairing a reader has to be able to read.
87
+ The second check finds pairs that no single CSS rule states. Only eleven rules
88
+ in the library set an ink and a ground together. Another 165 set only an ink,
89
+ and 112 set only a ground. The cascade combines them into the rest of the
90
+ pairs, and each of those pairs must still be readable.
77
91
 
78
- ## Do not reach past the semantic layer
92
+ ## Use only semantic tokens
79
93
 
80
94
  ```css
81
95
  /* Yours to read. */
@@ -85,8 +99,7 @@ color: var(--helix-color-foreground-muted);
85
99
  color: var(--_helix-neutral-500);
86
100
  ```
87
101
 
88
- The primitive layer underneath, `--_helix-neutral-500` and its kind, is
89
- `helix/styles`'s own business. The underscore marks it private and the
90
- indirection is the whole point: it is what lets a theme change what
91
- `--helix-color-primary` resolves to without every component knowing which step
92
- of which ramp it happened to be.
102
+ Primitive tokens such as `--_helix-neutral-500` belong to `helix/styles`. The
103
+ underscore marks them as private. Components use only semantic tokens, such as
104
+ `--helix-color-primary`. A theme can change what a semantic token resolves to,
105
+ and no component depends on which step of which ramp a color comes from.