# Icons Guide — Agent Instructions

This file is the source of truth for generating **Fractal icons**: thin, single-color, stroke
(outline) SVGs designed to read from 16px up. It is the guide _under test_ in the
`icon-set` prompt-eval pipeline, so it is deliberately prescriptive.

**Follows:** _Surface treatment_, _Color application_, and _Density_ from `philosophy.md`.

---

## What a Fractal icon is

- **Stroke / outline only.** No fills unless explicitly asked, no solid silhouettes, no gradients, no multiple colors.
- **One color**, applied with `stroke="currentColor"` so the icon inherits text color.
- **Quiet and precise.** Geometric, scientific, restrained — never playful or skeuomorphic.
- A member of a **system**: icons share base shapes (document, container, arrow, node) and a
  common stroke rhythm so a set reads as one family.

---

## Geometry (non-negotiable)

| Property           | Value                                                                                |
| ------------------ | ------------------------------------------------------------------------------------ |
| viewBox            | `0 0 32 32` (square) — **author every icon at 32**                                    |
| Trim (hard max)    | **1px** on every side — no artwork past coords ~1–31 (30×30 maximum extent)           |
| Live area          | **optical, by shape** — fill to a per-shape target inside the 30×30 (see table below) |
| Stroke width       | **2** user units, uniform across the whole icon                                       |
| Linecap / linejoin | `round` / `round`                                                                    |
| Fill               | `fill="none"`                                                                         |
| Corner radius      | **≈ 2.5–3 units** for rounded corners (small, consistent)                             |
| Color              | `stroke="currentColor"` — never a baked hex/rgb                                       |

The 32 grid keeps the stroke (2) and trim (1) as clean integers at every standard render size.

### Stroke scaling — do not fight it

Author the stroke at **2 units and let it scale with the viewBox**. Do **not** add
`vector-effect="non-scaling-stroke"`. Because the stroke scales proportionally, one authored value
renders correctly at every display size:

| Rendered size | Effective stroke |
| ------------- | ---------------- |
| 16px          | 1.0px            |
| 24px          | 1.5px            |
| 32px          | 2.0px            |

This is the intended behavior — thinner when small, heavier when large. (`2/32 = 1.5/24 = 1/16`,
so a 32-grid 2px stroke is the same proportional weight the set has always used.)

### Optical live area — fill by shape, not by bounding box

The 1px trim is a **hard ceiling, not a target.** Filling every icon's bounding box to 30×30 makes
the set optically *uneven* — a solid square reads much larger than a circle of the same box. Size
each icon so it **looks the same weight as its neighbors**, using these per-shape targets (longest
visual dimension, on the 32 grid):

| Shape class                                    | Target extent | Why                                    |
| ---------------------------------------------- | ------------- | -------------------------------------- |
| Solid / sparse round mark (filled dot, record) | **~30** ⌀     | solid circles read smallest — fill max |
| Ring-enclosed glyph (mark in a circle: status, help, circled-plus) | **~28.5** ⌀ | an OUTLINE ring reads by its diameter, like a container edge — not like a solid circle |
| Magnifier family (lens + handle)               | **~27.5**     | the lens ring dominates; sits next to boxy UI glyphs in lists |
| Pointed / diagonal / sparse (arrow, send, pin) | **~29–30**    | sparse mass — can run near the trim     |
| Default / complex glyph (most icons)           | **~28**       | the balanced middle                     |
| Square / boxy (stop, panel, document)          | **~26**       | squares read heaviest — pull in most    |
| Compact UI mark (chevron, close ×, minus)      | **~16**       | subordinate inline marks sit at half-grid so they don't dominate the text they sit beside |
| Wide rectangle                                 | **30 W**      | fill the long axis, keep ≤28 on short   |
| Tall rectangle                                 | **30 H**      | fill the long axis, keep ≤28 on short   |

Rule of thumb: a **solid** circle should be ~15% larger than a square to look the same size — but
that correction does NOT transfer to outline rings, whose perceived size tracks their diameter
(an over-corrected ring reads *larger* than its boxy neighbors, not equal). **Calibrate in
context:** verify candidates at 16px in a row alongside their real neighbors from the set (e.g. a
sidebar lineup of circled-plus · magnifier · folder · pin), not only on the isolated spec sheet.
These numbers are starting points, not absolutes.

### Required root attributes

```html
<svg
  xmlns="http://www.w3.org/2000/svg"
  viewBox="0 0 32 32"
  width="32"
  height="32"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <!-- paths -->
</svg>
```

Put the shared stroke attributes on the **root `<svg>`** and let child paths inherit them; only
override on a child when a specific shape genuinely needs it (rare).

---

## Drawing rules

1. **Optical centering over mathematical centering.** Center the icon so it _looks_ balanced;
   a visually heavy shape may need to sit slightly off the geometric center.
