# Legend lines — visual reference

This is the consolidated rulebook for the dashed-line callouts that connect outer-gutter labels to dots inside the wireframe. The rules below are non-negotiable.

## The pattern at a glance

Every callout has TWO halves: an anchor element inside the wireframe, and a CALLOUTS entry that describes how to draw the leader.

```jsx
// inside a component — option 1: id directly on a content element
<div id="a-mything" className="...">...</div>

// inside a component — option 2: a 0-size span anchor positioned at a chosen edge
<div className="relative ...">
  <span id="a-mything" className="absolute right-0 top-1/2" data-callout-anchor="" />
  ... content ...
</div>

// then in the state's _CALLOUTS array
const SXyz_CALLOUTS = [
  { id: "a-mything", side: "right", anchor: "right", label: "Description ≤ 80 chars" },
];
```

`side` picks the gutter the label lives in (`top` · `bottom` · `left` · `right`).
`anchor` picks which edge of the anchor element's bounding box the dot lands on (`left` · `right` · `top` · `bottom` · `center`).

For 0-size span anchors, the `anchor` field is essentially a no-op (the bbox has zero width/height) — the **span position is what matters**. Use `right-0 top-1/2` to put the dot at the right edge of the parent container, `left-0 top-1/2` for the left edge, `left-1/2 top-0` for top-centre, `left-1/2 bottom-0` for bottom-centre.

## The 7 rules

### 1. Anchor on the section's edge facing its label

A right-side label anchors on the section's right edge, not its centre. A left-side label anchors on the left edge. Why: the leader becomes a tiny stub from element-edge to gutter, instead of a long line cutting through the element.

### 2. Leader is mostly a single straight run

One horizontal or vertical run from anchor to label. No diagonals through the wireframe. The render code uses an L-shaped elbow when label.y ≠ anchor.y; rule 3 governs where the bend happens.

### 3. Bend only in the gutter, never over UI

When the label sits at a different y from the anchor (collision pushed it), the leader bends 16px inside the gutter — never crossing the wireframe stage. The render code already handles this (look for `elbowX = p.lx ± 16` in the Callouts component). Don't introduce custom paths that route through UI.

### 4. Comprehensive over minimal

Aim for **5–8 callouts per main-shell state**, covering each chrome column AND each section of the main-content area. Earlier versions of this skill said "less is more · 2-4 callouts" — that turned out to be wrong for presales artifacts where viewers want the entire UI explained. Drop a callout only if its leader genuinely cannot avoid crossing other UI.

The split: column callouts describe shell elements (topbar, sidebar, case list, right rail). Section callouts describe content (each section in the main centre). Both are valued.

### 5. Collision-avoidance push direction is fixed

When two labels on the same gutter would overlap, push later-in-DOM-order labels:
- Top/bottom labels: push right (later moves x+).
- Left/right labels: push down (later moves y+).

This keeps each label visually paired with its anchor — the eye expects to look down/right for the next callout, not up or left.

### 6. Overlay states annotate the overlay, not the underlying shell

Modal / sheet / banner states (Command palette, Why? sheet, Inbound banner, Wrap-up modal) draw their callouts targeting overlay anchors. The dimmed shell underneath is context, not content. Don't double up by annotating the shell at the same time.

### 7. Closest path wins — for both `side` and `anchor`

This is the most important rule and the one most often violated.

Pick the **side** whose gutter is NEAREST the anchor element. Then anchor on the element's edge facing that gutter.

- A banner at the top of the wireframe gets `side: "top"`, NEVER `"bottom"`. The leader should never cross the stage to reach a far gutter.
- A toast in the top-right corner gets `side: "top"` (or `"right"`).
- A button in the right rail gets `side: "right"`, even if the button is at the bottom of the rail.
- The KPI footer at the bottom of the wireframe gets `side: "bottom"`.

For row-spanning components (a row of cards · a footer strip · a multi-column row), the dot lands on the outer ROW's edge — humans read that as "this row of components" rather than "this single column". Add `relative` to the outer row wrapper and host the anchor span there.

## Common patterns table

