One thesis with a ladder under it: an icon at rest is never pure black. Plus the three well recipes and where the law is enforced.
How every glyph in the app is chosen, weighted, sized and — above all — colored. A cross-cutting law like feedback.md; the system-header rules in AGENTS.md are this law's nav-surface specialization. There is no separate kit: the law lives in the components that render icons (EmptyMedia, PageHeaderIcon, the tone vocabulary, the header cluster).
01
An icon at rest is never pure black. Text earns full foreground because it is content; a glyph is furniture. In light mode a full-foreground icon reads as a dirty ink blot next to the page's greys — the exact failure the header law caught for the toolbar, now generalized: a glyph's resting color is text-muted-foreground, and anything darker or brighter must be earned (hover, active, tone, brand).
02
| State | Color | Example |
|---|---|---|
| Rest (decorative, bare, or icon-only control) | text-muted-foreground | empty-state wells, toolbar triggers, pager arrows |
| Hover / focus on an interactive glyph | hover:text-foreground | header cluster, view-options trigger |
| Active / selected | text-foreground (or text-primary when selection is brand-marked) | sort arrow on the active column |
| Tone-bearing status | TONES[tone].text — never a raw palette literal | alert/toast glyphs, status badges |
| Brand / identity emphasis | text-primary | PageHeaderIcon, active nav item |
| Beside a text label inside a control | inherit the control's color | button icons, menu-item icons |
The last row is the legitimate exception: when a glyph sits next to a label they must be ONE color, and the control decides it (a primary button's icon is white because its label is). Bare glyphs — the ones with nothing anchoring them — are the ones that must be muted.
On dark nav surfaces (data-surface="nav") the same pair of classes is correct — the scope re-points the variables (see AGENTS.md).
03
An icon in a tile ("well") uses one of exactly three recipes:
bg-muted text-muted-foreground (EmptyMedia's default). For situational glyphs: empty states, placeholders.TONES[tone].bg + TONES[tone].text. For status: KPI tiles, error states, the tone vocabulary card.bg-primary/10 text-primary (PageHeaderIcon). For identity: page headers, feature tiles.A well never mixes recipes (muted background with a black glyph was the bug that prompted this law).
04
The glyphs that live inside form controls — chevrons, clear × buttons, calendar / clock triggers, steppers, search magnifiers — are furniture like any other bare glyph, so the ladder applies unchanged:
text-muted-foreground. This includes the select chevron (shadcn muted it with opacity-50 for the same reason).text-foreground, earned on the control's hover or open state, not just the glyph's own hitbox (NumberField steppers, the DatePicker trigger, the select chevron while its popup is open).Enforcement lives in two places: components we own carry the classes directly (NumberField, DataViewSearch, the DatePicker wrapper's trigger glyph), and Appica internals that hardcode text-foreground (the select chevron, the calendar nav buttons) are overridden by the affix block at the tail of src/app/appica-bridge.css. A new affix icon must state text-muted-foreground explicitly — never let it inherit the field's text color.
05
The icon system splits into two sets with different rules — the same split that lets shadcn's create swap icon libraries (their registry maps a small closed set by hand; icon libraries share no naming convention):
"One family: Solar" below reads against the expressive set; it does not mandate rewriting the utility glyphs inside ui/* primitives.
06
icon-[solar--…]). Brand marks Solar doesn't carry come from the logos collection (real logos, colored ones keep their colors) or inline SVG with fill-current.*-bold-duotone for identity/navigation chrome (sidebar, page-header tiles, header cluster); *-bold for solid status marks (the tone glyphs); *-linear for utility controls (arrows, chevrons, tools, filter/sort).size-3.5 inline arrows/chips · size-4 buttons and status glyphs · size-4.5 header cluster · size-5–size-5.5 wells · size-6 empty-state glyphs. Pick from the ladder; don't invent in-between sizes.ui/* [&_svg] sizing rules cannot reach iconify spans, so an unsized icon-[…] glyph silently renders at 1em (14px in a default button — the "slightly off" look). Button mirrors its whole svg ladder for icon-* classes, so unsized Solar glyphs inside buttons now grade with the button size; inside any other primitive, always state a ladder size on the span.@iconify-json/solar/icons.json — a typo'd name renders as an invisible empty span, not an error.aria-hidden; icon-only controls carry aria-label.07
In an architecture or flow diagram every box that names a PRODUCT wears that product's own mark. A generic glyph standing in for a named service is not a fallback — it is a box that tells the reader nothing its label had not already said, and it quietly claims the logo could not be found when usually nobody looked past the first shelf.
The lookup order, and each rung is exhausted before the next:
logos (iconify "SVG Logos") — the installed default.devicon, then vscode-icons — installed alongside it; between them they carry marks logos misses (zustand's bear lives in devicon).gh api "search/code?q=repo:OWNER/REPO+extension:svg" found better-auth's docs/public/branding/svg/ folder that four guessed URLs had missed, and listing Alchemy's public directory surfaced the icon-only flask.svg beside the horizontal wordmark that was all a plain fetch had returned. svgl.app is a good second stop (TanStack came from it). What you find gets VENDORED — read before inlining, ids namespaced, any <style> block stripped, monochrome marks switched to currentColor — so the page never phones out for an icon.A Solar glyph is still right for a box that is a ROLE rather than a product — a person at a browser, a route handler, a queue. The test is whether the label names something with a logo.
08
foreground / rendering pure black at rest.text-muted-foreground.TONES.aria-label.