diff --git a/docs/IMPLEMENTATION-PLAN.md b/docs/IMPLEMENTATION-PLAN.md
index 702a8ab..25a4efc 100644
--- a/docs/IMPLEMENTATION-PLAN.md
+++ b/docs/IMPLEMENTATION-PLAN.md
@@ -450,7 +450,7 @@ through `services/transfer.ts` (→ `normalizeImport` / envelope build), no moda
IDB-transaction atomicity would require exposing a raw transaction from `db.ts` — see
[arch 02](architecture/02-persistence.md)).
- **A11y:** modal focus trap + return, labelled icon buttons, contrast in both themes (§10) — ✅ in place.
-- **About & Privacy** and **Donate** modals ~~(§01)~~ ✅ (Donate URL is a placeholder
+- **About** and **Donate** modals ~~(§01)~~ ✅ (Donate URL is a placeholder
`DONATE_URL` pending the real link).
- **Offline/installable:** the manifest now ships a full SVG icon set (favicon / maskable /
monochrome) + `theme_color`, and the SW precaches the shell — the app is **installable**.
diff --git a/docs/architecture/09-visual-design.md b/docs/architecture/09-visual-design.md
index 317798c..ab0826d 100644
--- a/docs/architecture/09-visual-design.md
+++ b/docs/architecture/09-visual-design.md
@@ -94,15 +94,16 @@ and size is a relationship of 8 (with 2/4 as fine sub-steps):
Color is expressed as **roles**, never raw hexes, so themes can repaint the whole
UI by swapping one set of values. Borrowed from Carbon's layering model:
-| Role token | Meaning |
-| ---------------------------------------------------- | ----------------------------------------------------------------------------------- |
-| `--bg` | App canvas (lowest layer) |
-| `--layer-01` / `--layer-02` | Raised surfaces (panels, cards, popovers) — elevation by lightness step, not shadow |
-| `--border` / `--border-strong` | Subtle and prominent separators |
-| `--text` / `--text-secondary` / `--text-placeholder` | Text hierarchy |
-| `--accent` / `--accent-hover` / `--accent-contrast` | The expressive accent — **swappable**; UI must never hardcode a hue |
-| `--focus` | Focus-ring color (defaults to `--accent`) |
-| `--support-error / -success / -warning / -info` | Status only — color = meaning |
+| Role token | Meaning |
+| ---------------------------------------------------- | -------------------------------------------------------------------------------------- |
+| `--bg` | App canvas (lowest layer) |
+| `--layer-01` / `--layer-02` | Raised surfaces (panels, cards, popovers) — elevation by lightness step, not shadow |
+| `--border` / `--border-strong` | Subtle and prominent separators |
+| `--text` / `--text-secondary` / `--text-placeholder` | Text hierarchy |
+| `--accent` / `--accent-hover` / `--accent-contrast` | The expressive accent — **swappable**; UI must never hardcode a hue |
+| `--accent-soft` / `--accent-soft-hover` | Low-emphasis accent **wash** (accent mixed into `--bg`) for a tinted-but-quiet surface |
+| `--focus` | Focus-ring color (defaults to `--accent`) |
+| `--support-error / -success / -warning / -info` | Status only — color = meaning |
- **Neutrals** use the Carbon gray ramp (`#f4f4f4 … #161616`) — accessible and
legible. **Accent and theming are open**: the default accent is **deep teal**
@@ -113,6 +114,17 @@ UI by swapping one set of values. Borrowed from Carbon's layering model:
warning `#f1c21b`, info `#0043ce` — tuned per theme for contrast.
- **Contrast:** target WCAG AA (4.5:1 text, 3:1 large/UI). Accent-on-`--bg` and
text-on-`--accent` must both pass for any shipped theme.
+- **Soft accent is derived, not hardcoded.** `--accent-soft` /
+ `--accent-soft-hover` are `color-mix(in srgb, var(--accent) 12–20%, var(--bg))`,
+ so the wash follows whatever accent + theme is active rather than carrying a
+ per-accent value. Use it for a surface that should be _noticed_ without competing
+ with a primary action (the header's **Donate** button).
+- **Field-on-layer (Carbon layering).** A field is a layer _on top of_ the surface
+ it sits on — so it steps **toward `--bg`**, never to a darker gray. On the white
+ canvas and on a `--layer-01` panel alike, an input reads as **`--bg` fill +
+ `--border-strong` outline**, not a `--layer-02` fill. Stacking a darker-gray field
+ on a gray panel (canvas → `--layer-01` panel → `--layer-02` field) muddies into
+ three indistinct grays — the bug this rule prevents (the snippet metadata panel).
### 3.4 Shape & elevation
@@ -138,9 +150,19 @@ UI by swapping one set of values. Borrowed from Carbon's layering model:
## 4. Component conventions
- **Buttons:** square, 32px (compact) / 40px (default) tall. Variants: **primary**
- (filled `--accent`), **secondary** (bordered), **ghost** (text-only),
- **danger** (filled `--support-error`). 600-weight label. Clear hover/active and
- a visible focus ring.
+ (filled `--accent`), **secondary** (bordered), **ghost** (text-only, borderless),
+ **soft-accent** (ghost on an `--accent-soft` wash — a low-emphasis solicitation,
+ e.g. Donate), **danger** (filled `--support-error`). 600-weight label. Clear
+ hover/active and a visible focus ring.
+- **Emphasis hierarchy (Carbon button/usage).** A region carries **one**
+ high-emphasis (primary) button at most; everything else is lower emphasis. In
+ toolbars/headers full of utilities, the utilities go **ghost** so they recede
+ behind the work area and read as a row of equals — only the genuine call to
+ action is filled. Worked examples: the **editor toolbar** (Publish is the lone
+ primary; Extract/Revert are secondary, and collapse to icons when narrow — see
+ [arch 10 §8](10-interaction-and-feedback.md)), and the **header** (Datasets /
+ Import / Export / About are ghost; a divider then sets off the soft-accent Donate
+ and the ghost theme toggle).
- **Hover is variant-specific:** filled buttons (primary/danger) **darken**
(`--accent-hover` / a slight brightness drop); outlined/ghost buttons **gain a
fill one elevation step above their surface** — on `--bg` → `--layer-01`, on a
@@ -150,8 +172,10 @@ UI by swapping one set of values. Borrowed from Carbon's layering model:
must step its hover fill up to match.
- **Focus ring:** a 2px `--focus` outline (offset 1–2px). Always visible on
keyboard focus — accessibility is non-negotiable (principle 4).
-- **Fields** (text, textarea, select, search): square, 1px `--border`, `--layer-01`
- fill, accent border + focus ring on focus. Mono font for spec/JSON inputs.
+- **Fields** (text, textarea, select, search): square, 1px `--border-strong`,
+ **`--bg` fill** (field-on-layer, §3.3 — white on the canvas _and_ on a
+ `--layer-01` panel; never a darker-gray fill), accent border + focus ring on
+ focus. Mono font for spec/JSON inputs.
- **List rows** (snippet library): compact, full-row hover (`--layer-01`),
active row marked by an accent left-border + `--layer-01` fill, secondary
metadata in `--text-secondary`. Row-level actions reveal on hover.
@@ -232,15 +256,32 @@ Carbon and drawn `fill: currentColor`.
**Core set** — recurring, cross-surface:
-| Meaning | Carbon glyph | Form | Surfaces |
-| -------------------- | --------------- | --------------------- | ------------------------------------------------- |
-| Close / dismiss | `Close` (✕) | icon-only ⭐ | `ModalShell`, `Toaster` |
-| Theme → dark | `Asleep` (moon) | icon-only ⭐ | `ThemeToggle` (shown when light) |
-| Theme → light | `Light` (sun) | icon-only ⭐ | `ThemeToggle` (shown when dark) |
-| References a dataset | `DataTable` | icon + text | Library row marker, Linked-datasets list |
-| Add / create-new | `Add` | icon + text | Library "Create New Snippet", Datasets "New …" |
-| Delete | `TrashCan` | icon-only ⭐ (danger) | Library row delete¹ — text "Delete" in the panel² |
-| Unpublished draft | (CSS dot) | status-glyph | Library row (paired with a hidden label) |
+| Meaning | Carbon glyph | Form | Surfaces |
+| -------------------- | --------------- | ------------------------ | -------------------------------------------------------------------------------------------- |
+| Close / dismiss | `Close` (✕) | icon-only ⭐ | `ModalShell`, `Toaster` |
+| Theme → dark | `Asleep` (moon) | icon-only ⭐ | `ThemeToggle` (shown when light) |
+| Theme → light | `Light` (sun) | icon-only ⭐ | `ThemeToggle` (shown when dark) |
+| References a dataset | `DataTable` | icon + text | Library row marker, Linked-datasets list, header **Datasets**, editor **Extract**⁴ |
+| Add / create-new | `Add` | icon + text → icon-only⁴ | Library "Create New Snippet" (collapses to "+" when the pane is narrow), Datasets "New …" |
+| Delete | `TrashCan` | icon-only ⭐ (danger) | Library row delete¹ — text "Delete" in the panel² |
+| Import workspace | `Upload` | icon + text | Header **Import** (a file is brought into the app) |
+| Export workspace | `Download` | icon + text | Header **Export** (the workspace is written out) |
+| About / information | `Information` | icon + text | Header **About** |
+| Revert draft | `Reset` | icon + text → icon-only⁴ | Editor toolbar **Revert** (restore last published) |
+| Live search | `Search` | icon-in-field⁵ | Library search box (leading magnifier; the input's `aria-label`/placeholder names the field) |
+| Settings (gear) | `Settings` | icon-only ⭐ | Per-pane settings disclosures (Editor, Preview, Library dates) |
+| Unpublished draft | (CSS dot) | status-glyph | Library row (paired with a hidden label) |
+
+**Pane-toggle set** — the one **custom** sub-family (not single Carbon glyphs): a
+panel frame with one of three regions filled, where the filled bar's _position_
+encodes which pane it toggles. Icon-only by design — position is the meaning — each
+carrying an `aria-label`:
+
+| Meaning | Glyph | Form | Surface |
+| ------------------- | ------------------------------ | ------------ | ----------------- |
+| Toggle library pane | panel frame, **left** filled | icon-only ⭐ | `PaneToggleStrip` |
+| Toggle editor pane | panel frame, **centre** filled | icon-only ⭐ | `PaneToggleStrip` |
+| Toggle preview pane | panel frame, **right** filled | icon-only ⭐ | `PaneToggleStrip` |
**Status set** — Carbon's **filled** notification glyphs, one per severity. Unlike
the outline UI set, these are coloured **by status** (not by surrounding text) and
@@ -257,23 +298,37 @@ notifications/validation as they arrive):
| Success | `CheckmarkFilled` | `--support-success` | `Toaster` (success) |
| Info | `InformationFilled` | `--support-info` | `Toaster` (info) |
-**Scoped set** — registered (glyph reserved) but single-surface and **not yet
-implemented**:
+**Scoped set** — single-surface, glyph **reserved** in the ledger but **not yet in
+the `Icon` registry**:
-| Meaning | Carbon glyph | Form | Surface / note |
-| ---------------- | ------------------ | ----------- | ----------------------------------------------------- |
-| Swap / transpose | `ArrowsHorizontal` | icon + text | Chart Builder "Swap X/Y" — one button; icon is polish |
+| Meaning | Carbon glyph | Form | Surface / note |
+| ---------------- | ------------------ | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| Swap / transpose | `ArrowsHorizontal` | icon + text | Chart Builder "Swap X/Y" — the button **ships** today with an interim Unicode `⇄`, not a registry `Icon`. `ArrowsHorizontal` stays reserved here so the meaning is claimed; promote the button to it (add the `IconName` + glyph) when polishing the swap. |
-⭐ = **icon-only set**: close + theme (truly universal), plus **delete** as a
-deliberate destructive-row exception — a dense, repeated list action where a label
-would cost more than it gives. ✕ means **close only**; delete is `TrashCan`, never
-✕ (that collision is exactly what one-glyph-one-meaning forbids).
+⭐ = **icon-only set** (the glyph alone names the control, via `aria-label`): the
+**universal** glyphs `close` + `theme`; the conventional disclosure/affordance
+glyphs `settings` (gear) and the **pane-toggle** trio (position is the meaning);
+and **delete** as a deliberate destructive-row exception — a dense, repeated list
+action where a label would cost more than it gives. `search` is _not_ ⭐: its
+magnifier is a decorative lead-in to a labelled input (footnote ⁵), not a control
+named by the glyph. ✕ means **close only**; delete is `TrashCan`, never ✕ (that
+collision is exactly what one-glyph-one-meaning forbids). Admitting a glyph to ⭐ is
+a contract change (§5.1 rule 4), not a per-component call.
¹ Row delete is hover/focus-revealed and reddens on hover/focus (arch 10 — reveal &
destructive-intent rules). ² "Duplicate" and "Delete" in the detail panel stay
**text** (label-first; lower frequency, not a dense row). ³ The raw warning yellow
fails contrast on light surfaces, so the warning glyph uses `--support-warning-fg`
-(darkened amber; the yellow `--toast-accent` stays on the decorative border).
+(darkened amber; the yellow `--toast-accent` stays on the decorative border). ⁴
+**Responsive collapse**, not membership in the icon-only set: these are icon+text
+that _shed the label_ under width pressure (the editor toolbar's secondary actions,
+and the library's standalone Create CTA, when the pane is narrow — [arch 10
+§8](10-interaction-and-feedback.md)), keeping the accessible name in
+`aria-label`/`title`. A degradation that preserves the name is distinct from a
+permanent icon-only control (§5.1 rule 4), so it isn't a closed-set change. ⁵
+**Icon-in-field**: a decorative leading glyph _inside_ a labelled control — the
+search input's magnifier is `aria-hidden`, and the input itself carries the
+accessible name. Not icon-only (the control is named by its label, not the glyph).
### 5.3 Size
diff --git a/docs/architecture/10-interaction-and-feedback.md b/docs/architecture/10-interaction-and-feedback.md
index 60bc928..0c3da18 100644
--- a/docs/architecture/10-interaction-and-feedback.md
+++ b/docs/architecture/10-interaction-and-feedback.md
@@ -303,8 +303,10 @@ Carbon search/active-search, NN/g #3. This bullet is the contract; cite it, not
**Resolved — library sort (APG menu-button + NN/g recognition).** The Sort control (spec
§02) reuses the **disclosure popover** primitive (the settings-popover model above), **not**
-an ARIA menu — but its trigger shows the **current state for recognition** ("Sort: Modified
-↓"), per NN/g #6 (recognition over recall), instead of a bare gear. Fields are **Modified /
+an ARIA menu — but its trigger shows the **current state for recognition** ("Modified
+↓"), per NN/g #6 (recognition over recall), instead of a bare gear. (The visible text drops
+the "Sort:" verb prefix to stay compact in the narrow library rail — §8; the full name
+"Sort by Modified, descending" rides in `aria-label`.) Fields are **Modified /
Created / Name / Size**; the active field shows a **direction arrow** (↓ desc / ↑ asc) in both
the trigger and the field row, and the arrow's meaning is mirrored into the field's
`aria-label` ("Modified, descending") so it isn't carried by the glyph alone. **Selection
@@ -454,6 +456,42 @@ iconography contract, [arch 09 §5](09-visual-design.md).)
---
+## 8. Space-constrained controls (responsive collapse)
+
+The three work panes resize independently, so a toolbar's room is a function of its
+**pane's** width, not the viewport's. Controls must stay usable at the pane minimum
+without clipping, wrapping, or crowding.
+
+- **Query the pane, not the window.** Use a CSS **container query**
+ (`container-type: inline-size` on the row, `@container` on the controls), not a
+ media query — the pane width is what changed. Scope the container to the toolbar
+ row itself, away from heavy children (e.g. the Monaco editor) whose own layout
+ shouldn't inherit size containment.
+- **Shed labels under pressure; keep a contested primary labelled.** When a toolbar
+ would wrap or crowd, **secondary** actions collapse to icon-only and the label
+ moves to `aria-label`/`title`. A **primary that shares the row with secondaries**
+ keeps its text — the label is what marks it as _the_ action to take (the editor
+ toolbar below ~480px: Publish stays "Publish"; Extract/Revert become glyphs). A
+ **standalone primary CTA**, whose prominence is carried by fill + size + position
+ rather than its words, _may_ collapse to a universal-set icon at the pane floor
+ (the library's "Create New Snippet" → "+" below ~250px). Either way it's a
+ degradation that preserves the accessible name — distinct from the closed
+ icon-only set (arch 09 §5.1 rule 4).
+- **Trim a compact trigger's prose, not its state.** A disclosure trigger that names
+ its current state for recognition (NN/g #6) may drop the **verb prefix** to fit a
+ narrow rail — the library Sort trigger shows "Modified ↓", not "Sort: Modified ↓"
+ — but the full name (`aria-label="Sort by Modified, descending"`) is preserved for
+ assistive tech, so only redundant visible words are cut.
+- **A flex control must be able to shrink.** A side control (a Sort button) carries
+ `flex: 0 0 auto` so the flexible field (search) absorbs the slack; the field's
+ `` needs `min-width: 0`, or its intrinsic ~20ch width overflows the slot
+ and overlaps its neighbour. Right-aligned toolbars (`justify-content: flex-end`)
+ clip their **leftmost** item on overflow — left-align so the trimmable end is a
+ settings affordance, not a primary control, and size the pane minimum so it
+ doesn't overflow at all.
+
+---
+
## Do / Don't
**Do**
diff --git a/docs/architecture/visual-specimen.html b/docs/architecture/visual-specimen.html
index 083dcc1..095d8aa 100644
--- a/docs/architecture/visual-specimen.html
+++ b/docs/architecture/visual-specimen.html
@@ -87,6 +87,13 @@
--on-status: #161616;
}
+ /* Soft accent — derived wash (mirrors styles/tokens.css). References --accent
+ + --bg, so it follows whatever accent/theme is active. */
+ [data-theme] {
+ --accent-soft: color-mix(in srgb, var(--accent) 12%, var(--bg));
+ --accent-soft-hover: color-mix(in srgb, var(--accent) 20%, var(--bg));
+ }
+
/* --- Accent: the FREE, swappable layer (theme-aware). Deep teal is the
default; see styles/tokens.css for the shipped source of truth. --- */
[data-accent='teal'] {
@@ -436,6 +443,29 @@
.btn-ghost:hover {
background: var(--layer-01);
}
+ /* Header utility ghost: text-coloured, recedes; hover steps up one layer. */
+ .btn-ghost-util {
+ background: transparent;
+ color: var(--text);
+ gap: var(--space-2);
+ }
+ .btn-ghost-util:hover {
+ background: var(--layer-02);
+ }
+ /* Soft-accent: a low-emphasis accent wash (Donate) — noticed, not shouting. */
+ .btn-soft {
+ background: var(--accent-soft);
+ color: var(--accent-hover);
+ }
+ .btn-soft:hover {
+ background: var(--accent-soft-hover);
+ }
+ .hdr-divider {
+ width: var(--border-width);
+ height: 20px;
+ background: var(--border-strong);
+ margin: 0 var(--space-2);
+ }
.btn-danger {
background: var(--support-error);
color: var(--on-status);
@@ -459,13 +489,16 @@
font-size: 12px;
color: var(--text-secondary);
}
+ /* Field-on-layer (arch 09 §3.3): a field is a layer above its surface, so it
+ steps toward --bg (white) with a --border-strong outline — on the canvas
+ and on a gray panel alike — never a darker-gray fill. */
.input,
.select,
.textarea {
font: inherit;
font-size: 14px;
color: var(--text);
- background: var(--layer-01);
+ background: var(--bg);
border: var(--border-width) solid var(--border-strong);
border-radius: var(--radius);
padding: var(--space-3) var(--space-4);
@@ -930,6 +963,26 @@
Tab through to see the focus ring (2px accent outline).
+
+
Emphasis hierarchy — a header cluster
+
+ Utilities go ghost (icon + text, borderless) so they recede;
+ a divider sets off Donate on a soft-accent wash
+ — one low-key solicitation, no border. (Carbon: one high-emphasis action per
+ region; utilities at low emphasis.)
+
- In context: icon + text is the default; icon-only is the universal/destructive
- exception. ✕ means close only — delete is the trash glyph.
+ The full Core + pane-toggle set (arch 09 §5.2); the four status glyphs appear,
+ coloured, in the Toasts section below. In context: icon + text is the default;
+ icon-only is the universal/affordance/destructive exception. ✕ means close only —
+ delete is the trash glyph.
diff --git a/docs/spec/01-application-shell.md b/docs/spec/01-application-shell.md
index cd4ca26..f5b66e8 100644
--- a/docs/spec/01-application-shell.md
+++ b/docs/spec/01-application-shell.md
@@ -27,18 +27,18 @@ A fixed header spans the top of the app.
- **Left side**: the app icon, the app title ("Astrolabe"), and a version badge showing the current app version.
- **Right side**: a row of text entry points. Each opens a destination:
-| Entry point | Opens |
-| --------------- | --------------------------------------------------------------------------------------------------------------- |
-| Import | A file-picker dialog to choose a previously exported file; the chosen file is imported (see _Import & Export_). |
-| Export | Immediately produces a downloaded file containing all snippets and datasets (see _Import & Export_). |
-| Datasets | The Datasets manager modal (see _Datasets_). |
-| About & Privacy | The About & Help modal (keyboard shortcuts, about, and privacy information). |
-| Donate | The Donate modal. |
+| Entry point | Opens |
+| ----------- | -------------------------------------------------------------------------------------------------------------------- |
+| Import | A file-picker dialog to choose a previously exported file; the chosen file is imported (see _Import & Export_). |
+| Export | Immediately produces a downloaded file containing all snippets and datasets (see _Import & Export_). |
+| Datasets | The Datasets manager modal (see _Datasets_). |
+| About | The About & Help modal (keyboard shortcuts, about, and privacy information). Labelled "About"; privacy lives inside. |
+| Donate | The Donate modal. |
Notes:
- Import and Export act directly (file dialog / file download); they do not open in-app modals.
-- The Datasets, About & Privacy, and Donate entry points each open a modal (see _Modal System_).
+- The Datasets, About, and Donate entry points each open a modal (see _Modal System_).
- **Settings are not a header entry point.** A design review (see _Settings_) distributed
preferences to the panes they affect — the appearance theme is a header toggle, and the
Editor / Performance / Formatting clusters open from a gear control in their own pane. There
diff --git a/src/app/App.module.css b/src/app/App.module.css
index 28e45b4..2dfe36b 100644
--- a/src/app/App.module.css
+++ b/src/app/App.module.css
@@ -61,11 +61,20 @@
flex: 1;
}
-/* Header text-button (Datasets, and future header actions). */
+/*
+ * Header utility buttons (Datasets / Import / Export / About) — low-emphasis
+ * ghost buttons: borderless, icon + label, so they recede behind the work area
+ * and read as a row of equals (Carbon: utilities in toolbars/dashboards belong at
+ * low emphasis). The transparent border holds the box size so hover doesn't shift
+ * neighbours. Matches the ThemeToggle's ghost icon button beside them.
+ */
.headerButton {
+ display: inline-flex;
+ align-items: center;
+ gap: var(--space-2);
height: 32px;
- padding: 0 var(--space-4);
- border: var(--border-width) solid var(--border-strong);
+ padding: 0 var(--space-3);
+ border: var(--border-width) solid transparent;
border-radius: var(--radius);
background: transparent;
color: var(--text);
@@ -73,11 +82,13 @@
font-size: 13px;
font-weight: 500;
cursor: pointer;
- transition: background var(--dur-fast) var(--ease);
+ transition:
+ background var(--dur-fast) var(--ease),
+ color var(--dur-fast) var(--ease);
}
.headerButton:hover {
- background: var(--layer-01);
+ background: var(--layer-02);
}
.headerButton:focus-visible {
@@ -85,6 +96,31 @@
outline-offset: 2px;
}
+/* Vertical rule separating the utilities from Donate + the theme toggle. */
+.headerDivider {
+ flex: 0 0 auto;
+ width: var(--border-width);
+ height: 20px;
+ margin: 0 var(--space-2);
+ background: var(--border-strong);
+}
+
+/*
+ * Donate — the one solicitation in the header. A soft-accent wash lifts it just
+ * above the ghost utilities without competing with a primary action (it carries
+ * no border; the tint alone distinguishes it). Placed after .headerButton:hover
+ * so its own hover state wins the equal-specificity tie.
+ */
+.donate {
+ background: var(--accent-soft);
+ color: var(--accent-hover);
+}
+
+.donate:hover {
+ background: var(--accent-soft-hover);
+ color: var(--accent-hover);
+}
+
/* The Import file picker is driven programmatically by its header button. */
.hiddenInput {
display: none;
diff --git a/src/app/App.tsx b/src/app/App.tsx
index 3258d03..9f0a658 100644
--- a/src/app/App.tsx
+++ b/src/app/App.tsx
@@ -4,6 +4,7 @@ import { LivePreview } from './components/LivePreview';
import { ModalShell } from './components/ModalShell';
import { Onboarding } from './components/Onboarding';
import { PaneSplitHandle } from './components/PaneSplitHandle';
+import { Icon } from './components/Icon';
import { PaneToggleStrip } from './components/PaneToggleStrip';
import { ResizeHandle } from './components/ResizeHandle';
import { SnippetLibrary } from './components/SnippetLibrary';
@@ -74,6 +75,11 @@ export function App() {
Astrolabe
v{__APP_VERSION__}
+ {/* Header actions establish a hierarchy (Carbon button/usage → one
+ high-emphasis action per region; utilities in dashboards/toolbars read
+ better at low emphasis). The utilities are ghost (borderless) buttons
+ with a leading icon for scanning; a divider then sets off Donate — the
+ one solicitation, given a soft-accent wash — and the theme toggle. */}
+