Icons: place icons with custom properties, not inline SVG
Assisted-by: Claude Opus 5.5 via Claude Code
change olzppuzmkvurxqyvwluplsvksvmzmoyz
commit b9484458e0c83d957f1265c8ec54ffaaace4dea1
author Alpha Chen <alpha@kejadlen.dev>
date
parent lxtourpu
diff --git a/README.md b/README.md
index c1b757b..aa17b82 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
 # Gloss
 
 The design system for Arbitrary Definitions, a personal umbrella of
-self-hosted, single-user tools. Sixteen components and five foundations,
+self-hosted, single-user tools. Sixteen components and six foundations,
 documented by a small Ruby/ERB site (no Jekyll) with hand-maintained CSS and
 hand-written content.
 
diff --git a/SKILL.md b/SKILL.md
index 932af80..0b81e61 100644
--- a/SKILL.md
+++ b/SKILL.md
@@ -55,6 +55,10 @@ guessing from a similar one or from memory:
   regardless, so using them as selectors invents nothing.
 - `data-*` attributes for variants and states: `data-variant="primary"`,
   `data-size="sm"`, `data-tone="danger"`, `data-elevated`.
+- Icons are custom properties, never `<svg>` markup: `--gl-icon:
+  var(--gl-icon-trash)` on a Button, IconButton, or Avatar, or
+  `--gl-icon-start`/`--gl-icon-end` on anything else. See
+  `_foundations/icons.md`.
 - Exactly seven plain, unmodified classes exist, each because there is no
   element or attribute to hang a selector on: `.btn` (an `<a>` acting as a
   button — a bare `a` selector would catch every prose link on the page),
diff --git a/_components/button.md b/_components/button.md
index fa30668..20619ba 100644
--- a/_components/button.md
+++ b/_components/button.md
@@ -92,20 +92,19 @@ disabled primary still reads as the primary.
 
 ## With an icon
 
-Pass an icon as the first child. The gap is `--gl-space-2xs`, set by the
-button itself.
+Set `--gl-icon` to one of the [icon tokens](<%= relative_url('/foundations/icons/') %>).
+The button draws it before the label, at the label's size and color, with
+the button's own `--gl-space-2xs` gap.
 
 <figure class="example">
 <figcaption>Icon</figcaption>
 <div>
-<button data-variant="primary">
-  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 5v14M5 12h14" /></svg>
+<button data-variant="primary" style="--gl-icon: var(--gl-icon-plus)">
   New
 </button>
 </div>
 <details><summary>Markup</summary>
-<pre><code>&lt;button data-variant=&quot;primary&quot;&gt;
-  &lt;svg width=&quot;14&quot; height=&quot;14&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.9&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; aria-hidden=&quot;true&quot;&gt;&lt;path d=&quot;M12 5v14M5 12h14&quot; /&gt;&lt;/svg&gt;
+<pre><code>&lt;button data-variant=&quot;primary&quot; style=&quot;--gl-icon: var(--gl-icon-plus)&quot;&gt;
   New
 &lt;/button&gt;</code></pre>
 </details>
@@ -174,6 +173,7 @@ class Button needs, and only anchors reach for it.
     <tr><td><code>data-variant</code></td><td><code>primary | dark | ghost | danger</code></td><td>Omit for the default, quiet look — no fill at rest.</td></tr>
     <tr><td><code>data-size</code></td><td><code>sm | lg</code></td><td>Omit for the default (medium) size. Padding, plus one step of font size at each end.</td></tr>
     <tr><td><code>data-block</code></td><td>boolean</td><td>Full width of its container.</td></tr>
+    <tr><td><code>--gl-icon</code></td><td>an icon token</td><td>Draws the icon before the label. See <a href="<%= relative_url('/foundations/icons/') %>">Icons</a>.</td></tr>
     <tr><td><code>disabled</code></td><td>boolean</td><td>Removes the button from the tab order.</td></tr>
     <tr><td><code>aria-disabled</code></td><td><code>"true"</code></td><td>Looks disabled, stays focusable.</td></tr>
     <tr><td><code>[role="group"]</code></td><td></td><td>Wrap adjacent buttons in a group to join their radii; a hairline divider separates them, since the buttons themselves carry no border.</td></tr>
diff --git a/_components/card-row.md b/_components/card-row.md
index e4bfe7d..e777e31 100644
--- a/_components/card-row.md
+++ b/_components/card-row.md
@@ -63,9 +63,7 @@ patterns for it in context).
 <div data-surface="sunken">
 <ul class="card" style="max-width: 24rem;">
   <li>
-    <span class="avatar" data-square>
-      <svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 8 12 3 3 8v8l9 5 9-5V8Z" /><path d="M3 8l9 5 9-5M12 13v8" /></svg>
-    </span>
+    <span class="avatar" data-square style="--gl-icon: var(--gl-icon-package)"></span>
     <div style="flex:1; min-width:0;">
       <div style="font-weight: 550;">Dishwasher manual</div>
       <div class="type-label" style="margin-top: 2px;">PDF · 2.1MB</div>
@@ -77,9 +75,7 @@ patterns for it in context).
 <details><summary>Markup</summary>
 <pre><code>&lt;ul class=&quot;card&quot; style=&quot;max-width: 24rem;&quot;&gt;
   &lt;li&gt;