2. **Fill the optical live area.** Size the icon to its per-shape target (see _Optical live area_
   above), not to the 30×30 ceiling — it should look the same weight as its neighbors. Don't draw a
   tiny icon floating in the middle, and never bleed past the 1px trim (coords ~1–31).
3. **Design for 16px first.** Keep enough gap between parallel strokes (≥ ~2.7 units on the 32 grid,
   i.e. ≥ ~1.3px at 16px) that they don't merge when scaled down. Drop detail that can't survive 16px.
4. **Snap to the grid.** Land key points, centers, and terminals on whole or half units (centre is
   `16,16`). Prefer 45° / 90° angles unless the metaphor needs otherwise.
5. **Fewest elements that communicate.** Every stroke must earn its place. Remove decorative
   detail; literal over-illustration is a failure, not thoroughness.
6. **Reuse base shapes** across the set (rounded rect for documents/bubbles, circle for nodes,
   chevron/arrow for direction) so icons feel like one family.
7. **One clear primary metaphor** per icon, plus at most one modifier (e.g. a base shape + a
   `plus`/`arrow`/`check` affix). Don't stack metaphors.
8. **Distinctiveness over literalism.** The icon must be distinguishable from unrelated common
   shapes (a plain polygon, gear, nut, spinner, ring). If a metaphor collapses to a generic
   primitive — e.g. a bare hexagon for "molecule" — add the minimal distinguishing feature (atom
   nodes and bonds, a label-free cue) that makes it unmistakable. A cold reader should name it
   without the label.
9. **Node-on-a-line: interrupt, don't cross.** Where a node (knob, atom, joint, junction dot) sits
   on a stroke, **break the stroke at the node** — stop the line at the node's edge with a small
   gap — so the node reads as resting _on_ the line, not pierced by it. Split the line into segments
   rather than relying on a background fill (icons are `fill="none"`). Applies to sliders,
   molecule atoms on bonds, flow-graph nodes, and similar.
10. **Even dashed strokes.** A dashed circle/path must have **evenly distributed** dashes — every
    gap equal, with no odd gap where the pattern wraps. A raw `stroke-dasharray` rarely divides a
    circle's circumference exactly, so set **`pathLength="100"`** on the element and use a dash
    pattern that divides 100 into a whole number of segments (e.g. `stroke-dasharray="2.5 7.5"` →
    10 even dashes). Uneven dashes are an Optical Balance failure.
    **Round caps eat the gap.** With our round caps, each dash's cap extends ~½ the stroke (≈1 unit)
    *past* each end, so a gap narrower than ~2 units (the full stroke width) closes into a continuous
    bead-string — it stops reading as dashed. Keep the gap **≥ stroke width + the visible gap you
    want** (so ≈3+ units of real gap). On a **short** path (e.g. a node-to-node connector) the
    `pathLength="100"` percentages map to tiny real units — convert to real units and verify the
    *rendered* gap, don't trust the ratio. (A short connector reads cleanly at roughly
    `stroke-dasharray="16 26"` — ~3 dashes with clear separation — not the tighter `"13 12"`.)
11. **Interior detail and the container edge.** Where an inner stroke meets an enclosing shape, decide
    whether it should connect or stay clear — never leave a round cap floating a hair inside the
    border. (a) If it should **connect** (e.g. a mountain ridge inside an image frame), land its
    endpoint **on the container edge** so the cap is painted over by the frame stroke and disappears.
    (b) If it should stay **separate** (e.g. an exclamation dot inside a warning triangle), give it
    clear breathing room from the edge. A cap nub that floats just inside the border reads as a defect.
12. **Even distribution of repeated marks.** Repeated marks — dashes, ticks, rungs, slider knobs,
    status dots — must be **evenly distributed**. Where fixed features (shape edges, strand ends, line
    crossings) divide the icon into regions, place the marks at the **midpoints or even divisions** of
    those regions, not at arbitrary equal y-steps that ignore the structure. (Even dashes around a
    path are the special case in rule 10; slider knobs use an A/B/A rhythm; DNA rungs sit at the
    region midpoints and trisections between the ribbon ends and the helix crossings.)

---

## Building a set: parity & disparity

When you generate **more than one icon**, the set has to hold together as much as each glyph does.
Before and after drawing, check the icon against its neighbors on two axes:

- **Parity — related icons must match.** Icons that mirror or belong together share construction,
  weight, base shapes, and geometry. A **download** and an **upload** should be the *same* tray with
  the arrow flipped; a **left-panel** and **right-panel** toggle should be mirror images; everything
  in one `grp` (e.g. all chevrons, all file-type badges) should read as one family. If two icons are
  related, build the second by transforming the first, not from scratch.
