diff --git a/docs/designs/DECISIONS.md b/docs/designs/DECISIONS.md
index ee3403cb4..d492ab97c 100644
--- a/docs/designs/DECISIONS.md
+++ b/docs/designs/DECISIONS.md
@@ -279,6 +279,7 @@ check enforces the mechanical half. Full rationale:
| DL-208 | Both Solid-2 blocker deps are FORKED into the RigelBuild org and consumed as externalized pinned artifacts, never vendored into the compass tree (matching the org-wide fork-externalization ruling): `@tanstack/solid-virtual` is forked with Solid-2 support + an upstream PR to TanStack/virtual (a 136-line thin wrapper over the framework-agnostic `@tanstack/virtual-core`, likely to land), consumed via a `package.json` git ref (`github:RigelBuild/virtual`) until upstream merges then dropped; `solid-markdown` (effectively abandoned upstream) is forked-and-owned, published as `@rigelbuild/solid-markdown` with the `solid-js` 2.x peer bump + Solid-2 codemods (imports + effect lifecycle, no algorithmic rewrite, no in-repo AST renderer). Overrides this record's earlier adapter-over-core virtualizer recommendation | Active (Matt, 2026-08-18) / solid-markdown clause superseded by DL-218 (Matt, 2026-08-19) | [solid v2 migration §Resolved decisions](ui/compass-ui-solid-v2/design.md#resolved-decisions) |
| DL-209 | The Wails packaged-shell seam under Solid 2 is low-risk-but-verified (no automation): the seam is Solid-free plain TS (`daemon-transport.ts` string-name `Call`/`Events`; `MarkdownText.tsx:1` `Browser.openExternal`), so effect-flush timing is the lesser risk; the packaged shell is the first place the babel-preset-solid 2 / vite-plugin-solid 3 / `@solidjs/web` production-build compilation and v2's render-root-owned delegated events run outside dev/happy-dom, so S7's manual packaged-shell smoke (RPC round-trip + openExternal) stays the mandatory gate — no automation justified for a one-shot migration | Active (Matt, 2026-08-18) | [solid v2 migration §Resolved decisions](ui/compass-ui-solid-v2/design.md#resolved-decisions) |
| DL-218 | The owned `RigelBuild/solid-markdown` fork adopts upstream `andi23rosca/solid-markdown` #44 (the react-markdown-10 API rewrite — a third-party draft, upstream effectively inactive on it) + #45 (the bundle-deps jsdom-compat fix), re-ported to Solid 2 via a fork-local `src/jsx-runtime.ts` `jsx`/`jsxs`/`Fragment` triple over `createComponent`/`merge` + `@solidjs/web` `Dynamic` (solid-jsx dropped as Solid-1-bound), published as a clean `@rigelbuild/solid-markdown@3.0.0` (no `-rigel.N` suffix — the fork is ours and sets its own versions); this retires the fork's in-repo AST renderer in favor of the maintained `hast-util-to-jsx-runtime`. The apps/ui consumer injects @mention chips via a consumer-side rehype plugin over the existing `mentionRuns` splitter (the removed `text` component override), derives inline-vs-block code structurally via a `pre`/`code` override split (the removed `inline` prop), keeps `renderingStrategy="reconcile"` (un-deprecated in the owned fork), and adds a synchronous consumer-side `(lang, code)` highlight cache that `CodeBlock` reads at initial render (seeding highlighted HTML and bypassing the debounce + async `createResource` on a hit) to suppress the code-highlight flash caused by #44's per-tick component teardown (accepting marginal within-stream selection loss; the fine-grained-walker re-port is the escalation if it bites). Supersedes DL-208's `solid-markdown` clause ONLY (no-algorithmic-rewrite/no-in-repo-AST-renderer) — DL-208's `@tanstack/solid-virtual` clause and its org-wide fork-externalization stance stay Active | Active (Matt, 2026-08-19) | [markdown react10 §Approach](ui/compass-ui-markdown-react10/design.md#approach) |
+| DL-367 | Compass UI chrome glyphs are dot-matrix `crispEdges` SVG on an 11×11 grid, adopting the technique DL-150/DL-199 froze at 9×9 for `StateDot`/`BadgeGlyph` (which stay 9×9 — two grid sizes coexist, the components are separate). `ActivityBarItem.icon: string` is split AT THE ITEM into `GlyphTabItem { kind: "glyph"; name: GlyphName }` and `AvatarTabItem { kind: "avatar"; letter: string }` — the field conflated a fixed symbol with an agent handle's initial, and a field-level union was rejected because it preserves exactly that confusion. The avatar initial is NFKD-normalized and combining-marks-stripped before an ASCII clamp (`É`→`E`), falling back to `?` only for scripts no Latin letter represents — a bare clamp would collapse distinct non-ASCII handles to identical `?` tabs, defeating the per-agent distinguishability the initial exists for. 11×11 does NOT resolve the pre-existing 15px slot violation (that rule governs Departure Mono font sizes via UPM, not SVG boxes); the real hazard is half-pixel placement in the 34px tab, so the glyph box is pinned to a whole-pixel offset. Conversion is wholesale — every chrome symbol becomes a ``, including ones Space Mono covers — accepting baseline churn for one vocabulary. This is the path that retires the ~14 MB Unifont pin, in its own stacked PR after the glyph lane recaptures baselines | Active (Matt, 2026-09-12) | [glyph primitives](ui/compass-glyph-primitives/design.md) |
## Threading & rendering
diff --git a/docs/designs/ui/compass-glyph-primitives/design.md b/docs/designs/ui/compass-glyph-primitives/design.md
new file mode 100644
index 000000000..33dceb2ba
--- /dev/null
+++ b/docs/designs/ui/compass-glyph-primitives/design.md
@@ -0,0 +1,498 @@
+# Compass glyph primitives (RIG-3603)
+
+Status: Active
+Ledger: DL-367
+Owner lane: compass-ux (design) → compass-ui (execution)
+Refs: RIG-3603. Adopts the technique frozen by DL-150 (state dot: 9×9
+`crispEdges` 1-bit SVG) and DL-199 (badge: pixel-art 1-bit glyph + 2-char mono
+axis code). All code references are to main `97741c5ce2f4`.
+
+## Problem / Intent
+
+UI chrome renders ~23 non-ASCII glyphs (`▦`, `🗀`, `⎇`, `⇄`, `▸`, `◆`, `★`, …)
+that neither brand face (Departure Mono, Space Mono) covers, so the e2e font
+stack pins a ~14 MB Unifont fallback (`tools/toolchain/chromium-e2e-env.nix`)
+and end users get whatever their OS substitutes. Separately,
+`ActivityBarItem.icon: string` conflates two different primitives — a fixed
+chrome symbol and a person's initial — behind one string field. This record
+replaces chrome glyphs with dot-matrix `crispEdges` SVG per DL-150/DL-199,
+splits the item type at the item per Matt's frozen ruling, and states the
+conditions under which the Unifont pin can be retired.
+
+## Global Constraints
+
+- **Solid v2 (`solid-js@2.0.0-rc.1`), TS strict, Biome (tabs).** Props are
+ never destructured in components.
+- **Technique is frozen, not chosen here.** DL-150/DL-199 already ruled: 1-bit
+ whole-cell grids, `shape-rendering="crispEdges"`, one `` per lit cell, filled with `currentColor` so tier colors keep
+ applying through the wrapper (`StateDot.tsx`, `BadgeGlyph.tsx` are the
+ shipped exemplars).
+- **Night Owl palette via `--cx-*` tokens only.** No raw hex; glyphs carry no
+ color of their own — `currentColor` end to end.
+- **The even-11px-multiple rule governs Departure Mono font sizes, not SVG
+ boxes** (`tokens.css:51-57` — it is about UPM font-pixels). The glyph grid
+ therefore neither inherits nor fixes the pre-existing 15px `.r-tab-icon`
+ violation (`app.css` `.r-tab .r-tab-icon`). What the grid MUST satisfy is
+ whole-pixel placement inside the 34px `.r-tab`, so its 1px cells do not
+ straddle device pixels.
+- **Item-level union, per Matt's frozen ruling.** `ActivityBarItem` splits AT
+ THE ITEM: `{ kind: "glyph"; … }` vs `{ kind: "avatar"; … }`. A field-level
+ union (one `icon` field with two shapes) was explicitly rejected. Both the
+ refactor and the glyph work land in RIG-3603.
+- **Out of scope for SVG conversion:** the 4 comment-only glyphs (`─` U+2500,
+ `≙` U+2259, `⌘` U+2318, `═` U+2550); math/punctuation glyphs in comments and
+ test names (`§`, `⇒`, `≥`, …), which never render.
+- **The `stub-data.ts` fixture log glyphs are not converted, but they are not
+ free either.** `➜` (U+279C) and `▪▪▪▪` render through `.term-body`, which
+ uses `var(--cx-font-ui)` (`app.css:1273`) — there is NO separate terminal
+ font stack — and `agent.png` captures that exact fixture
+ (`AGENT_ID = "acc-compass-ui"`). So after the pin is removed they would
+ rasterize as tofu. They stay text, but T8 must first replace the two fixture
+ strings with ASCII equivalents (`->`, `####`); the module hand-fakes a
+ representative fleet, so the strings are not a contract.
+- **Unifont-pin removal is a separate stacked PR**, and it is NOT the only
+ recapture. T4/T5 change pixels inside existing baselines, so the glyph lane
+ recaptures at T6; T8 then recaptures again against the shrunk font set.
+- Markdownlint-clean record; Simple Technical English.
+
+## Approach
+
+### The `` primitive
+
+Adopt the DL-150/DL-199 technique as-is. A new
+`apps/ui/src/components/Glyph.tsx` renders a fixed **11×11** 1-bit grid at one
+CSS px per cell:
+
+```tsx
+
+```
+
+11×11 (not StateDot's 9×9) for two reasons: an odd grid has a true center cell,
+matching the 9×9 exemplars, and pictographs (a folder, a branch, a gear) need
+more cells than a state dot.
+
+This does **not** reconcile the pre-existing 15px slot violation — that rule
+governs Departure Mono font sizes via UPM font-pixels, and an SVG box has no
+UPM (D2). The real hazard is placement: an 11px box flex-centered in the 34px
+`.r-tab` lands on a half-pixel offset, smearing every 1px cell across two
+device pixels, so the box is pinned to a whole-pixel offset (T3+T4).
+
+Bitmap storage follows `BadgeGlyph.tsx` exactly: a module-level
+`GLYPH_CELLS: Record>`
+of `[x, y]` lit cells, transcribed from frozen ASCII grids added to
+`design/components.md` §Glyphs (`#` = lit). Keying the table on the exhaustive
+`GlyphName` union makes a name without a bitmap a compile error, not a runtime
+blank — the same guard `BadgeGlyph`'s `GlyphKey` provides.
+
+`GlyphName` starts as the closed set of the four static tabs —
+`"status" | "files" | "vcs" | "pr"` — and grows semantic names (never
+character names) as the chrome audit (T5) converts further sites.
+
+### The `ActivityBarItem` split (Matt's frozen ruling)
+
+The union splits **at the item**, in `apps/ui/src/constants.ts`:
+
+```ts
+interface ActivityBarItemBase {
+ title: string;
+ group: RightTabGroup;
+}
+
+/** A static tab: a fixed chrome symbol from the closed glyph set. */
+export interface GlyphTabItem extends ActivityBarItemBase {
+ kind: "glyph";
+ id: StaticRightTab;
+ name: GlyphName;
+}
+
+/** A fleet agent tab: a person's initial, derived once from the handle. */
+export interface AvatarTabItem extends ActivityBarItemBase {
+ kind: "avatar";
+ id: `agent:${string}`;
+ letter: string;
+ group: "fleet";
+ agentId: string;
+ unreachable?: boolean;
+}
+
+export type ActivityBarItem = GlyphTabItem | AvatarTabItem;
+```
+
+The split also moves `agentId`/`unreachable` onto the avatar arm only — today
+they are optional on every item, but only fleet agent tabs ever carry them
+(`RightSidebar.tsx` `FleetPane`/`AgentUnreachable` and the `StateDot` badge all
+key off `agentId`). `FleetPane`, `AgentUnreachable`, and `activeFleetItem`
+narrow from `ActivityBarItem` to `AvatarTabItem`, deleting their
+`agentId ? … : undefined` dances.
+
+The avatar arm owns the initial derivation **once**. Both
+`fleetItemForAgent` and `unreachableFleetItem` currently duplicate
+`(handle.at(0) ?? "?").toUpperCase()`, which splits a surrogate pair and can
+expand to multiple characters for some scripts. A single helper replaces both:
+
+```ts
+/** First grapheme of the handle, NFKD-normalized with combining marks
+ * stripped (É→E), uppercased, then clamped to one printable ASCII
+ * character; anything else degrades to "?" (see D1). */
+export function avatarInitial(handle: string): string;
+```
+
+Implementation, in order: `Array.from(handle)[0]` (code-point safe, not
+`.at(0)`, which splits a surrogate pair) → `.normalize("NFKD")` and drop
+combining marks → `.toUpperCase()` → return `"?"` unless the result is exactly
+one character in the printable-ASCII range.
+
+The NFKD step is what keeps accented-Latin handles distinguishable (`Émile`→`E`
+rather than `?`); the clamp is what lets the Unifont pin retire. Both are D1.
+
+### The render site and the CSS split
+
+The single render site (`RightSidebar.tsx`, the `{tab.icon}` span inside the
+`.r-tab` button) branches on the discriminant. `tab` is a plain `For` item, so
+a ternary narrows cleanly:
+
+```tsx
+
+ {tab.kind === "glyph" ? : tab.letter}
+
+```
+
+The shared `.r-tab .r-tab-icon { font-size: 15px }` rule splits by `data-kind`:
+
+- `.r-tab .r-tab-icon[data-kind="glyph"]` — an 11×11 box (`display: inline-flex`,
+ no font properties; the SVG is the content), centered in the unchanged
+ `.r-tab` button.
+- `.r-tab .r-tab-icon[data-kind="avatar"]` — text, DS mono face at 15px
+ (Space Mono is legal at ≤16px), `line-height: 1` as today.
+
+The `StateDot` sibling and its absolute bottom-right badge CSS are untouched.
+
+### Chrome glyph audit
+
+Beyond the activity bar, rendered chrome glyphs (`▸` disclosure, `◆`/`★`/`☆`
+markers, `✓`/`✗` status, `⚙` settings, `⊞` grid, `✕` close, `⟨⟩` framing, and
+the rest of the ~23-site census) convert to `` inline at their
+sites, extending `GlyphName` per adopted symbol (T5). Fixture log content in
+`stub-data.ts` and comment-only glyphs stay untouched (Global Constraints).
+Whether typographic marks that a brand face already covers (e.g. `−`, `•`) may
+stay as text is settled by D3.
+
+### Unifont retirement conditions
+
+The pin (`tools/toolchain/chromium-e2e-env.nix`, fontDirs: unifont +
+unifont_upper) can be removed only when **no rendered character falls outside
+brand-face coverage**, which requires ALL of:
+
+1. Every rendered chrome glyph converted or verified covered (T5 audit).
+2. The avatar arm clamped to printable ASCII (`avatarInitial`, D1) — handles
+ are charset-unconstrained (`agent.proto` `from_handle`, no schema
+ validation), so without the clamp an arbitrary initial re-opens the gap.
+3. Fixture log glyphs (`➜`, `▪▪▪▪`) replaced with ASCII in `stub-data.ts`
+ (T8 step 1). They are content, not chrome, so they are never converted to
+ SVG — but they are NOT exempt from coverage: `.term-body` renders through
+ `var(--cx-font-ui)` (`app.css:1273`), the same stack as everything else,
+ and `agent.png` captures them. There is no terminal font stack to fall back
+ on.
+
+Removal changes the fontconfig nix store path and invalidates all 11 visual
+baselines in `apps/ui/e2e/__screens__/`, so it lands as its own stacked PR
+with its own recapture (T8) — never mixed into a glyph slice. Note this is the
+SECOND recapture: T4/T5 already move pixels inside the same baselines, which
+T6 recaptures first.
+
+## Alternatives considered
+
+- **Field-level union (keep one `icon` field, vary its type).** REJECTED by
+ Matt, explicitly and finally. A field union leaves every consumer testing the
+ field's shape instead of the item's kind, keeps `agentId`/`unreachable`
+ optional on items that can never carry them, and gives the avatar arm no
+ natural owner for the initial derivation. Do not re-litigate.
+- **Keep the Unifont pin permanently.** Rejected: ~14 MB of toolchain weight
+ to paper over a small closed set of chrome symbols, and it only fixes the
+ e2e environment — end-user machines never had the pin, so real browsers
+ already render these glyphs from arbitrary OS fallback fonts, off-brand and
+ inconsistent. The pin hides the problem from the one environment that could
+ catch it.
+- **A webfont/icon-font of custom glyphs.** Rejected: re-introduces font
+ loading, hinting, and anti-aliasing where DL-150/DL-199 already shipped a
+ crisper, zero-asset answer; a font also cannot guarantee whole-cell 1-bit
+ rendering at exactly one CSS px per cell.
+- **9×9 grid matching StateDot.** Rejected for chrome glyphs: it has no
+ center-cell advantage over 11×11 (both are odd), and it gives pictographs —
+ gear, folder, branch — too few cells to read at tab size. The baseline
+ argument does NOT apply: neither 9 nor 11 resolves the 15px slot violation,
+ because that rule governs Departure Mono font sizes, not SVG boxes (D2).
+
+## Plan
+
+Tasks in dependency order. T1–T7 are one PR lane (RIG-3603); T8 is a separate
+stacked PR.
+
+### T1 — `` primitive and bitmap data
+
+Add `apps/ui/src/components/Glyph.tsx`: the `GlyphName` union, the
+`GLYPH_CELLS` table for the four static-tab glyphs (status grid, files folder,
+vcs branch, pr arrows), and the component rendering the 11×11 `crispEdges`
+SVG on `currentColor`, following `BadgeGlyph.tsx`'s shape. Transcribe the four
+frozen ASCII grids into `design/components.md` §Glyphs; `GLYPH_CELLS` cites
+them. Unit-test that every `GlyphName` key yields a non-empty cell list and
+all cells lie in `0..10`.
+
+Interfaces:
+
+```ts
+export type GlyphName = "status" | "files" | "vcs" | "pr";
+export const Glyph: Component<{ name: GlyphName }>;
+```
+
+### T2 — `avatarInitial` helper
+
+In `apps/ui/src/constants.ts`: the single owner of initial derivation, today
+duplicated at `:137` and `:154`. Take `Array.from(handle)[0]` (never `.at(0)`,
+which splits a surrogate pair), NFKD-normalize it and strip combining marks so
+`É`→`E`, uppercase, then clamp to one printable ASCII character, else `"?"`
+(D1).
+Unit-test: plain handle, empty string, surrogate-pair-leading handle (e.g. an
+emoji → `?`), a lowercase handle, an accented Latin handle (`Émile` → `E`), a
+non-Latin script (`Живко` → `?`), and a script whose uppercase expands to
+multiple characters.
+
+Interfaces:
+
+```ts
+export function avatarInitial(handle: string): string;
+```
+
+### T3+T4 — `ActivityBarItem` union, constructors, render site and CSS (ONE commit)
+
+**These cannot be separate commits.** `GlyphTabItem` has neither `icon` nor
+`agentId`, and the activity-bar loop reads BOTH on the un-narrowed union:
+`{tab.icon}` at `RightSidebar.tsx:689` and
+`tab.agentId ? store.agentById(tab.agentId) : undefined` at `:662`. Landing the
+union without the render branch is a TS-strict error, so a split leaves a red
+commit mid-stack and breaks bisection.
+
+In `apps/ui/src/constants.ts`: replace the single interface with the
+`GlyphTabItem | AvatarTabItem` union (shapes as frozen in `## Approach`).
+`RIGHT_SIDEBAR_TAB_BY_ID` entries become `kind: "glyph"` with `name` replacing
+`icon`; `fleetItemForAgent`/`unreachableFleetItem` return `AvatarTabItem` with
+`letter: avatarInitial(…)`. Narrow `FleetPane`, `AgentUnreachable`, and
+`activeFleetItem` in `RightSidebar.tsx` and the `rightTabGroups` memo typing in
+`store.ts` to the arm they actually handle.
+
+At the render site: the `.r-tab-icon` span gains `data-kind={tab.kind}` and
+branches `tab.kind === "glyph" ? : tab.letter`; the
+`:662` agent lookup moves behind a `tab.kind === "avatar"` guard. In `app.css`
+split `.r-tab .r-tab-icon` into the `[data-kind="glyph"]` box (no font
+properties) and the `[data-kind="avatar"]` 15px mono rule; `.r-tab
+.cx-state-dot` untouched. The glyph box MUST sit at a whole-pixel offset inside
+the 34px `.r-tab` — an 11px box flex-centered there lands on a half pixel and
+smears every 1px cell across two device pixels (see D2).
+
+Unit-test both constructors: a resolvable agent yields `kind: "avatar"` with no
+`unreachable`; a pin yields `unreachable: true`; both route the initial through
+`avatarInitial`. Component-test BOTH arms and the whole-pixel offset — no test
+covers this surface today.
+
+Interfaces:
+
+```ts
+export interface GlyphTabItem {
+ kind: "glyph";
+ id: StaticRightTab;
+ name: GlyphName;
+ title: string;
+ group: RightTabGroup;
+}
+export interface AvatarTabItem {
+ kind: "avatar";
+ id: `agent:${string}`;
+ letter: string;
+ title: string;
+ group: "fleet";
+ agentId: string;
+ unreachable?: boolean;
+}
+export type ActivityBarItem = GlyphTabItem | AvatarTabItem;
+export function fleetItemForAgent(agent: Agent): AvatarTabItem;
+export function unreachableFleetItem(pin: PinnedAgent): AvatarTabItem;
+```
+
+```tsx
+// RightSidebar.tsx render-site contract
+
+ {tab.kind === "glyph" ? : tab.letter}
+
+```
+
+Component tests (the missing regression net): a static tab renders an SVG with
+`shape-rendering="crispEdges"` and no text; a fleet tab renders the initial as
+text plus its `StateDot`; an unreachable tab renders no `StateDot`.
+
+### T5 — chrome glyph audit and conversion (THREE commits)
+
+Sweep the rendered non-ASCII chrome sites (census at main `97741c5ce2f4`:
+`LeftSidebar.tsx` `◆ ★ ☆ ▸ ▼ ◉ ○ ▦ ▤ ✓ ⚙`, `RightSidebar.tsx`
+`▸ ▾ 🗀 ⎇ ✓ ✗ • − ·`, `App.tsx` `◇ ▦ ▐ ▌`, `AgentView.tsx` `⊞ ▁ ▏ ✕`,
+`UsageBar.tsx` `⎇`, `LogPanel.tsx` `■ ⟨⟩`, `SessionTrace.tsx` `↗`,
+`BacklogView.tsx` `▸`, `IssueCard.tsx`/`DoneView.tsx` `−`). The `·` in
+`RightSidebar.tsx:32` `FILE_ICON` is part of the census — Space Mono covers
+it, so it is a "kept, covered" row, not a conversion.
+
+**Split into three commits by surface**, each independently eyeball-able:
+T5a `LeftSidebar` + `App`; T5b `RightSidebar` + `AgentView`; T5c the rest.
+Pixel-art authorship for ~15 bitmaps is judgment work, and one review of 10
+files × 15 grids is past the size where a reviewer checks each grid against
+its rendering. The audit table accretes per commit.
+
+Per site: convert to `` with a new semantic `GlyphName` +
+bitmap, or record it in the audit table as covered-by-brand-face/text-kept
+(per D3: convert wholesale). Extend the T1 cell-validity test automatically via the
+keyed table.
+
+The audit table carries an **a11y column** per site: *decorative*
+(`aria-hidden`, no change) or *name-bearing* (the site must gain or keep a
+text/`aria-label` name). Without it each conversion silently decides whether a
+glyph carried meaning — e.g. `LogPanel.tsx` `■ stop` keeps its text name, but
+bare `✓`/`✗` verdict marks read as nothing once hidden.
+
+Its completeness criterion is checkable, not trusted: **every match of a
+non-ASCII sweep over `apps/ui/src` at the pinned rev** appears as a row.
+
+Deliverable includes the audit table appended as a section in this record, not
+a new file elsewhere.
+
+Interfaces:
+
+```ts
+// GlyphName grows per adopted symbol; names are semantic ("disclosure",
+// "check"), never character names ("triangle-right"). Example after T5:
+export type GlyphName =
+ | "status" | "files" | "vcs" | "pr"
+ | "disclosure" | "disclosure-open" | "check" | "cross" | "close"
+ | "gear" | "star" | "star-outline"; // …exact set fixed by the audit table
+```
+
+### T6 — baseline recapture for the glyph conversion
+
+T3+T4 and T5 change rendered pixels inside committed baselines:
+`right-sidebar.png` clips `aside.right`, which holds the activity bar T3+T4
+rewrites, and the seven `fullPage: true` captures (`bridge`, `bridge-empty`,
+`agent`, `backlog`, `done`, `settings`, `bridge-prs`) hold the LeftSidebar and
+App glyphs T5 converts. So this lane must recapture before it can go green;
+without this task the T1–T5 PR fails the visual gate with no step that fixes
+it.
+
+Recapture via the CI regen lane (`ci.yml` `workflow_dispatch`, `regen: visual`),
+then eyeball every diff: only glyph cells may move. `state-dot.png` is the
+positive control and MUST NOT change — it is textless, so any diff there means
+the conversion leaked into unrelated rendering.
+
+Interfaces:
+
+```text
+apps/ui/e2e/__screens__/ — baselines recaptured for the glyph conversion
+state-dot.png — positive control, byte-identical before and after
+```
+
+### T7 — docs
+
+Update `design/components.md` §Glyphs with the full frozen grid set and the
+two-arm activity-bar item contract; note the DL-367 adoption in the section
+header. No changelog beyond the record.
+
+Interfaces:
+
+```text
+design/components.md §Glyphs — frozen ASCII grids, one per GlyphName ("#" = lit)
+```
+
+### T8 — Unifont pin removal + baseline recapture (SEPARATE STACKED PR)
+
+Gated on T6 complete; D1's clamp is what makes it possible. Three steps, in order:
+
+1. Replace the `stub-data.ts` fixture log glyphs with ASCII (`➜` → `->` at
+ :525-526, `▪▪▪▪` → `####` at :535-537 and :722). Without this the recapture
+ bakes tofu into `agent.png`, because `.term-body` has no separate font stack.
+2. Remove `unifont` and `unifont_upper` from
+ `tools/toolchain/chromium-e2e-env.nix` fontDirs.
+3. Recapture all 11 baselines; eyeball each diff for tofu/substitution before
+ accepting.
+
+The PR carries nothing beyond these, so a rendering regression bisects to the
+font change alone — relative to the post-T6 baselines, not to today's.
+
+Before opening it, re-run the census: no rendered character may fall outside
+SpaceMono + departure-mono coverage. That is the real retirement condition; T6
+and D1 are necessary, not sufficient.
+
+Interfaces:
+
+```text
+apps/ui/src/stub-data.ts — fixture log strings ASCII-only
+tools/toolchain/chromium-e2e-env.nix — fontDirs shrinks to SpaceMono + departure-mono
+apps/ui/e2e/__screens__/ — 11 baselines recaptured against the shrunk font set
+```
+
+## Tasks
+
+- [ ] T1: `` primitive + 4 static-tab bitmaps + `components.md` grids + cell-validity test
+- [ ] T2: `avatarInitial` helper + unit tests (surrogate pair, empty, accented Latin, non-Latin, multi-char uppercase)
+- [ ] T3+T4 (ONE commit): `ActivityBarItem` → `GlyphTabItem | AvatarTabItem` union, both constructors, the render branch for `{tab.icon}` (`:689`) AND `tab.agentId` (`:662`), the `.r-tab-icon` CSS split with a whole-pixel glyph box, and tests for BOTH arms
+- [ ] T5a: chrome glyph conversion — `LeftSidebar` + `App` (audit table rows, a11y column)
+- [ ] T5b: chrome glyph conversion — `RightSidebar` + `AgentView`
+- [ ] T5c: chrome glyph conversion — remaining surfaces; audit table complete against a non-ASCII sweep
+- [ ] T6: baseline recapture for the glyph conversion (`state-dot.png` unchanged)
+- [ ] T7: `design/components.md` §Glyphs update (DL-367)
+- [ ] T8: Unifont pin removal + 11-baseline recapture — SEPARATE stacked PR, gated on T6 + D1
+
+## Resolved decisions
+
+- **D1 — avatar fallback: NFKD-normalize, then clamp to ASCII.** Ruled by Matt.
+ `avatarInitial` takes the first grapheme, NFKD-normalizes it and strips
+ combining marks (`É`→`E`, `Ó`→`O`), uppercases, then clamps to one printable
+ ASCII character, falling back to `?` only for scripts no Latin letter
+ represents (CJK, Cyrillic, emoji). Handles are charset-unconstrained (proto
+ `from_handle`, no schema validation), and the initial exists to tell agents
+ apart (`constants.ts:127-130`: "a per-agent glyph, no hardcoded Supervisor
+ ◆") — a bare clamp would collapse distinct non-ASCII handles to identical `?`
+ tabs. A font fallback was rejected: it keeps the pin forever and makes T8
+ impossible. The tab's `title`/`aria-label` carries the full handle in every
+ case. **This is what lets the Unifont pin retire.**
+- **D2 — glyph cell grid: 11×11.** Ruled by Matt. An odd grid gives a true
+ center cell like the 9×9 exemplars, and the extra resolution matters for
+ pictographs (gear, folder, branch). StateDot and BadgeGlyph stay frozen at
+ 9×9 per DL-150/DL-199; they are physically separate components, so two grid
+ sizes coexisting is fine.
+ 11×11 does **not** resolve the pre-existing 15px slot violation — the
+ even-11px-multiple rule (`tokens.css:51-57`) governs Departure Mono font
+ sizes via UPM font-pixels, and an SVG box has no UPM. An earlier draft of
+ this record claimed it did and was wrong.
+ The real hazard is placement: an 11px box flex-centered in the 34px `.r-tab`
+ lands on a half-pixel offset, smearing every 1px `crispEdges` rect across two
+ device pixels. **T3+T4 MUST pin the glyph box to a whole-pixel offset and
+ assert it.** A 12×12 grid would center integrally for free but loses the
+ center cell; rejected on that trade.
+- **D3 — audit breadth: convert wholesale.** Ruled by Matt. Every chrome symbol
+ becomes a ``, including ones Space Mono already covers. One
+ vocabulary is cheaper to hold than a coverage table, and a per-character
+ check against two font files is fragile and invisible in review. The cost is
+ accepted: converting covered glyphs adds baseline churn without adding
+ coverage. Typographic marks inside prose (`−` in a diff stat, `•` as a
+ separator, `·` in `FILE_ICON`) stay text where the brand face covers them —
+ the audit table records each keep with its reason.
+- **D4 — avatar slot type size: keep 15px.** Non-load-bearing, deferred by
+ design. The avatar arm keeps 15px mono; a later pass may move it to 16px
+ (still ≤16px Space Mono) or an 11px-multiple display treatment. This record
+ changes the avatar arm's type, not its look.