-    &lt;span class=&quot;avatar&quot; data-square&gt;
-      &lt;svg width=&quot;17&quot; height=&quot;17&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.75&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; aria-hidden=&quot;true&quot;&gt;&lt;path d=&quot;M21 8 12 3 3 8v8l9 5 9-5V8Z&quot; /&gt;&lt;path d=&quot;M3 8l9 5 9-5M12 13v8&quot; /&gt;&lt;/svg&gt;
-    &lt;/span&gt;
+    &lt;span class=&quot;avatar&quot; data-square style=&quot;--gl-icon: var(--gl-icon-package)&quot;&gt;&lt;/span&gt;
     &lt;div style=&quot;flex:1; min-width:0;&quot;&gt;
       &lt;div style=&quot;font-weight: 550;&quot;&gt;Dishwasher manual&lt;/div&gt;
       &lt;div class=&quot;type-label&quot; style=&quot;margin-top: 2px;&quot;&gt;PDF · 2.1MB&lt;/div&gt;
diff --git a/_components/icon-button.md b/_components/icon-button.md
index 83fd99c..ee51585 100644
--- a/_components/icon-button.md
+++ b/_components/icon-button.md
@@ -16,31 +16,21 @@ pattern this system does not otherwise reach for.
 A square icon-only button is still a `<button>`, but there is no attribute
 that tells it apart from a plain [Button](<%= relative_url('/components/button/') %>)
 sitting alone — `variant`/`size` mean different things for each — so
-`.icon-button` is the one class this component needs.
+`.icon-button` is the one class this component needs. The icon is
+`--gl-icon`, one of the [icon tokens](<%= relative_url('/foundations/icons/') %>),
+sized to the label type a Button of the same size would carry.
 
 <figure class="example">
 <figcaption>Variants</figcaption>
 <div>
-<button class="icon-button" aria-label="Delete">
-  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16M9 7V4h6v3M6 7l1 13h10l1-13" /></svg>
-</button>
-<button class="icon-button" data-variant="outline" aria-label="Settings">
-  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="3" /><path d="M12 2.5v3M12 18.5v3M3.6 7.2l2.6 1.5M17.8 15.3l2.6 1.5M3.6 16.8l2.6-1.5M17.8 8.7l2.6-1.5" /></svg>
-</button>
-<button class="icon-button" data-variant="accent" aria-label="Filter (active)">
-  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 5h16l-6 8v5l-4 2v-7z" /></svg>
-</button>
+<button class="icon-button" aria-label="Delete" style="--gl-icon: var(--gl-icon-trash)"></button>
+<button class="icon-button" data-variant="outline" aria-label="Settings" style="--gl-icon: var(--gl-icon-settings)"></button>
+<button class="icon-button" data-variant="accent" aria-label="Filter (active)" style="--gl-icon: var(--gl-icon-filter)"></button>
 </div>
 <details open><summary>Markup</summary>
-<pre><code>&lt;button class=&quot;icon-button&quot; aria-label=&quot;Delete&quot;&gt;
-  &lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.75&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; aria-hidden=&quot;true&quot;&gt;&lt;path d=&quot;M4 7h16M9 7V4h6v3M6 7l1 13h10l1-13&quot; /&gt;&lt;/svg&gt;
-&lt;/button&gt;
-&lt;button class=&quot;icon-button&quot; data-variant=&quot;outline&quot; aria-label=&quot;Settings&quot;&gt;
-  &lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.75&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; aria-hidden=&quot;true&quot;&gt;&lt;circle cx=&quot;12&quot; cy=&quot;12&quot; r=&quot;3&quot; /&gt;&lt;path d=&quot;M12 2.5v3M12 18.5v3M3.6 7.2l2.6 1.5M17.8 15.3l2.6 1.5M3.6 16.8l2.6-1.5M17.8 8.7l2.6-1.5&quot; /&gt;&lt;/svg&gt;
-&lt;/button&gt;
-&lt;button class=&quot;icon-button&quot; data-variant=&quot;accent&quot; aria-label=&quot;Filter (active)&quot;&gt;
-  &lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.75&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; aria-hidden=&quot;true&quot;&gt;&lt;path d=&quot;M4 5h16l-6 8v5l-4 2v-7z&quot; /&gt;&lt;/svg&gt;
-&lt;/button&gt;</code></pre>
+<pre><code>&lt;button class=&quot;icon-button&quot; aria-label=&quot;Delete&quot; style=&quot;--gl-icon: var(--gl-icon-trash)&quot;&gt;&lt;/button&gt;
+&lt;button class=&quot;icon-button&quot; data-variant=&quot;outline&quot; aria-label=&quot;Settings&quot; style=&quot;--gl-icon: var(--gl-icon-settings)&quot;&gt;&lt;/button&gt;
+&lt;button class=&quot;icon-button&quot; data-variant=&quot;accent&quot; aria-label=&quot;Filter (active)&quot; style=&quot;--gl-icon: var(--gl-icon-filter)&quot;&gt;&lt;/button&gt;</code></pre>
 </details>
 </figure>
 
@@ -56,14 +46,14 @@ is ever retuned.
 <figure class="example">
 <figcaption>Sizes</figcaption>
 <div>