| Component type | Side | Anchor edge |
|---|---|---|
| Sidebar (column on left edge of stage) | left | left |
| Inbox / case list (column to right of sidebar) | left | left (so dot is at the column's left boundary, not buried inside) |
| Right rail (column on right edge of stage) | right | left or right (whichever is the gutter side) |
| Topbar | top | bottom or top |
| Slide-down banner at top of stage | top | top |
| Top-right corner toast | top | top |
| KPI footer | bottom | bottom |
| Channel-cards row | right | right |
| Multi-column row (announcements + wrap-ups + recent) | right | right of the OUTER row wrapper |
| Modal centred on screen | left or right | the matching edge of the modal |
| Slide-in sheet flush against wireframe right | right | left (so the leader has visible travel through the sheet) |
| Single in-row cell (e.g. table column header on left side of a wide row) | left | matching edge of the cell |

## Anchor placement quick reference

Where to put the `<span id="..." className="absolute ..." />` based on what edge you want the dot on:

| Span position | Dot lands at | Use case |
|---|---|---|
| `right-0 top-1/2` | Right edge, vertical centre | Right-side callout pointing into the parent's right edge |
| `left-0 top-1/2` | Left edge, vertical centre | Left-side callout |
| `left-1/2 top-0` | Top edge, horizontal centre | Top-side callout (banner, topbar) |
| `left-1/2 bottom-0` | Bottom edge, horizontal centre | Bottom-side callout (KPI footer, button at bottom of section) |

For row-spanning callouts: put the span at `right-0 top-1/2` of the OUTER row wrapper (which is `relative`), not inside an inner cell.

## Stage geometry (the magic numbers)

| Knob | Value | Notes |
|---|---|---|
| Stage design size | 1920 × 1080 | Fixed; everything scales from this |
| Outer container max-width | 2400 px | Allows 1:1 scale on wide monitors |
| Stage container maxWidth | 1920 px | Caps scale = 1.0 |
| Gutter (each side) | 320 px | Hard constraint: ≥ label_width + offset + slack |
| Top/bottom callout offset | 60 px | Distance from stage edge to dot |
| Top padding above stage | mt-32 = 128 px | Tab-bar clearance |
| Label width | 230 px | Fits ~2 lines of 12px text |
| Label height (estimated) | ~32 px | Two-line labels render up to ~45px |

The interlocking constraints:
- `gutter ≥ label_width + offset` — must hold or labels clip past viewport edge
- `top_padding ≥ tab_bar_height + label_height + buffer` — must hold or top labels hide behind tab bar
- Outer max-width should be ≥ stage_width + 2 × gutter — so on max-width monitor, no compression

## When NOT to add a callout

- Pure flow views (`agent-flow`, `sup-flow`) — the layout itself is the explanation.
- A second callout pointing at the same `id` as another callout in the same view — one is enough; duplicate IDs produce two leaders into the same dot.
- An element whose function is obvious from its on-screen label.

## Iteration history (where these rules came from)

The rules above didn't come out of one design pass. They came from a 7-iteration loop with a real user reviewing real outputs:

1. **i1**: Initial build of 14 wireframe states. Naive callouts.
2. **i2**: Removed duplicate-anchor entries (S4/S6 had two callouts targeting same id), routed flow tabs more cleanly, added 3 interactive demo tabs.
3. **i3**: User said "page is too small, tabs cut off, need column callouts". Stage gutters expanded, tab bar wraps, callout density bumped to 5–8/state. **Rule 4 flipped from "less is more" to "comprehensive".**
4. **i4**: Playwright-verified everything. Bug found: tab bar covered top callouts (it was `sticky`). Removed sticky, bumped gutter further, added top padding. **Rule 7 v1 added: "edge nearest the gutter wins".**
5. **i5a–b**: Sidebar + Inbox leaders moved to left edge of column. KPI footer leader moved to bottom border. Announcements anchor moved to outer row wrapper. **Edge-nearest-gutter became universal.**
6. **i5c**: Per-step demo callouts wired up. App-level dynamicCallouts state. Race condition discovered (parent reset useEffect overrides child mount push) → fixed via cleanup-on-unmount.
7. **i5d**: User pointed out banner callouts were going to the FAR gutter. **Rule 7 generalized: closest path wins for `side` AND `anchor`.**

Each iteration came with browser-rendered evidence. If a rule above feels arbitrary, it isn't — it's the answer to a real bug.