- **Disparity — different concepts must look different.** An icon must not collide with a
  *different-meaning* icon in the set. This is the trap that catches arrow-family glyphs: a
  **share** (box + up arrow) sitting next to an **upload** (tray + up arrow) reads as the same thing;
  a **molecule** (node graph) next to a **share** (node graph) collides. When a metaphor lands too
  close to another icon's, change the *element that distinguishes the concepts*, not just the size.
- **Latent collisions — check the whole vocabulary, not just this batch.** The worst collisions are
  with common UI glyphs that aren't in the current set: a horizontal **pill** reads as a **toggle**;
  a plain cylinder as a **battery**; a **node graph** as **share**; a **box + diagonal arrow** as
  **open-external**. Before shipping an icon, test it against the broad common-glyph vocabulary
  (toggle, battery, switch, share, network, menu, external-link, checkbox, spinner…), and if it lands
  on one, add the distinguishing feature (a pour spout for the beaker; a diagonal axis for the pill).

Practically: keep a mental (or written) map of the set, note which icons must pair and which must
stay distinct, and treat a collision as a defect to fix — even if the colliding icon is clean on its
own. This is scored as **Set Fit** in the eval rubric.

### State variants hold their silhouette

Paired state variants of the same object — **open/closed folder, locked/unlocked, on/off** — must
keep the **same outer silhouette**: identical footprint, corners, and proportions. Convey the state
with an interior cue (a seam, a swung **flap drawn as a perspective edge inside the outline**, a
changed inner mark) — never by nudging the outer corners or shrinking one variant. A shifted corner
between variants reads as a drawing error, not a state change. (Example: `folder-open` keeps the
closed folder's exact tab and four corners; the opening is a diagonal hinge line inside the shape.
A fully perspective-skewed variant — the object slightly rotated about its vertical axis — is
permitted only when the *whole* icon is drawn in that perspective coherently, per the
object-in-perspective rule; do not mix a flat body with one skewed corner.)

### Designing for context

An icon never appears alone — its label, position, and surrounding affordances supply part of the
meaning. When the context already names the category, **drop the redundant wrapper and lead with the
distinguishing mark.** Example: in a list labeled "Local files," wrapping every glyph in a document
outline is *anti-distinctive* — the document repeats everywhere and the tiny type-mark inside is what
the user actually needs to tell files apart. Stripping the wrapper makes each mark bigger and clearer
(a bare `</>` for code, a bare grid for a spreadsheet). Keep the wrapper only where it *is* the
meaning (a generic/unknown file, a "new file" action). Design the icon for where it will actually live.

### Enclosed (circled) variants

Some glyphs need an **enclosed variant** — the same inner mark inside a circle — alongside the bare
form, for standalone or low-context placements where the bare glyph could be misread. Example: a
bare rounded-square **stop** can read as an empty checkbox; wrapping it in a circle makes it
unmistakably a media-control button.

- Use the **bare** form when surrounding affordances already disambiguate (e.g. a stop that replaces
  a play button in a transport bar).
- Use the **enclosed** form when the icon stands alone with nothing nearby to clarify it.

The enclosed variant keeps the **same inner glyph**, optically centered in a circle sized to the live
area, same 2 stroke. Ship both; don't pick one globally.

---

## Color & accents

Icons are **monochrome** and inherit `currentColor`. Do not paint status or accent colors into the
SVG. If an icon needs to signal state (success, warning, error), the _consumer_ sets `color` using a
token from `color/colors.css` (e.g. `color: var(--fg-secondary)` for a muted icon,
`color: var(--success-fg)` for a success glyph). The SVG itself stays neutral and reusable.

**Sanctioned fill — small solid features only.** Icons are otherwise `fill="none"`, but a **small
solid feature** that can't survive as a stroke at 16px may use a `fill="currentColor"` shape:
- a **point-mark** — an ellipsis dot, a question-mark dot, a record/active dot, a junction node, a
  list bullet. **Draw it _as stroke_ so it tracks the stroke weight at every size.** Use a small
  `<circle fill="currentColor">` that **keeps the inherited stroke** (do **not** add `stroke="none"`)
  with a tiny radius (**r ≈ 0.3**, 32 grid): the inherited 2-unit stroke supplies the body, so the dot
  renders at ≈ the stroke width and — because the stroke scales with the viewBox (1px@16 · 1.5px@24 ·
  2px@32) — the dot scales in lockstep with every other line at all display sizes. (A zero-length
  round-capped stroke — a `<line>`/`<path>` with coincident endpoints — is the equivalent pure-stroke
  form and renders at *exactly* the stroke width; the v2 warning dot uses this.) This is why the
  existing dots (v8 help `?`, v4 pending ellipsis, v8 chat) **omit** `stroke="none"` — keep doing that.
  **Do not** make a stroke-weight dot with `fill="currentColor"` + `stroke="none"` + an explicit
  radius: that decouples it from the stroke, so it stops tracking across sizes **and** the
  `optical-scale.mjs` pass rescales the radius, drifting the weight. Reserve the explicit-radius filled
  form (with `stroke="none"`, up to ~r1.5) **only** for a deliberately **larger** focal mark — a
  bullseye center, a record dot — where the dot *is* artwork and scaling it with the icon is correct;