-<button class="icon-button" data-variant="outline" data-size="sm" aria-label="Back"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 18l-6-6 6-6" /></svg></button>
-<button class="icon-button" data-variant="outline" aria-label="Back"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 18l-6-6 6-6" /></svg></button>
-<button class="icon-button" data-variant="outline" data-size="lg" aria-label="Back"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 18l-6-6 6-6" /></svg></button>
+<button class="icon-button" data-variant="outline" data-size="sm" aria-label="Back" style="--gl-icon: var(--gl-icon-chevron-left)"></button>
+<button class="icon-button" data-variant="outline" aria-label="Back" style="--gl-icon: var(--gl-icon-chevron-left)"></button>
+<button class="icon-button" data-variant="outline" data-size="lg" aria-label="Back" style="--gl-icon: var(--gl-icon-chevron-left)"></button>
 </div>
 <details><summary>Markup</summary>
-<pre><code>&lt;button class=&quot;icon-button&quot; data-variant=&quot;outline&quot; data-size=&quot;sm&quot; aria-label=&quot;Back&quot;&gt;&lt;svg width=&quot;14&quot; height=&quot;14&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.9&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; aria-hidden=&quot;true&quot;&gt;&lt;path d=&quot;M15 18l-6-6 6-6&quot; /&gt;&lt;/svg&gt;&lt;/button&gt;
-&lt;button class=&quot;icon-button&quot; data-variant=&quot;outline&quot; aria-label=&quot;Back&quot;&gt;&lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.9&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; aria-hidden=&quot;true&quot;&gt;&lt;path d=&quot;M15 18l-6-6 6-6&quot; /&gt;&lt;/svg&gt;&lt;/button&gt;
-&lt;button class=&quot;icon-button&quot; data-variant=&quot;outline&quot; data-size=&quot;lg&quot; aria-label=&quot;Back&quot;&gt;&lt;svg width=&quot;18&quot; height=&quot;18&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.9&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; aria-hidden=&quot;true&quot;&gt;&lt;path d=&quot;M15 18l-6-6 6-6&quot; /&gt;&lt;/svg&gt;&lt;/button&gt;</code></pre>
+<pre><code>&lt;button class=&quot;icon-button&quot; data-variant=&quot;outline&quot; data-size=&quot;sm&quot; aria-label=&quot;Back&quot; style=&quot;--gl-icon: var(--gl-icon-chevron-left)&quot;&gt;&lt;/button&gt;
+&lt;button class=&quot;icon-button&quot; data-variant=&quot;outline&quot; aria-label=&quot;Back&quot; style=&quot;--gl-icon: var(--gl-icon-chevron-left)&quot;&gt;&lt;/button&gt;
+&lt;button class=&quot;icon-button&quot; data-variant=&quot;outline&quot; data-size=&quot;lg&quot; aria-label=&quot;Back&quot; style=&quot;--gl-icon: var(--gl-icon-chevron-left)&quot;&gt;&lt;/button&gt;</code></pre>
 </details>
 </figure>
 
