On 2026-08-26 the card spec gained a zone model — identity, content, action. It never reached the screen, and the reason turned out not to be styling at all. A card’s closing --- is not a horizontal rule: with no blank line before it, CommonMark reads it as a setext heading underline, and the entire card body — title, anchor, the sender’s prose, the rename — is promoted into one <h2> element. The zones are indistinguishable because they are literally the same element.
That explains every symptom this area has chased for weeks: why the provenance line is the widest thing on the card, why italic “came back heavier than the title” (italic inside an <h2> is large bold-italic), and why the closing rule is invisible — it was consumed as the underline. It also means blank lines alone do not fix it: they split the paragraph, so the heading stops swallowing the whole card and starts swallowing the last block before the rule — the rename. Below: the parser evidence, the one-line fix, the register grammar that becomes possible once the heading is gone, and the 48-site work-list.
Sample data is synthetic. Titles, Codes and names are stand-ins at the real strings’ shape and length — this repo publishes, and every branch gets a preview URL.
Title and anchor line only: the object’s name, its ⇄ address, and ·-joined facets. Nothing a human wrote in sentences belongs in this zone. A sentence sitting inside the identity block is what makes a card read as a wall.
Not a rule — an underline. text\n--- is setext H2 in CommonMark, and a setext heading spans every line of the paragraph above it. With no blank lines the whole card is one paragraph, so the whole card becomes the heading. This is the defect; everything else on this page is downstream of it.
A single \n is not separation: some hosts collapse it and the card renders as a run-on. The rule predates the defect — _claim_manic_block’s docstring has stated it since #1163. It is load-bearing and it is shipped. It is also not enough on its own: without Z2 it just moves the heading onto the rename.
Separation says these are three groups. It does not say this group is quieter than that one — and the sender’s provenance sentence is still the widest thing on the card. Prose the sender wrote (origin, forward note, description) should render as a blockquote: someone else’s voice inside your card, indented rather than weighted.
Twice confirmed, expensively. A line that is italic on its own comes back from the claude.ai renderer as large serif bold-italic. It made Origin the heaviest thing on the card, and then made the carried-over lead the same. Any future “make this recede” instinct that reaches for *…* is reaching for the trap. Structure is the only lever left, which is what makes Z4 a quote and not an emphasis.
Three helpers independently chose 👤 for org kind, sender kind and visibility. On a personal-org, human-sent, private relay — the most common case there is — all three collapse, and the icons carry zero information exactly when the card renders most often.
Quote becomes an indent, sigils become their plain equivalents, the blank line stays a blank line. Plain is an unstyled transcript, not a second design — and it is the branch that gets skipped, which is the most common defect in this area.
A receive card as production renders it today, reproduced verbatim. Four registers, one weight, no gaps. The reader’s eye has nowhere to land after the title, so the sender’s provenance sentence — the least important line on the card — occupies the most visual space on it.
The title. Correct, and the only line whose weight is earned.
Data: the Code, who sent it, when. Also correct — and it should be structurally unlike everything below it, which is the point.
Prose, in a data block, with no mark and no gap. Italic was tried and removed because it “came back heavier than the title it sat under”; nothing replaced it. It is now the widest thing on the card.
The one line the user can act on, rendered adjacent to and at the same weight as the facts about the object. Nothing separates a thing you read from a thing you do.
In CommonMark, text followed by --- is not a paragraph and a horizontal rule. It is a setext heading — and a setext heading takes every line of the paragraph above it, not just the last. A card whose lines carry no blank line between them is one paragraph, so the closing rule promotes all of it.
Every symptom this area has chased for weeks falls out of that one <h2>:
| Symptom | Explanation |
|---|---|
| Zones look identical | They are the same element. No amount of blank-line grouping changes the fact that a heading renders as a heading throughout. |
| Provenance is the widest thing | It is the longest line in a heading, so it wraps at heading size across three lines. |
| Italic “came back heavier” | Italic inside an <h2> is large bold-italic. Removing the italic was right; the diagnosis under it — “italic is a display treatment in this renderer” — was one level too shallow. It is a display treatment because it is inside a heading. |
| No closing rule appears | There is no closing rule. It was eaten. The card only ever emits one <hr>, the opening one. |
Four candidate strings, run through a CommonMark parser. h2 is how many heading elements the card produces; it should be zero.
| Variant | h2 | hr | What the heading swallows | Verdict |
|---|---|---|---|---|
| A — live on main | 1 | 1 | The entire card — title, anchor, provenance, rename | the defect |
| B — blank lines only | 1 | 1 | The rename line — the last block before the rule | relocated |
| C — blank line before --- | 0 | 2 | Nothing. Each zone is its own <p>. | fixed |
| D — close with *** | 0 | 2 | Nothing — *** can only ever be a rule. | fixed |
Three groups, three gaps. Nothing was added and nothing was styled — two single newlines became two blank ones, and the run-on becomes three readable units. The whole fix, and it was a regression being undone rather than a feature being built.
Grouping is not differentiation. Three groups at one weight read as three paragraphs of the same kind of thing, and the eye still lands on the longest one. The remaining question is narrow and specific: what does “quieter” look like when italic is a trap and bold is spoken for?
Packaged out of a relay-platform session that had just shipped the apex-router URL sweep. That work lived in the same card-grammar layer, so this resource map is first-hand.✎ Name this chat → “MCP response cards — … — FBQJ4HYNP9” · open ↗ --- content loaded — card above 📌 Start here to work on Relay’s MCP card display — the confusions are named with traced evidence.the eye lands on the title, then the Code, then the ask
The zone table the renderer would implement. Separation is layer 1 and is shipped; mark and treatment are the open part.
| Zone | Holds | Separation | Mark | Rich | Plain |
|---|---|---|---|---|---|
| Identity | title, ⇄ anchor, facets | opens the frame | object sigil + ⇄ | bold title, mono Code, ·-joined | sigil, bare Code, ·-joined |
| Narrative | origin, forward note, description | \n\n | > quote | blockquote, no emphasis | 4-space indent |
| Reader ask | rename, gates, advisories | \n\n | ✎ 🔔 | bold verb, quoted value | > + verb |
| Signal | “content loaded” | closing rule above it | — | italic, after --- | plain line |
| Sender ask | the context_ask | \n\n | 📌 | own line, no emphasis | * + text |
A separate defect in the same area, untouched by either layer above. The send card does not use the shared renderer at all — it is hand-built in both branches. Its anchor line runs six facets and uses ✦ where the canonical anchor is ⇄. Three of those facets independently resolved to 👤.
Five changes, each traceable to a named helper.
| Facet | Helper | Was | Is | Why |
|---|---|---|---|---|
| Anchor | transfer.py inline | ✦ | ⇄ | The canonical anchor, described in _display.py as “the universal Code-line anchor … so the Code is never buried”. The send card is the surface that most needs it and the one that opted out. |
| Visibility | _vis_icon() | 👤 | 🔒 | Private is a lock, not a person. Frees 👤 for the only facet that means a human. Org stays 👥, broadcast stays 📢. |
| Org kind | org_display_icon() | 👤 | 🏢 | Moves off the anchor line onto the sender line and takes the building glyph in both cases — a personal org is still a container, and the word in is what says so. |
| Sender | _sender_emoji() | 👤 | 👤 | Keeps 👤 — the one facet the glyph was always right for. 🤖 for an agent sender is unchanged. |
| Receives | _claims_label() | — | ∞ | The only fragment on the line with no glyph, so it read as a continuation of the facet before it. ∞ receives / 1 receive / 3 receives. |
There is a grammar for what a card looks like and none for what a response is made of. Six independent pieces compete for position, and the order they land in today is a set of individually-argued local decisions — each with a good reason, none written down together. Naming the order is the prerequisite for rearranging any of it.
Identity → narrative → reader ask. The thing agents are instructed to render verbatim, and therefore the best-protected slot in the response. Anything that must survive a summarising agent belongs inside it.
Moved inside the frame on 2026-08-26, beside the title it renames. It held the closing line as a robustness hack, not because it earned it.
“content loaded — card above”. The cue that the card ended and prose may begin. A card with no signal is one the model reads as a preamble and talks over — the documented cause of both June 2026 card bugs.
Passed an explicit placement on the receive path, because a generic “surface after the result” fought the receive’s own tail for last place.
One field, two placements: above the excerpt on a preview (decision support — is this worth receiving?), below the signal on a receive (a directive, next to the thing responding to it).
The action CTA is inserted before the rename CTA on all five content-loading receive branches, so the instruction reads offer-then-rename. Reordering buries the regression that ordering exists to prevent.
Every "display" site in src/relay/mcp/, tiered by exposure. Tier 1 carries the narrative zone and is where both the report and the shipped fix landed. 43 of 48 hand-build their card string, so most of tier 4 is a migration, not an edit — which is the right order: fix the grammar once in _display.py and the rest inherit it.
| Tier | Cards | Sites | Layer 1 | Layer 2 |
|---|---|---|---|---|
| T1 | receive — owner view, re-receive, preview, the received card, self view, preview-new; series receive | 7 | shipped | Z4 quote on the narrative zone |
| T2 | the rename block, on receive and series receive | 5 | shipped | The card-vs-next_step either/or decision |
| T3 | send — created, staged, forwarded | 3 | Not yet — hand-built, no gaps | Z6 glyph table, canonical anchor, sender/org split, signal line |
| T4 | session (10) · series (6) · stream (4) · save (3) · request (3) · profile (3) · nudges (2) · manicure (2) · feedback (1) · approve/deny (2) · other transfer (8) | 33 | Not yet | Migrate onto relay_card() and inherit Z1–Z7. Both branches, one commit. |
The server cannot read a client’s negotiated capabilities at tool-call time, and real hosts diverge from spec — which is why the display QA runbook is manual. Every rule on this page is a bet on how a host renders a markdown structure, and this area has already lost that bet twice with italic.
| Bet | Risk | Fallback if it fails |
|---|---|---|
| Blockquote reads quieter | A host that renders > with a tinted ground or a heavier face reproduces the original defect — heavier, not quieter. Precisely how italic failed, twice. | Four-space indent: every host treats it as preformatted and none emphasises it. Loses the wrap-spanning rule, keeps the register. |
| Blank lines survive | Already the shipped mechanism, so this one is load-bearing for layer 1 too. A host that collapses \n\n erases the grouping entirely. | None — this is why the pinning test exists. A host that collapses hard breaks needs the thin-rule treatment instead. |
| New glyphs render | 🔒 🏢 ∞ are broadly safe; a terminal without emoji falls back to boxes. | Plain mode already has an ASCII table — extend it rather than restrict the rich set. |
SPEC.relay-display-grammar.md · DESIGN-NOTE.relay-card-display.md §Zones
Work-list: relay-platform REFERENCE.card-conformance-audit.md
Siblings: Object envelope v2 · Relay card display · Code presence