diff --git a/specs/glyph-protocol.md b/specs/glyph-protocol.md
new file mode 100644
index 0000000000..a582360688
--- /dev/null
+++ b/specs/glyph-protocol.md
@@ -0,0 +1,811 @@
+# Glyph Protocol
+
+**Author:** Raphael Amorim
+**Year:** 2026
+**Last updated:** 2026-05-03
+
+**See also:**
+- Blog post introducing the protocol and its rationale:
+
+- Reference implementation: [Rio terminal](https://raphamorim.io/rio)
+- Example apps (ratatui, bubbletea v2, ink) registering real Nerd Font
+ outlines and colrv1/colrv0 emojis at empty PUA-B slots:
+ [glyph-protocol-examples](https://github.com/raphamorim/glyph-protocol-examples)
+
+---
+
+## Abstract
+
+Glyph Protocol is a terminal protocol that lets applications ship
+custom vector glyphs to the terminal at runtime — monochrome via
+OpenType `glyf`, or full-colour via OpenType `COLR` v0 (flat layered
+colour) and `COLR` v1 (paint graph with gradients, transforms, and
+composites) — without requiring the user to install a patched font
+(Nerd Fonts, Powerline, etc.). Registrations are restricted to the
+Unicode Private Use Areas — ranges the user never types and existing
+text never contains — so the protocol cannot be used to modify the
+appearance of real text.
+
+The protocol is transported over APC (Application Program Command)
+sequences. The default payload is the OpenType `glyf` simple-glyph
+record for monochrome icons; colour icons ride OpenType `COLR` v0
+(layered flat colour) or `COLR` v1 (full paint graph). Four verbs
+are defined: support-negotiation (`s`), query (`q`), register
+(`r`), and clear (`c`).
+
+## 1. Motivation
+
+Terminal applications today rely on out-of-band font distribution
+to render non-ASCII iconography. The dominant workflow is:
+
+1. Application author picks codepoints in the Unicode Private Use
+ Area.
+2. User installs a multi-megabyte patched font that maps those
+ codepoints to glyphs.
+3. User switches their terminal's font to the patched font.
+4. Application emits the codepoint and hopes the mapping is correct.
+
+This workflow has three structural problems:
+
+- **Distribution cost.** Users carry megabytes of glyphs they
+ never see.
+- **Coupling.** Adding a new icon requires the entire font
+ ecosystem to update. Application authors are locked into a fixed
+ PUA allocation.
+- **Invisible failure.** An application cannot tell whether a
+ given codepoint will render; it can only emit it and accept the
+ result.
+
+Glyph Protocol moves glyph ownership from the font file to the
+application, and gives applications a way to ask the terminal what
+it can render before it renders it.
+
+## 2. Design goals
+
+- **Small surface.** Few verbs, three payload formats (one
+ required, two optional), no daemons, no caches, no cross-session
+ state.
+- **Zero new terminal dependencies.** Every terminal that renders
+ OpenType text already links a `glyf` rasterizer; terminals that
+ render Apple / Google colour emoji also already parse `COLR` +
+ `CPAL`. No new format support is required — the protocol reuses
+ the tables the font stack already decodes.
+- **Resolution independent.** Glyphs are vector and scale to any
+ cell size.
+- **Graceful degradation.** Terminals that do not implement the
+ protocol ignore the APC message. Applications detect support by
+ sending a query and watching for a reply.
+- **No override of user text.** Registrations are confined to PUA
+ codepoints — ranges no user types and no pre-existing text
+ contains. The rendered appearance of `a`, `ssh`, or any URL
+ cannot be changed by any program that writes to the terminal.
+ See §9.
+- **Small on the wire.** Typical icons are 50–400 bytes of
+ `glyf`, 2–4× smaller than the equivalent SVG.
+
+## 3. Transport
+
+Glyph Protocol uses APC (Application Program Command,
+`ESC _ ... ESC \`). APC is specified for application-defined
+commands; terminals that do not implement a particular APC command
+are required to ignore it, making APC safer than OSC for
+introducing new protocols.
+
+### 3.1 Identifier
+
+Every Glyph Protocol message begins with the Unicode codepoint
+**U+25A1** (WHITE SQUARE), written in the message as the
+lowercase hex string `25a1`. Terminals MUST ignore any APC
+message whose body does not begin with this identifier.
+
+### 3.2 Framing
+
+The general form of a Glyph Protocol message is:
+
+```
+ESC _ 25a1 ; [ ; key=value ]* [ ; ] ESC \
+```
+
+Parameter keys use lowercase ASCII. Values are lowercase hex for
+codepoints, decimal for integers, base64 for binary payloads, and
+lowercase ASCII names for enums (single value, or comma-separated
+for lists). The `status` field's shape is verb-specific: `r` and
+`c` return a `u8` (`0` = success, nonzero paired with `reason=`);
+`q` returns a comma-separated coverage list (§5.2).
+
+### 3.3 Verbs
+
+| Verb | Meaning |
+|------|---------|
+| `s` | Advertise supported payload formats. Doubles as a protocol-detection ping — any reply confirms Glyph Protocol; a timeout means unsupported. |
+| `q` | Query the state of a codepoint. |
+| `r` | Register a glyph for a PUA codepoint. |
+| `c` | Clear one slot or every slot in this session's glossary. |
+
+The `s` verb takes no parameters. The reply lists the terminal's
+supported payload formats in the `fmt` key as a comma-separated
+list of lowercase ASCII names, with no surrounding whitespace:
+
+```
+ESC _ 25a1 ; s ; fmt=glyf,colrv0,colrv1 ESC \
+```
+
+Format names defined in v1.8:
+
+| Name | Format |
+|----------|----------------------------------------------------------|
+| `glyf` | Monochrome simple-glyph (§8). |
+| `colrv0` | Layered flat colour, OpenType `COLR` v0 + `CPAL` (§8.6). |
+| `colrv1` | OpenType paint graph, `COLR` v1 (§8.7). |
+
+Order is not significant; clients MUST treat the value as a set.
+An empty `fmt=` value means the terminal recognises Glyph Protocol
+but currently advertises no payload formats — every `r` will be
+rejected. Clients MUST ignore names they do not recognise rather
+than failing the reply, so future format names are forward-
+compatible. A terminal that advertises only `fmt=glyf` and
+receives an `r` with `fmt=colrv0` / `fmt=colrv1` MUST reject the
+registration (`reason=malformed_payload` is acceptable for
+parser-level rejection). Clients SHOULD check the `s` reply
+before emitting colour registrations so they can fall back to a
+monochrome `fmt=glyf` without making a doomed round-trip.
+
+## 4. Glossary namespace
+
+Registrations target codepoints the application picks, constrained
+to the three Unicode Private Use Areas:
+
+| Range | Plane | Common use |
+|------------------------------|-------|------------|
+| `U+E000`–`U+F8FF` | BMP | Basic PUA. Nerd Fonts, Powerline, Font Awesome all live here. |
+| `U+F0000`–`U+FFFFD` | 15 | Supplementary PUA-A. Nerd Fonts v3 Material icons live here. |
+| `U+100000`–`U+10FFFD` | 16 | Supplementary PUA-B. No common convention — clean space for apps that want it. |
+
+Any other codepoint — ASCII, Latin-1, CJK, emoji, control chars —
+is rejected by `r` and `c` with `reason=out_of_namespace`.
+
+Each terminal session holds at most **1024 simultaneous
+registrations**. One codepoint = one slot regardless of payload
+format: a `fmt=colrv1` registration with 500 inner outlines still
+consumes exactly one glossary slot. When the glossary is full and
+a new `r` arrives, the terminal evicts the oldest registration in
+FIFO order; the new registration succeeds. Applications that
+cannot tolerate silent eviction SHOULD query their codepoint with
+`q` before emitting.
+
+Each terminal session (tab, pane, PTY) owns its own glossary. Two
+sessions can independently register `U+E0A0`, each pointing at a
+different glyph. Registrations MUST NOT leak between sessions.
+
+## 5. Query (`q`)
+
+### 5.1 Request
+
+```
+ESC _ 25a1 ; q ; cp= ESC \
+```
+
+Parameters:
+
+- `cp` — codepoint in hex. Any valid Unicode scalar value (not a
+ surrogate). May be inside or outside PUA.
+
+### 5.2 Response
+
+```
+ESC _ 25a1 ; q ; cp= ; status= ESC \
+```
+
+`status` is a comma-separated list of coverage names — the set of
+sources that can render `cp` in this session. Order is not
+significant; clients MUST treat the value as a set.
+
+| `status=` value | Meaning |
+|---------------------|---------|
+| (empty) | No font in the fallback chain renders `cp`, and the glossary has no registration for it. The cell will render as tofu. |
+| `system` | Some font in the fallback chain renders `cp`. No glossary registration (or `cp` is outside PUA). |
+| `glossary` | `cp` is in PUA and has a live registration in this session. No system font covers it. |
+| `system,glossary` | `cp` is in PUA, has a live registration, AND a system font also covers it. The registration shadows the system font at render time. |
+
+For non-PUA codepoints only the empty value and `status=system`
+are possible. Clients MUST ignore unknown coverage names so
+future sources are forward-compatible.
+
+## 6. Register (`r`)
+
+### 6.1 Request
+
+```
+ESC _ 25a1 ; r ; cp= ; fmt=glyf ; reply=<0|1|2> ; upm= ; aw= ; lh= ; width=<1|2> ; size= ; align=, ; pad=,,, ; ESC \
+```
+
+Parameters:
+
+- `cp` — target codepoint in hex. MUST be in one of the PUA ranges
+ defined in §4. Otherwise the request is rejected with
+ `reason=out_of_namespace`.
+- `fmt` — payload format. One of `glyf`, `colrv0`, `colrv1`.
+ Optional; `glyf` is the default. See §8 for each format's wire
+ layout.
+- `reply` — reply-level control. Optional; default `1`.
+ - `reply=0` — the terminal MUST NOT emit any reply for this
+ registration (neither success nor failure). Intended for bulk
+ fire-and-forget startup registrations that won't be read back.
+ - `reply=1` — the terminal emits both success and failure replies
+ (the default; equivalent to omitting the parameter).
+ - `reply=2` — the terminal emits failure replies only; success
+ registrations are silent. Useful for bulk registrations that
+ still want to learn about the broken ones without a success
+ ACK for every glyph.
+ Unknown values fall back to `reply=1`.
+- `upm` — units per em, the coordinate space the outline is
+ authored in. Optional; default `1000`.
+- `aw` — authored advance width, in upm units. The intended
+ horizontal extent of the glyph, NOT the outline's bounding box.
+ Optional; default `upm`.
+- `lh` — authored line height, in upm units. The intended vertical
+ extent (descender-to-ascender), NOT the outline's bounding box.
+ Optional; default `upm`.
+- `width` — the codepoint's Unicode width, in the `wcwidth` /
+ UAX #11 sense. One of `1` (narrow) or `2` (wide). Optional;
+ default `1`. Authoritative for all terminal layout decisions
+ (cursor advance, wrapping, selection geometry), overriding the
+ codepoint's UAX #11 East Asian Width (all PUA ranges are
+ Ambiguous by default).
+- `size` — scale policy. One of `height`, `advance`, `contain`,
+ `cover`, `stretch`. Optional; default `height`. See §8.5.
+- `align` — placement of the scaled outline within the render span,
+ as a comma-separated pair `,`. `` is one of `start`,
+ `center`, `end`; `` is one of `start`, `center`, `end`,
+ `baseline`. Optional; default `center,center`. See §8.5.
+- `pad` — insets from the render span edges, as comma-separated
+ fractions `,,,` (each `0.0`–`1.0`;
+ top/bottom are fractions of cell height, left/right of render
+ span width). Optional; default `0,0,0,0`. See §8.5.
+- payload — base64-encoded payload for the declared `fmt`.
+
+### 6.2 Response
+
+Replies are gated by the request's `reply` parameter (§6.1).
+For `reply=1` (the default), successful registrations emit:
+
+```
+ESC _ 25a1 ; r ; cp= ; status=0 ESC \
+```
+
+`cp` is echoed from the request.
+
+Failures, when not suppressed by `reply=0`, emit:
+
+```
+ESC _ 25a1 ; r ; cp= ; status= ; reason= ESC \
+```
+
+At `reply=2`, successful registrations are silent; failures still
+emit the error reply above. At `reply=0`, neither success nor
+failure produces any output — the registration is fire-and-forget.
+
+Defined error codes:
+
+| Code | Meaning |
+|-------------------------|---------|
+| `out_of_namespace` | `cp` is not in any PUA range. |
+| `composite_unsupported` | Payload contains composite glyphs. |
+| `hinting_unsupported` | Payload contains hinting instructions. |
+| `malformed_payload` | Payload failed to parse as `glyf`. |
+| `payload_too_large` | Payload exceeds 64 KiB post-base64-decode. |
+
+### 6.3 Overwrite and eviction
+
+A second `r` on the same `cp` overwrites the first. This is how
+applications update a glyph or react to theme changes.
+
+When the glossary already holds 1024 registrations and the new `r`
+is for a `cp` that is NOT already registered, the terminal evicts
+the oldest registration (FIFO) to make room. Eviction silently
+invalidates the evicted codepoint: subsequent emissions fall
+through to the system font (or tofu). Applications SHOULD query
+before emitting if they cannot tolerate silent eviction.
+
+### 6.4 Lifetime
+
+Registrations live for the duration of the terminal session. A
+terminal reset (e.g. `ESC c`) MAY clear the entire glossary.
+Registrations MUST NOT persist across terminal restarts.
+
+## 7. Clear (`c`)
+
+### 7.1 Request
+
+```
+ESC _ 25a1 ; c [ ; cp= ] ESC \
+```
+
+If `cp` is omitted, every slot in the session's glossary is
+cleared. Otherwise the slot corresponding to `cp` is cleared. `cp`
+MUST be in a PUA range; otherwise the request is rejected with
+`reason=out_of_namespace`.
+
+### 7.2 Response
+
+Success:
+
+```
+ESC _ 25a1 ; c ; status=0 ESC \
+```
+
+Clearing an empty slot is a no-op and MUST return `status=0`.
+
+Failure:
+
+```
+ESC _ 25a1 ; c ; status=1 ; reason=out_of_namespace ESC \
+```
+
+### 7.3 Cache invalidation
+
+When a slot is cleared (explicitly, via overwrite, or via
+eviction), the terminal MUST invalidate any rasterization cached
+for that codepoint. A subsequent `r` that reuses the codepoint
+MUST rasterize the new outline fresh, not serve stale pixels.
+
+## 8. Payload format: `glyf`
+
+### 8.1 Scope
+
+Glyph Protocol reuses the OpenType `glyf` table's simple-glyph
+record as its wire format. Authoritative references:
+
+- OpenType `glyf` specification (Microsoft Typography).
+- Apple TrueType Reference Manual, Chapter 6.
+
+### 8.2 Constraints
+
+Terminals implementing Glyph Protocol MUST accept the following
+subset of `glyf` and MAY reject anything else with
+`reason=composite_unsupported` or `reason=hinting_unsupported`:
+
+- **Simple glyphs only** (in v1). No composite glyphs, no references
+ to other glyphs. Composite support may land in a future version
+ once the simple-glyph baseline is settled in the field.
+- **Standard flag encoding** as defined by the OpenType spec
+ (on-curve, off-curve, x-short, y-short, repeat).
+- **No hinting instructions** (in v1). The `instructionLength`
+ field MUST be zero. Bytecode hinting may be allowed in a future
+ version once the unhinted baseline is settled; for now the
+ terminal's own anti-aliasing is what users see.
+- **Coordinate space** defined by `upm`. The terminal maps this
+ space onto its cell at render time.
+
+### 8.3 Contour semantics
+
+A `glyf` record stores a glyph as a set of closed contours. Each
+contour is a sequence of points; each point carries a single
+on-curve/off-curve flag bit. Contour walking follows standard
+TrueType semantics:
+
+- Two on-curve points in a row → straight line.
+- An off-curve point between two on-curve points → quadratic
+ Bézier with the off-curve point as the control point.
+- Two off-curve points in a row → an implied on-curve point at
+ their midpoint.
+
+### 8.4 Color
+
+`glyf` outlines carry no color. Terminals MUST render them in the
+current foreground color. For colored icons see the `colrv0` and
+`colrv1` formats in §8.6 / §8.7.
+
+### 8.5 Sizing, placement, and coordinate convention
+
+Every registered outline passes through three transforms at render
+time, in order: **pad** (compute the effective render span),
+**size** (pick scale factors), **align** (position the scaled
+outline within the span).
+
+#### 8.5.1 Coordinate convention
+
+Outlines are authored in `upm`-unit space, Y-up, with `y=0` at the
+baseline. The authored extent is the rectangle `[0, aw] × [y_min,
+y_max]` where `lh = y_max − y_min` (matching OpenType line height:
+descender to ascender, with descender ≤ 0). Points outside this
+rectangle are allowed — they clip or overflow per the `size`/align`
+rules below.
+
+#### 8.5.2 Render span and padding
+
+The render span is the rectangle of pixels the outline is scaled
+into. Before scaling, the span is:
+
+```
+W = width × cell_width_px (cell_width_px from the terminal)
+H = cell_height_px
+```
+
+`pad=,,,` shrinks the span:
+
+```
+W' = W × (1 − l − r)
+H' = H × (1 − t − b)
+```
+
+Padding values are fractions. If `l + r ≥ 1` or `t + b ≥ 1` the
+terminal MUST treat the request as if `pad=0,0,0,0` (no padding)
+— a degenerate span is not useful and suggests a client bug. The
+effective span `W' × H'` is what `size` and `align` operate on.
+
+#### 8.5.3 Size modes
+
+Given the authored extent `aw × lh` and effective span `W' × H'`:
+
+| `size` | `sx` | `sy` | Aspect ratio | Notes |
+|------------|-------------------|-------------------|--------------|-------|
+| `height` | `H' / lh` | `H' / lh` | Preserved | Default. Line-height drives. Horizontal extent is whatever `aw` scales to; may overflow the span. |
+| `advance` | `W' / aw` | `W' / aw` | Preserved | Advance drives. Vertical extent is whatever `lh` scales to; may overflow. |
+| `contain` | `min(W'/aw, H'/lh)` | same | Preserved | Outline fits entirely inside the span on both axes. May leave empty space on one axis. |
+| `cover` | `max(W'/aw, H'/lh)` | same | Preserved | Outline fills the span on both axes. May overflow on one axis. |
+| `stretch` | `W' / aw` | `H' / lh` | Not preserved | Each axis independent. Useful for box-drawing. |
+
+`height` is the default because it matches how characters behave
+(the terminal's line-height maps to the cell's vertical pixels,
+the horizontal footprint is what the glyph's own advance dictates).
+For icons that must stay inside the cell regardless of aspect,
+prefer `contain`.
+
+#### 8.5.4 Alignment
+
+After scaling, the outline has a scaled authored extent `(aw×sx)
+× (lh×sy)` positioned somewhere within the effective span. `align`
+picks where.
+
+Horizontal:
+
+- `start` — outline's `x=0` aligns with the span's left edge
+ (`pad_left`).
+- `center` — outline's horizontal midpoint aligns with the span's
+ horizontal midpoint.
+- `end` — outline's `x=aw` aligns with the span's right edge
+ (`W − pad_right`).
+
+Vertical (Y-up):
+
+- `start` — outline's `y=y_min` aligns with the span's bottom edge
+ (`pad_bottom`).
+- `center` — outline's vertical midpoint aligns with the span's
+ vertical midpoint.
+- `end` — outline's `y=y_max` aligns with the span's top edge
+ (`H − pad_top`).
+- `baseline` — outline's `y=0` aligns with the terminal's text
+ baseline within the cell. Preferred for character-like glyphs
+ that must sit on the same baseline as surrounding text;
+ descenders extend below the baseline naturally.
+
+When `size=stretch`, the scaled extent exactly matches the span on
+both axes, so `align` has no visible effect. When `size=cover`,
+the scaled extent overflows on one axis; `align` picks which edge
+to anchor (and therefore which overflow is visible vs. clipped).
+When `size=contain`/`height`/`advance`, the scaled extent may be
+smaller than the span on at least one axis; `align` picks where
+the empty space goes.
+
+#### 8.5.5 Resolution independence
+
+Applications MUST NOT assume a particular cell size and MUST NOT
+re-register glyphs on font size change. All scaling is computed
+at render time from the parameters above and the terminal's
+current cell metrics.
+
+#### 8.5.6 Coordinated sets (no scale groups)
+
+There is no `group` parameter. A set of glyphs that must visually
+align — spinner frames, progress-bar steps, a multi-glyph logo —
+aligns automatically if authored with identical `aw`, `lh`, `size`,
+`align`, and `pad`, and if their outline geometry is coordinated
+(e.g. all spinner frames sized inside a common bounding circle).
+Scale-group semantics can be added later if authoring experience
+shows they are genuinely needed; for v1 the burden sits with
+the author, not the protocol.
+
+### 8.6 Payload format: `colrv0`
+
+`fmt=colrv0` carries a layered flat-colour glyph using the
+OpenType `COLR` v0 and `CPAL` tables verbatim. The protocol wraps
+those tables in a small container that also ships the simple-glyph
+outlines each layer references, so a colour glyph is self-
+contained: no external font needed.
+
+**Container layout** (all integers big-endian, post-base64-decode):
+
+```
+u16 n_glyphs # 1..=1024
+repeat n_glyphs:
+ u16 glyf_len
+ glyf_len bytes # simple-glyph record, §8.2 subset
+u16 colr_len # > 0
+colr_len bytes # OpenType COLR v0 table
+u16 cpal_len # may be 0 (see below)
+cpal_len bytes # OpenType CPAL table (required for v0)
+```
+
+`GlyphId` values in the `COLR` table resolve to indices into the
+outline array (glyph 0 is the base glyph rendered when the
+terminal emits `cp`). `paletteIndex` values in the `COLR` layer
+records resolve to entries in the CPAL colour records array, in
+standard OpenType order (one record = one BGRA quadruple).
+`paletteIndex = 0xFFFF` MUST be rendered as the current foreground
+colour, per the OpenType spec.
+
+**Rendering rules.**
+
+- Layers composite in painter order (first layer painted first).
+- Per-layer colours come from CPAL; `0xFFFF` means foreground.
+- `COLR` v0 defines no transforms or compositing modes beyond
+ `src-over`, so terminals MAY implement v0 in one pass with no
+ graphics-state stack.
+
+**Validation.** Terminals SHOULD validate the wrapped `COLR` and
+`CPAL` tables using an OpenType parser (e.g. `ttf-parser`); a
+`COLR` table that fails to parse SHOULD be rejected with
+`reason=malformed_payload`. Every carried outline MUST satisfy the
+`glyf` simple-glyph subset of §8.2; violations use the same error
+codes as `fmt=glyf`.
+
+### 8.7 Payload format: `colrv1`
+
+`fmt=colrv1` shares the container layout of §8.6 but ships an
+OpenType `COLR` v1 table, which adds a full paint graph: linear,
+radial, and sweep gradients, affine transforms, clip boxes, and
+per-layer compositing modes. `CPAL` remains valid but is optional
+— v1 paints may carry sRGBA directly — so `cpal_len = 0` is
+permitted and means "the COLR references no palette index."
+
+**Paint types.** Terminals implementing `colrv1` SHOULD support
+the full OpenType paint-graph vocabulary for maximum interop. A
+conforming subset for low-overhead implementations is:
+
+- Solid (direct sRGBA or palette index).
+- Linear gradient.
+- Radial gradient.
+- Affine transforms on paint subtrees.
+- `src-over` layer composite.
+
+Terminals MAY render unsupported paint nodes (sweep gradients,
+blend modes beyond `src-over`, variations) using a reasonable
+fallback — typically the paint subtree's first solid colour —
+rather than rejecting the registration.
+
+**Foreground inheritance.** CPAL palette index `0xFFFF` and
+v1's `PaintSolid` with the foreground sentinel MUST resolve to
+the cell's current foreground colour at rasterisation time.
+Terminals that cache rasterised colour glyphs MUST re-rasterise
+on foreground change for any glyph whose paint graph references
+`0xFFFF`.
+
+**Security.** The colour formats add no new attack surface beyond
+§9: `cp` is still PUA-only, the cell buffer is still authoritative
+for copy/selection, and registrations are still session-scoped.
+A malformed `COLR` is a rendering error, not an injection vector
+— the rendered pixels can only affect cells the client itself
+emits at a PUA codepoint.
+
+### 8.8 Authoring
+
+Most applications will not hand-author `COLR` bytes either.
+Typical flows:
+
+- **From an existing colour font.** Use `fontTools` to extract the
+ `COLR`/`CPAL` tables for the glyphs of interest, then pack them
+ with the referenced outlines into the container above.
+- **From SVG.** The Skia team publishes `nanoemoji` / `maximum-color`,
+ which compiles a directory of SVGs into a `COLR` v1 font; feed
+ its output into the packer.
+
+## 9. Security considerations
+
+The core property Glyph Protocol must preserve is that **an
+application cannot change how existing text looks**. Enforcement
+is structural:
+
+- Register accepts a `cp` parameter, but `cp` MUST be in one of the
+ three Unicode Private Use Areas (§4). Any non-PUA codepoint is
+ rejected with `reason=out_of_namespace`.
+- Users never type PUA codepoints. No pre-existing text —
+ filenames, URLs, commands, variable names, log lines — contains
+ them. A program that registers a glyph can only affect how PUA
+ codepoints render, and PUA codepoints only appear in text the
+ same application (or another one opting into the same
+ convention) has deliberately emitted.
+- The cell buffer is authoritative. Selection, copy, search,
+ hyperlinks, shell history, and any other text extraction MUST
+ return the codepoint the application emitted, never the
+ rendered glyph.
+
+Without these properties, a program writing to the PTY could
+register a glyph for `a` that looks like `o` and mislead the
+reader. With them, the worst a program can do is render a
+weird-looking character at a PUA codepoint the user never types
+and the cell buffer honestly reports.
+
+Other considerations:
+
+- **Resource bounds.** The 1024-slot cap and 64 KiB per-payload cap
+ give a hard upper bound of 64 MiB on the glossary's memory
+ footprint per session.
+- **No code execution.** The `glyf` subset defined in §8.2
+ excludes hinting instructions, which is the only part of
+ TrueType that is executable. Glyph Protocol is purely
+ declarative.
+- **No filesystem access.** Glyph Protocol messages do not
+ reference files and MUST NOT be used to load data from disk.
+- **Session isolation.** Glossaries MUST NOT leak between terminal
+ tabs, windows, multiplexer panes, or PTY sessions.
+
+## 10. Non-goals (v1)
+
+- **No non-PUA codepoints.** Registration is restricted to the
+ three PUA ranges — see §4.
+- **No ligatures.** Registration applies to a single codepoint.
+ Sequence-keyed substitution is out of scope; programming
+ ligatures are already handled by OpenType fonts.
+- **No persistence across sessions.** Glyphs are shipped fresh on
+ each run.
+- **No cross-application sharing.** Each terminal session owns its
+ glossary. No IPC, no daemon.
+- **No bitmap colour glyphs.** Colour is delivered via `colrv0`
+ and `colrv1` (§8.6 / §8.7), which are vector. `CBDT`/`sbix`/
+ `SVG ` tables are explicitly out of scope so resolution
+ independence is preserved.
+- **No subpixel positioning control.** The terminal's normal cell
+ positioning applies.
+- **No bitmap payloads.** Vector only, to preserve resolution
+ independence.
+
+## 11. Conformance
+
+A terminal emulator is Glyph Protocol v1 conformant if it:
+
+1. Recognizes the `25a1` identifier in APC sequences.
+2. Implements the `s`, `q`, `r`, and `c` verbs with the semantics
+ defined in this specification, and lists every accepted payload
+ format by name in the `fmt=` value of the `s` reply.
+3. Restricts register/clear `cp` to the three PUA ranges; rejects
+ anything else with `reason=out_of_namespace`.
+4. Holds at most 1024 simultaneous registrations per session and
+ evicts in FIFO order when full.
+5. Accepts the `glyf` simple-glyph subset defined in §8.2. The
+ `colrv0` and `colrv1` formats are OPTIONAL; terminals that
+ accept them MUST list the corresponding name in the `s` reply.
+6. Renders registered `glyf` glyphs in the current foreground
+ color; renders `colrv0`/`colrv1` glyphs using the COLR paint
+ graph, resolving palette index `0xFFFF` to the current
+ foreground color.
+7. Scales and positions glyphs according to `upm`, `aw`, `lh`,
+ `width`, `size`, `align`, and `pad` as specified in §8.5, and
+ treats the registered codepoint as having the declared `width`
+ (`1` or `2`) for every layout decision, overriding the
+ codepoint's UAX #11 East Asian Width.
+8. Enforces the cell-buffer authority invariant in §9: selection,
+ copy, and search return the raw codepoint.
+9. Ignores unrecognized parameters rather than failing the
+ request.
+
+A client (application) is Glyph Protocol v1 conformant if it:
+
+1. Emits `cp` only from the three PUA ranges.
+2. Treats query timeout as "terminal does not implement Glyph
+ Protocol."
+3. Emits only the `glyf` subset defined in §8.2.
+4. Handles all `reason=*` error codes without crashing.
+
+## 12. Reference implementation
+
+The reference implementation ships in Rio terminal. A companion
+helper, `svg2glyf`, ships alongside to convert existing SVG assets
+to the accepted `glyf` subset at build time.
+
+## Appendix A. Worked example: register an icon in empty PUA
+
+```python
+import base64, sys
+from fontTools.pens.ttGlyphPen import TTGlyphPen
+
+# A stylised outline in glyf coordinate space (upm=1000, Y-up).
+pen = TTGlyphPen(None)
+# ... draw commands ...
+pen.closePath()
+
+payload = base64.b64encode(pen.glyph().compile(None)).decode("ascii")
+
+# Register at U+100000 — the first codepoint of Supplementary PUA-B.
+# No known font covers this range, so the registration is the sole
+# source of the rendered glyph and the demo is unambiguous.
+sys.stdout.write(
+ f"\x1b_25a1;r;cp=100000;upm=1000;{payload}\x1b\\"
+)
+sys.stdout.flush()
+
+# From now on, U+100000 renders our outline.
+sys.stdout.write(f"icon: {chr(0x100000)}\n")
+```
+
+## Appendix B. Worked example: query before registering
+
+```python
+import sys
+
+def q(cp: int) -> None:
+ sys.stdout.write(f"\x1b_25a1;q;cp={cp:x}\x1b\\")
+ sys.stdout.flush()
+
+# Does the user already have Nerd Fonts installed?
+q(0xE0A0)
+# Expected reply parsed from the PTY:
+# status=system → system font covers it; don't register, just emit cp
+# status= → empty list (nothing covers it); register and emit
+```
+
+## Appendix C. Implementation notes
+
+These are not normative but reflect lessons from the first
+implementations.
+
+**Response draining.** By default `r` and `c` produce an APC reply
+on the PTY. Client applications that register at startup and do not
+care about the reply have three options, in order of preference:
+
+1. Use `reply=0` on the register request (§6.1). The terminal emits
+ nothing, so there's nothing to drain and nothing to leak. Best
+ for bulk startup registrations.
+2. Use `reply=2` to keep failure replies but drop success ACKs.
+ Retains debuggability (you still learn about malformed payloads)
+ without the success-reply noise of a 100-glyph registration.
+3. Let the framework's input reader swallow the reply — safe only
+ while that reader is alive.
+
+The failure mode to watch for is sending `r` or `c` with `reply=1`
+AFTER the framework has torn down its input reader — typically on
+exit — at which point the reply arrives in the PTY but nobody reads
+it, and the shell that takes over the PTY after the app exits emits
+the queued bytes as visible text (`.25a1;c;status=0` or
+`.25a1;r;cp=…;status=0`). For exit-time cleanup, prefer skipping
+the `c` altogether (registrations expire with the session anyway).
+
+**Practical source of `glyf` data.** Most apps will not hand-author
+`glyf` bytes. The typical pipeline is:
+
+1. Open a Nerd Font or similar icon TTF with `fontTools`.
+2. For each codepoint of interest, pull the glyph record.
+3. If composite, flatten via `fontTools.pens.ttGlyphPen.TTGlyphPen`.
+4. Strip hinting instructions (`instructionLength := 0`).
+5. Compile to bytes; base64-encode; register at a codepoint of the
+ app's choosing.
+
+Because the source codepoint in the font is irrelevant to the
+protocol, applications commonly pull outlines from a Nerd Font's
+basic-PUA codepoints (`U+E0A0`, `U+F07B`, …) and register them at
+Supplementary PUA-B slots (`U+100000`+) — that way the rendered glyph
+is unambiguously from the registration, not from a system font that
+happens to cover the same codepoint.
+
+**Atlas cache invalidation.** The cache-invalidation rule in §7.3
+applies to overwrite and eviction too, not only explicit clear.
+Implementations that key their glyph atlas on some stable slot id
+(rather than `cp`) must ensure the slot id is either released on
+clear/evict or paired with a per-registration invalidation tag, so
+that a subsequent register reusing the id rasterizes fresh bytes
+rather than serving a stale bitmap.
+
+## Appendix D. Change log
+
+| Date | Version | Notes |
+|------------|---------|-------|
+| 2026-04-17 | v1 | Initial release. Register accepts a client-picked `cp` restricted to PUA; 256-slot glossary with FIFO eviction; numeric `status` field; ligatures out of scope. |
+| 2026-04-19 | v1.1 | Added `s` verb (support advertisement / protocol ping). |
+| 2026-04-19 | v1.2 | Added `fmt=colrv0` and `fmt=colrv1` payload formats wrapping OpenType `COLR` / `CPAL` tables with sidecar `glyf` outlines. Both advertised via bits 1 and 2 of the `s` reply's `fmt=` bitfield. |
+| 2026-04-19 | v1.3 | Added `reply=0|1|2` parameter to the `r` verb so bulk registrations can suppress success ACKs (`reply=2`) or go fully fire-and-forget (`reply=0`). Default `reply=1` preserves v1.0 behaviour. |
+| 2026-04-19 | v1.4 | Raised the glossary capacity from 256 to 1024 simultaneous registrations per session, and raised the `n_glyphs` cap in `fmt=colrv0`/`colrv1` containers from 256 to 1024 outlines. Both bumps quadruple the worst-case memory footprint; the 64 KiB per-payload cap is unchanged. |
+| 2026-04-21 | v1.5 | Added `cp=auto` to the `r` verb: the terminal allocates a free PUA codepoint (SHOULD come from PUA-B) and echoes it in the success reply so the client can emit it. Added `reason=auto_unsupported` and `reason=glossary_exhausted` error codes. `cp=auto` forces a success reply regardless of `reply=0|2` because the allocated codepoint is only carried in the reply. |
+| 2026-04-23 | v1.6 | Removed `cp=auto` from the `r` verb (introduced in v1.5). Auto-allocation forced a stateful round-trip reply the client depended on to learn its codepoint, which recording tools like `asciinema` and `tee` cannot capture or replay — making `cp=auto` output impossible to reproduce from a transcript. Clients must pick their own PUA codepoint. The `auto_unsupported` and `glossary_exhausted` error codes are withdrawn. |
+| 2026-04-23 | v1.7 | Added a sizing and placement model to the `r` verb: `aw` / `lh` (authored extent in upm units), `width` (Unicode/wcwidth width, `1` or `2`, authoritative), `size` (`height`/`advance`/`contain`/`cover`/`stretch`), `align` (`,` positioning after scale, with `v=baseline` for character-like glyphs), and `pad` (fractional insets from the render span). Pinned the coordinate convention: Y-up, `y=0` at baseline, `lh` measured descender-to-ascender (OpenType). Scale groups are intentionally omitted — coordinated sets align via matching parameters and outline geometry. |
+| 2026-05-03 | v1.8 | Replaced the `s` reply's `u8` bitfield with a comma-separated list of format names (e.g. `fmt=glyf,colrv0,colrv1`). Names extend without bit-collision worries and stay readable in transcripts; an empty `fmt=` means the terminal advertises no payload formats. Unknown names MUST be ignored by clients, so future formats are forward-compatible. |
+| 2026-05-03 | v1.9 | Replaced the `q` reply's `u8` two-bit `status` field with a comma-separated list of coverage names: `status=system`, `status=glossary`, `status=system,glossary`, or empty for "free". Same motivation as v1.8 for the `s` reply. The `r` and `c` replies still use `status=` for success/failure since that's a closed boolean, not an extensible set. |