- a **small inner feature** — e.g. a thermometer's mercury reservoir bulb, or a fill level — when the
  feature would vanish as a thin inner stroke.

Keep the filled element small and monochrome; the outer and structural shapes stay outlined. This is
the **only** sanctioned fill.

---

## Using the shipped set (check here BEFORE drawing a new icon)

The finished icon set ships in this directory:

- **`svg/<name>.svg`** — one file per icon, ready to inline into HTML or paste into a
  presentation. Hosted copies live at `https://design.mirrorphysics.com/design/icons/svg/<name>.svg`.
- **`icons.json`** — the machine-readable manifest. Each entry has `name`, `file`, `functions`
  (what the icon is *for*), and `variants` (directional forms derived by rotation). **To find an
  icon, search `functions` by your intent**, not the name.

Naming follows one test: **does the shape genuinely serve many purposes, or is it locked to one?**

- **Multi-purpose glyphs are named by form** (`arrow-left`, not "back"; `floppy-disk`, not "save";
  `eye-slash`, not "hide"; `chip`; `hexagon`). One primitive serves many purposes — the context you
  place it in supplies the meaning. Search `functions` to find them by intent.
- **Glyphs whose meaning is too established to read as anything else are named semantically**, even
  though that names a purpose: `launch` (not "box-arrow-up-right"), `minimize`/`maximize` (not
  "corners-in/out"), `checkbox`/`checkbox-checked` (not "square"/"square-check"), `copy`. Forcing a
  form name on these helps no one — a reader sees the purpose, not the shape, so the name should too.

Rule of thumb: if you can list three unrelated uses for the bare shape, give it a form name; if you
can't, give it the semantic name. Reserve form names (`chip`, `box`, `square`) for the genuinely
reusable primitives so they stay available for novel uses. Directional variants (chevrons, arrows)
ship as one base glyph; derive the others by rotating about the center `(16,16)` per the manifest's
`rotations` field (e.g. `transform="rotate(90 16 16)"` on a wrapping `<g>`).

The shipped SVGs are **label-free** (no `role`/`aria-label`) — add them per the Accessibility
section below. They are **generated** by `evals/scripts/promote-icons.mjs` from the icon-set eval
pipeline; never edit `svg/` or `icons.json` by hand.

---

## Accessibility

- **Meaningful** icon (conveys info on its own): give it a label.
  ```html
  <svg role="img" aria-label="Export">…</svg>
  ```
- **Decorative** icon (sits next to a text label): hide it from assistive tech.
  ```html
  <svg aria-hidden="true" focusable="false">…</svg>
  ```

---

## Sizing in use

Use the tokens and utilities in `icons.css`: `--icon-size-sm` (16), `--icon-size-md` (20/24),
`--icon-size-lg` (32), and the `.icon` base class (sets `currentColor`, `fill: none`, vertical
alignment). Size an inline icon to the adjacent text with `width: 1em; height: 1em`.

---

## Do / Don't

**Do**

- Author at `viewBox="0 0 32 32"`; keep stroke a uniform 2; round caps and joins; `fill="none"`.
- Use `currentColor`; size to the per-shape optical target inside the 30×30; design for 16px.
- Reuse base shapes; keep one metaphor + at most one modifier.

**Don't**

- No fills, solid shapes, gradients, or more than one color.
- No baked hex/rgb colors; no `vector-effect="non-scaling-stroke"`.
- No off-grid or variable stroke widths; no sub-1px-at-16 detail; no decorative noise.
- Don't bleed past the 1px trim, and don't blanket-fill every box to 30×30 (breaks optical balance).
- No detail that disappears or merges at 16px.

---

## Worked example — `search`

**Good** — circle + handle on the 32 grid, uniform 2 stroke, fills the sparse/diagonal optical
target (~29), reads at 16px:

```html
<svg
  viewBox="0 0 32 32"
  fill="none"
  stroke="currentColor"
  stroke-width="2"
  stroke-linecap="round"
  stroke-linejoin="round"
>
  <circle cx="13.5" cy="13.5" r="10" />
  <line x1="20.6" y1="20.6" x2="28.5" y2="28.5" />
</svg>
```

**Bad** — `fill` baked in, color hard-coded, handle too thin to match, wrong grid: violates the
stroke, color, grid, and consistency rules.

```html
<svg viewBox="0 0 24 24">
  <circle cx="11" cy="11" r="7" fill="#000" />
  <line x1="16" y1="16" x2="21" y2="21" stroke="#333" stroke-width="1" />
</svg>
```