@@ -75,17 +65,13 @@ An icon-only control almost always wants a [Tooltip](<%= relative_url('/componen
 <figcaption>With a tooltip</figcaption>
 <div>
 <span>
-  <button class="icon-button" aria-label="Delete" aria-describedby="icon-btn-tip">
-    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16M9 7V4h6v3M6 7l1 13h10l1-13" /></svg>
-  </button>
+  <button class="icon-button" aria-label="Delete" aria-describedby="icon-btn-tip" style="--gl-icon: var(--gl-icon-trash)"></button>
   <span role="tooltip" id="icon-btn-tip">Delete</span>
 </span>
 </div>
 <details><summary>Markup</summary>
 <pre><code>&lt;span&gt;
-  &lt;button class=&quot;icon-button&quot; aria-label=&quot;Delete&quot; aria-describedby=&quot;icon-btn-tip&quot;&gt;
-    &lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.75&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; aria-hidden=&quot;true&quot;&gt;&lt;path d=&quot;M4 7h16M9 7V4h6v3M6 7l1 13h10l1-13&quot; /&gt;&lt;/svg&gt;
-  &lt;/button&gt;
+  &lt;button class=&quot;icon-button&quot; aria-label=&quot;Delete&quot; aria-describedby=&quot;icon-btn-tip&quot; style=&quot;--gl-icon: var(--gl-icon-trash)&quot;&gt;&lt;/button&gt;
   &lt;span role=&quot;tooltip&quot; id=&quot;icon-btn-tip&quot;&gt;Delete&lt;/span&gt;
 &lt;/span&gt;</code></pre>
 </details>
@@ -98,6 +84,7 @@ An icon-only control almost always wants a [Tooltip](<%= relative_url('/componen
   <tbody>
     <tr><td><code>button.icon-button</code></td><td></td><td>Required base — the one class in this component, since nothing distinguishes it from Button structurally.</td></tr>
     <tr><td><code>aria-label</code></td><td>string</td><td>Required — becomes the accessible name and the native tooltip.</td></tr>
+    <tr><td><code>--gl-icon</code></td><td>an icon token</td><td>Required — the icon. See <a href="<%= relative_url('/foundations/icons/') %>">Icons</a>.</td></tr>
     <tr><td><code>data-size</code></td><td><code>sm | lg</code></td><td>Square, matching the Button of the same size: 25px small, 39–43px large. Omit for the default, 35–38px.</td></tr>
     <tr><td><code>data-variant</code></td><td><code>outline | accent</code></td><td>Omit for the default, transparent ghost look.</td></tr>
   </tbody>
diff --git a/_components/tooltip.md b/_components/tooltip.md
index f8d9f16..2a2e957 100644
--- a/_components/tooltip.md
+++ b/_components/tooltip.md
@@ -20,17 +20,13 @@ Hover or tab to the button below.
 <figcaption>Tooltip</figcaption>
 <div>
 <span>
-  <button class="icon-button" aria-label="Delete" aria-describedby="tooltip-basic">
-    <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 7h16M9 7V4h6v3M6 7l1 13h10l1-13" /></svg>
-  </button>
+  <button class="icon-button" aria-label="Delete" aria-describedby="tooltip-basic" style="--gl-icon: var(--gl-icon-trash)"></button>
   <span role="tooltip" id="tooltip-basic">Delete</span>
 </span>
 </div>
 <details open><summary>Markup</summary>
 <pre><code>&lt;span&gt;
-  &lt;button class=&quot;icon-button&quot; aria-label=&quot;Delete&quot; aria-describedby=&quot;tooltip-basic&quot;&gt;
-    &lt;svg width=&quot;16&quot; height=&quot;16&quot; viewBox=&quot;0 0 24 24&quot; fill=&quot;none&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.75&quot; stroke-linecap=&quot;round&quot; stroke-linejoin=&quot;round&quot; aria-hidden=&quot;true&quot;&gt;&lt;path d=&quot;M4 7h16M9 7V4h6v3M6 7l1 13h10l1-13&quot; /&gt;&lt;/svg&gt;
-  &lt;/button&gt;
+  &lt;button class=&quot;icon-button&quot; aria-label=&quot;Delete&quot; aria-describedby=&quot;tooltip-basic&quot; style=&quot;--gl-icon: var(--gl-icon-trash)&quot;&gt;&lt;/button&gt;
   &lt;span role=&quot;tooltip&quot; id=&quot;tooltip-basic&quot;&gt;Delete&lt;/span&gt;
 &lt;/span&gt;</code></pre>
 </details>
diff --git a/_foundations/icons.md b/_foundations/icons.md
new file mode 100644
index 0000000..d9caf47
--- /dev/null
+++ b/_foundations/icons.md
@@ -0,0 +1,83 @@
+---
+title: Icons
+summary: >-
+  Icons are tokens, like colors. A component gets one by setting a custom
+  property, never by carrying an &lt;svg&gt; in its markup, and the icon
+  takes the text's color and size.
+---
+
+## Placing an icon
+
+Set `--gl-icon` on a Button, IconButton, or Avatar. Each one decides where
+its icon goes, so the markup only names the icon:
+
+```html
+<button style="--gl-icon: var(--gl-icon-plus)">New</button>
+```
+
+The property can come from a stylesheet as easily as an inline style, and
+it reacts to anything a selector can:
+
+```css
+button[data-action="delete"] { --gl-icon: var(--gl-icon-trash); }
+:root[data-theme="dark"] [data-theme-toggle] { --gl-icon: var(--gl-icon-moon); }
+```
+
+Any other element takes `--gl-icon-start`, drawn in its `::before`, or
+`--gl-icon-end`, drawn in its `::after`. An icon replaces whatever that
+pseudo-element would otherwise draw.
+
+None of the three properties inherit: an icon set on a `<p>` does not
+repeat on every link inside it.
+
+## Tokens
+
+Every icon is a monochrome SVG on a 24-unit grid with rounded strokes. It
+is painted as a mask, so the stroke color written into the token never
+shows.
+
+<table>
+  <thead><tr><th scope="col">Token</th><th scope="col">Icon</th><th scope="col">Use for</th></tr></thead>
+  <tbody>
+    <tr><td><code>--gl-icon-camera</code></td><td><span style="--gl-icon-start: var(--gl-icon-camera)"></span></td><td>Capturing a photo.</td></tr>
+    <tr><td><code>--gl-icon-chevron-left</code></td><td><span style="--gl-icon-start: var(--gl-icon-chevron-left)"></span></td><td>Back, in a header.</td></tr>
+    <tr><td><code>--gl-icon-chevron-down</code></td><td><span style="--gl-icon-start: var(--gl-icon-chevron-down)"></span></td><td>Select's own chevron. It is the one icon Select paints as a background, so its stroke carries <code>--gl-color-text-secondary</code> and is redefined for the dark theme.</td></tr>
+    <tr><td><code>--gl-icon-filter</code></td><td><span style="--gl-icon-start: var(--gl-icon-filter)"></span></td><td>Narrowing a list.</td></tr>
+    <tr><td><code>--gl-icon-moon</code></td><td><span style="--gl-icon-start: var(--gl-icon-moon)"></span></td><td>The dark theme, in a theme toggle.</td></tr>
+    <tr><td><code>--gl-icon-package</code></td><td><span style="--gl-icon-start: var(--gl-icon-package)"></span></td><td>A stored file or item, in an Avatar.</td></tr>
+    <tr><td><code>--gl-icon-plus</code></td><td><span style="--gl-icon-start: var(--gl-icon-plus)"></span></td><td>Creating something new.</td></tr>
+    <tr><td><code>--gl-icon-settings</code></td><td><span style="--gl-icon-start: var(--gl-icon-settings)"></span></td><td>Opening settings.</td></tr>
+    <tr><td><code>--gl-icon-sun</code></td><td><span style="--gl-icon-start: var(--gl-icon-sun)"></span></td><td>The light theme, in a theme toggle.</td></tr>
+    <tr><td><code>--gl-icon-trash</code></td><td><span style="--gl-icon-start: var(--gl-icon-trash)"></span></td><td>Deleting.</td></tr>
+  </tbody>
+</table>
+
+A new icon is one more line in `tokens.css`: a `url()` holding an SVG,
+with `<`, `>`, and `#` percent-encoded.
+
+## Styling
+
+These properties do inherit, so setting one on a container styles every
+icon inside it:
+
+<table>
+  <thead><tr><th scope="col">Property</th><th scope="col">Default</th><th scope="col">Effect</th></tr></thead>
+  <tbody>
+    <tr><td><code>--gl-icon-style-size</code></td><td><code>1em</code></td><td>Width and height.</td></tr>
+    <tr><td><code>--gl-icon-style-color</code></td><td><code>currentColor</code></td><td>Fill. Point it at a color token, never a literal.</td></tr>
+    <tr><td><code>--gl-icon-style-spacing</code></td><td><code>0.3em</code></td><td>Gap between the icon and the text beside it. Button, IconButton, and Avatar set it to zero and space with their own layout.</td></tr>
+    <tr><td><code>--gl-icon-style-voffset</code></td><td><code>-0.02em</code></td><td>Optical nudge. The icon is centered on the cap height, and a positive value moves it up.</td></tr>
+  </tbody>
+</table>
+
+## Limits
+
+- An icon has one color. A multicolor mark stays inline SVG.
+- An icon can't have its own background or border. Put those on the
+  element and the icon inside it, as Avatar does.
+- An icon can't animate its paths. It can change on any state a selector
+  can reach, but it swaps, not morphs.
+
+The approach, and the WebKit workarounds in `components/icon.css`, come
+from Lea Verou's [The best way to use icons on the Web](https://lea.verou.me/blog/2026/icons/)
+and her `@nudeui/icons` package.
diff --git a/_layouts/page.erb b/_layouts/page.erb
index 48a91fc..922de2d 100644
--- a/_layouts/page.erb
+++ b/_layouts/page.erb
@@ -64,14 +64,6 @@
       <div class="gl-doc-header__actions">
         <button data-variant="ghost" data-size="sm" data-theme-toggle
                 aria-label="Switch color theme" title="Switch color theme">
-          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" aria-hidden="true" data-theme-icon="light">
-            <circle cx="12" cy="12" r="4.5" stroke="currentColor" stroke-width="2"/>
-            <path d="M12 1.8v2.4M12 19.8v2.4M4.6 4.6l1.7 1.7M17.7 17.7l1.7 1.7M1.8 12h2.4M19.8 12h2.4M4.6 19.4l1.7-1.7M17.7 6.3l1.7-1.7"
-                  stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
-          </svg>
-          <svg width="16" height="16" viewBox="0 0 24 24" fill="none" aria-hidden="true" data-theme-icon="dark" hidden>
-            <path d="M20 14.2A8.4 8.4 0 0 1 9.8 4a8.4 8.4 0 1 0 10.2 10.2Z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/>
-          </svg>
           <span class="gl-visually-hidden" data-theme-label>Theme</span>
         </button>
         <a class="btn" data-size="sm" href="https://github.com/<%= REPOSITORY %>">Source</a>
diff --git a/_patterns/archive.md b/_patterns/archive.md
index b74b11c..5e5d72a 100644
--- a/_patterns/archive.md
+++ b/_patterns/archive.md
@@ -25,7 +25,7 @@ Three screens, abbreviated to what each one adds.
     </div>
     <ul class="card">
       <li>
-        <span class="avatar" data-square><svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 8 12 3 3 8v8l9 5 9-5V8Z" /><path d="M3 8l9 5 9-5M12 13v8" /></svg></span>
+        <span class="avatar" data-square style="--gl-icon: var(--gl-icon-package)"></span>
         <div class="pattern-row">
           <div class="pattern-file-title pattern-file-title--truncate">Dishwasher manual</div>
           <div class="type-label pattern-file-meta">PDF · 2.1MB</div>
@@ -33,7 +33,7 @@ Three screens, abbreviated to what each one adds.
         <span class="type-mono pattern-row__meta">2h</span>
       </li>
       <li>
-        <span class="avatar" data-square><svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 8 12 3 3 8v8l9 5 9-5V8Z" /><path d="M3 8l9 5 9-5M12 13v8" /></svg></span>
+        <span class="avatar" data-square style="--gl-icon: var(--gl-icon-package)"></span>
         <div class="pattern-row">
           <div class="pattern-file-title">Sofa receipt</div>
           <div class="type-label pattern-file-meta">JPG · 640KB</div>
@@ -41,7 +41,7 @@ Three screens, abbreviated to what each one adds.
         <span class="type-mono pattern-row__meta">1d</span>
       </li>
       <li>
-        <span class="avatar" data-square><svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 8 12 3 3 8v8l9 5 9-5V8Z" /><path d="M3 8l9 5 9-5M12 13v8" /></svg></span>
+        <span class="avatar" data-square style="--gl-icon: var(--gl-icon-package)"></span>
         <div class="pattern-row">
           <div class="pattern-file-title">Passport scan</div>
           <div class="type-label pattern-file-meta">PDF · 890KB</div>
@@ -51,20 +51,18 @@ Three screens, abbreviated to what each one adds.
     </ul>
   </div>
   <div class="pattern-dock">
-    <button data-variant="primary" data-size="lg" data-block>
-      <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 8a2 2 0 0 1 2-2h1.5l1-1.5h9l1 1.5H20a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8Z" /><circle cx="12" cy="13" r="3.5" /></svg>
+    <button data-variant="primary" data-size="lg" data-block style="--gl-icon: var(--gl-icon-camera)">
       Take a photo
     </button>
   </div>
 </div>
 <div class="pattern-phone">
   <header class="pattern-phone__header">
-    <button class="icon-button" data-size="sm" aria-label="Back"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 18l-6-6 6-6" /></svg></button>
+    <button class="icon-button" data-size="sm" aria-label="Back" style="--gl-icon: var(--gl-icon-chevron-left)"></button>
     <span class="pattern-phone__title">Save</span>
   </header>
   <div class="pattern-phone__body">
-    <div class="pattern-photo-slot">
-      <svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 8 12 3 3 8v8l9 5 9-5V8Z" /><path d="M3 8l9 5 9-5M12 13v8" /></svg>
+    <div class="pattern-photo-slot" style="--gl-icon-start: var(--gl-icon-package)">
       <span class="type-label">Photo preview</span>
     </div>
     <label class="field" style="margin-top: var(--gl-space-m);">
@@ -79,7 +77,7 @@ Three screens, abbreviated to what each one adds.
 </div>
 <div class="pattern-phone">
   <header class="pattern-phone__header">
-    <button class="icon-button" data-size="sm" aria-label="Back"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 18l-6-6 6-6" /></svg></button>
+    <button class="icon-button" data-size="sm" aria-label="Back" style="--gl-icon: var(--gl-icon-chevron-left)"></button>
     <span class="type-label">Archive</span>
   </header>
   <div class="pattern-phone__body">
@@ -91,7 +89,7 @@ Three screens, abbreviated to what each one adds.
     <div class="type-label pattern-aside__label--tight">File</div>
     <ul class="card">
       <li>
-        <span class="avatar" data-square><svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 8 12 3 3 8v8l9 5 9-5V8Z" /><path d="M3 8l9 5 9-5M12 13v8" /></svg></span>
+        <span class="avatar" data-square style="--gl-icon: var(--gl-icon-package)"></span>
         <div class="pattern-row" style="font-size: var(--gl-step--1);">PDF · 2.1MB</div>
       </li>
     </ul>
diff --git a/_patterns/loop.md b/_patterns/loop.md
index 6fc2075..c0bc082 100644
--- a/_patterns/loop.md
+++ b/_patterns/loop.md
@@ -20,8 +20,7 @@ actual project built on this system would write.
   <header class="pattern-dashboard__header">
     <span class="pattern-dashboard__title">Loop</span>
     <div class="pattern-spacer"></div>
-    <button data-variant="primary" data-size="sm">
-      <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 5v14M5 12h14" /></svg>
+    <button data-variant="primary" data-size="sm" style="--gl-icon: var(--gl-icon-plus)">
       New
     </button>
     <span class="avatar">km</span>
diff --git a/_sass/components/button.css b/_sass/components/button.css
index 02254a5..95435b9 100644
--- a/_sass/components/button.css
+++ b/_sass/components/button.css
@@ -27,6 +27,9 @@ a.btn {
   --_pad-y: var(--gl-space-2xs);
   --_pad-x: var(--gl-space-xs);
   --_font: var(--gl-step--1);
+  /* The flex gap already separates the icon from the label. */
+  --gl-icon-start: var(--gl-icon);
+  --gl-icon-style-spacing: 0;
 
   display: inline-flex;
   align-items: center;
@@ -145,6 +148,10 @@ button:not(.icon-button):disabled,
   --_size: calc(2 * var(--_pad) + 1.15 * var(--_line));
   --_bg: transparent;
   --_fg: var(--gl-color-text-secondary);
+  /* No spacing, so stray whitespace in the markup can't push the icon
+     off-center. */
+  --gl-icon-start: var(--gl-icon);
+  --gl-icon-style-spacing: 0;
 
   display: inline-flex;
   align-items: center;
@@ -157,6 +164,7 @@ button:not(.icon-button):disabled,
   border-radius: var(--gl-radius-md);
   background: var(--_bg);
   color: var(--_fg);
+  font-size: var(--_line);
   cursor: pointer;
   transition: background-color var(--gl-dur-fast) var(--gl-ease),
               color var(--gl-dur-fast) var(--gl-ease);
diff --git a/_sass/components/card.css b/_sass/components/card.css
index 8e86580..ddcdde5 100644
--- a/_sass/components/card.css
+++ b/_sass/components/card.css
@@ -110,6 +110,10 @@
    <span> is too generic for a bare selector, so it keeps one class. */
 .avatar {
   --_size: 1.875rem;
+  --gl-icon-start: var(--gl-icon);
+  /* Sized from the box, not the initials' small font, so it follows data-size. */
+  --gl-icon-style-size: calc(var(--_size) * 0.55);
+  --gl-icon-style-spacing: 0;
 
   display: inline-flex;
   align-items: center;
diff --git a/_sass/components/icon.css b/_sass/components/icon.css
new file mode 100644
index 0000000..691d52f
--- /dev/null
+++ b/_sass/components/icon.css
@@ -0,0 +1,104 @@
+/* Icon
+   Icons are tokens (--gl-icon-* in tokens.css), placed by setting a custom
+   property — no <svg> in the markup. Adapted from Lea Verou's @nudeui/icons
+   0.0.3 (MIT), https://lea.verou.me/blog/2026/icons/, minus its opt-in
+   classes: style queries reach every browser this system supports, and
+   the class list is closed.
+
+   --gl-icon-start renders in ::before, --gl-icon-end in ::after. --gl-icon
+   means "give this component an icon"; each component redirects it to a
+   location (see button.css). An icon takes over that pseudo-element, so a
+   component that already draws its own ::before or ::after must not
+   redirect there.
+
+   All three are non-inheriting: an icon on a <p> must not repeat on every
+   <a> inside it. The pseudo-element inherits explicitly instead. */
+
+@property --gl-icon {
+  syntax: "*";
+  inherits: false;
+}
+@property --gl-icon-start {
+  syntax: "*";
+  inherits: false;
+}
+@property --gl-icon-end {
+  syntax: "*";
+  inherits: false;
+}
+
+/* A 1em square of the text color, masked by the icon. The image-set()
+   first candidate is a transparent placeholder for the box's content; the
+   second only exists to make the declaration fail — and so drop the
+   pseudo-element — when the icon property is unset. overflow: hidden puts
+   the box's baseline at its bottom edge so vertical-align can center it on
+   the cap height (https://blog.kizu.dev/cap-height-align/). */
+@container style(--gl-icon-start) {
+  ::before {
+    --gl-icon-start: inherit;
+    content: image-set(linear-gradient(transparent 0 100%), var(--gl-icon-start));
+    display: inline-block;
+    vertical-align: calc(0.5cap - var(--gl-icon-style-size, 1em) / 2 + var(--gl-icon-style-voffset, -0.02em));
+    overflow: hidden;
+    width: var(--gl-icon-style-size, 1em);
+    height: var(--gl-icon-style-size, 1em);
+    background-color: var(--gl-icon-style-color, currentColor);
+    mask: var(--gl-icon-start) no-repeat center / contain;
+    pointer-events: none;
+    forced-color-adjust: none;
+    flex: none;
+  }
+  :where(:not(:empty))::before { margin-inline-end: var(--gl-icon-style-spacing, 0.3em); }
+}
+
+@container style(--gl-icon-end) {
+  ::after {
+    --gl-icon-end: inherit;
+    content: image-set(linear-gradient(transparent 0 100%), var(--gl-icon-end));
+    display: inline-block;
+    vertical-align: calc(0.5cap - var(--gl-icon-style-size, 1em) / 2 + var(--gl-icon-style-voffset, -0.02em));
+    overflow: hidden;
+    width: var(--gl-icon-style-size, 1em);
+    height: var(--gl-icon-style-size, 1em);
+    background-color: var(--gl-icon-style-color, currentColor);
+    mask: var(--gl-icon-end) no-repeat center / contain;
+    pointer-events: none;
+    forced-color-adjust: none;
+    flex: none;
+  }
+  :where(:not(:empty))::after { margin-inline-start: var(--gl-icon-style-spacing, 0.3em); }
+}
+
+/* WebKit only, each fix in its own layer so any unlayered rule beats it.
+   https://bugs.webkit.org/show_bug.cgi?id=301609 — icons stay missing
+   until the first interaction; an animation forces style to re-resolve
+   against the right container. Its default duration is already zero.
+   https://bugs.webkit.org/show_bug.cgi?id=325900 — `inherit` on a
+   non-inheriting registered property is unreliable, so WebKit gets them
+   inheriting (the later registration wins) and descendants reset instead. */
+@keyframes gl-webkit-301609 {}
+
+@supports ((-webkit-nbsp-mode: normal) or (-webkit-touch-callout: none) or (font: -apple-system-body)) and (content-visibility: auto) {
+  @layer gl-webkit-301609 {
+    ::before,
+    ::after { animation-name: gl-webkit-301609; }
+  }
+
+  @property --gl-icon-start {
+    syntax: "*";
+    inherits: true;
+  }
+  @property --gl-icon-end {
+    syntax: "*";
+    inherits: true;
+  }
+
+  @layer gl-webkit-325900 {
+    @container style(--gl-icon-start) {
+      * { --gl-icon-start: initial; }
+    }
+    @container style(--gl-icon-end) {
+      * { --gl-icon-end: initial; }
+    }
+  }
+}
diff --git a/_sass/site/shell.css b/_sass/site/shell.css
index d9fa6bb..29c2d3e 100644
--- a/_sass/site/shell.css
+++ b/_sass/site/shell.css
@@ -66,6 +66,13 @@
 
 .gl-doc-header__actions { margin-inline-start: auto; display: flex; align-items: center; gap: var(--gl-space-2xs); }
 
+/* Shows the theme in effect, following the same selectors as tokens.css. */
+[data-theme-toggle] { --gl-icon: var(--gl-icon-sun); --gl-icon-style-size: 1rem; }
+:root[data-theme="dark"] [data-theme-toggle] { --gl-icon: var(--gl-icon-moon); }
+@media (prefers-color-scheme: dark) {
+  :root:not([data-theme="light"]) [data-theme-toggle] { --gl-icon: var(--gl-icon-moon); }
+}
+
 .gl-doc-layout {
   display: grid;
   grid-template-columns: 16rem minmax(0, 1fr);
diff --git a/assets/css/tokens.css b/assets/css/tokens.css
index 57f7bfb..0bba6a4 100644
--- a/assets/css/tokens.css
+++ b/assets/css/tokens.css
@@ -43,8 +43,21 @@
   --gl-color-text-secondary: #5f5d59;
   --gl-color-text-tertiary: #888681;
 
-  /* Icons — a data URI can't read a custom property, so each stroke repeats
-     its color by hand and is redefined alongside it in the dark blocks. */
+  /* Icons — monochrome SVGs, painted as a mask in the text color by
+     components/icon.css, so their own stroke color never shows. */
+  --gl-icon-camera: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M3 8a2 2 0 0 1 2-2h1.5l1-1.5h9l1 1.5H20a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8Z'/%3E%3Ccircle cx='12' cy='13' r='3.5'/%3E%3C/svg%3E");
+  --gl-icon-chevron-left: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.9' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M15 18l-6-6 6-6'/%3E%3C/svg%3E");
+  --gl-icon-filter: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 5h16l-6 8v5l-4 2v-7z'/%3E%3C/svg%3E");
+  --gl-icon-moon: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 14.2A8.4 8.4 0 0 1 9.8 4a8.4 8.4 0 1 0 10.2 10.2Z'/%3E%3C/svg%3E");
+  --gl-icon-package: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M21 8 12 3 3 8v8l9 5 9-5V8Z'/%3E%3Cpath d='M3 8l9 5 9-5M12 13v8'/%3E%3C/svg%3E");
+  --gl-icon-plus: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.9' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M12 5v14M5 12h14'/%3E%3C/svg%3E");
+  --gl-icon-settings: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='3'/%3E%3Cpath d='M12 2.5v3M12 18.5v3M3.6 7.2l2.6 1.5M17.8 15.3l2.6 1.5M3.6 16.8l2.6-1.5M17.8 8.7l2.6-1.5'/%3E%3C/svg%3E");
+  --gl-icon-sun: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='12' cy='12' r='4.5'/%3E%3Cpath d='M12 1.8v2.4M12 19.8v2.4M4.6 4.6l1.7 1.7M17.7 17.7l1.7 1.7M1.8 12h2.4M19.8 12h2.4M4.6 19.4l1.7-1.7M17.7 6.3l1.7-1.7'/%3E%3C/svg%3E");
+  --gl-icon-trash: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 7h16M9 7V4h6v3M6 7l1 13h10l1-13'/%3E%3C/svg%3E");
+
+  /* Select paints its chevron as a background, not a mask, and a data URI
+     can't read a custom property — so this one stroke repeats
+     --gl-color-text-secondary by hand and is redefined in the dark blocks. */
   --gl-icon-chevron-down: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='8' viewBox='0 0 12 8'%3E%3Cpath d='M1 1.5 6 6.5l5-5' fill='none' stroke='%235f5d59' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
 
   /* Accent */
diff --git a/assets/js/system.js b/assets/js/system.js
index bba90eb..6872652 100644
--- a/assets/js/system.js
+++ b/assets/js/system.js
@@ -35,9 +35,6 @@
   function paintToggle(theme) {
     document.querySelectorAll("[data-theme-toggle]").forEach(function (toggle) {
       var next = theme === "dark" ? "light" : "dark";
-      toggle.querySelectorAll("[data-theme-icon]").forEach(function (icon) {
-        icon.hidden = icon.dataset.themeIcon !== theme;
-      });
       toggle.setAttribute("aria-label", "Switch to the " + next + " theme");
       toggle.setAttribute("title", "Switch to the " + next + " theme");
       var label = toggle.querySelector("[data-theme-label]");
diff --git a/index.md b/index.md
index bfa4ec6..c8d1fa6 100644
--- a/index.md
+++ b/index.md
@@ -55,7 +55,7 @@ same afternoon.
     <div class="card-body">
       <h3><a href="<%= relative_url('/foundations/color/') %>">Foundations</a></h3>
       <p>
-        Color, type, space, elevation, and motion — with contrast ratios
+        Color, type, space, elevation, motion, and icons — with contrast ratios
         computed from the tokens and checked by hand, not typed by guess.
       </p>
     </div>
diff --git a/lib/build.rb b/lib/build.rb
index 98feb37..0af5ad0 100644
--- a/lib/build.rb
+++ b/lib/build.rb
@@ -48,6 +48,7 @@ LANG        = "en"
 CSS_BUNDLES = {
   "base" => %w[base/reset.css base/typography.css],
   "components" => %w[
+    components/icon.css
     components/button.css
     components/field.css
     components/badge.css
diff --git a/rationale.md b/rationale.md
index 7c63049..f70699a 100644
--- a/rationale.md
+++ b/rationale.md
@@ -139,8 +139,8 @@ Being honest about the design is being honest about the gaps too.
   by a source of truth beyond this site.
 - **No logo or brand mark.** Render "Gloss" or the project's own name in
   type wherever a mark would go — don't invent one.
-- **The icon set (Lucide) is a flagged substitution** — there's no shipped
-  icon library of its own yet.
+- **The icon set is only what these pages use** — ten tokens, not a
+  library. Add one to `tokens.css` when a screen needs it.
 - **Signal Teal, the default accent, is one pick among the sanctioned
   alternates**, not a fixed identity.
 - **No Table, Callout, or Progress component.** An earlier pass at this site