# Layout Math SVG has no auto-layout. Every coordinate, width, and height is hand-computed. Most diagram failures trace back to text overflowing its container or elements landing outside the viewBox. This file is the arithmetic you run *before* writing any ``. ## The coordinate system - **viewBox**: `0 0 680 H`. Width is always 680. Compute H from content. - **Safe area**: `x ∈ [40, 640]`, `y ∈ [40, H - 40]`. Leave 40px of breathing room on every edge. - **Usable width**: 600 px (640 − 40). - **Pixel units are 1:1.** The `width="100%"` with `viewBox="0 0 680 H"` means the browser scales the whole coordinate space to fit the container. A 14px font is really 14px. Character widths below assume this 1:1 ratio. ## Text width estimation SVG `` never auto-wraps. If you put more characters in a box than it can hold, they overflow visibly. Use these character-to-pixel multipliers, calibrated on Anthropic Sans: | Case | Approx. width per character | |------------------------------------------|-----------------------------| | 14px latin, weight 500 (`th`, titles) | ~8 px | | 14px latin, weight 400 (`t`, body) | ~8 px | | 12px latin, weight 400 (`ts`, subtitles) | ~7 px | | 14px CJK (Chinese, Japanese, Korean) | ~15 px (~2× Latin) | | 12px CJK | ~13 px (~1.9× Latin) | **Special characters:** chemical formulas (C₆H₁₂O₆), math symbols (∑ ∫ √), subscripts, superscripts all take the same horizontal space as normal characters — don't assume subscripts are narrower. For labels that mix Latin and formulas, add 30–50% padding to your width estimate. ### Empirical calibration samples Real measurements of Anthropic Sans at the sizes this skill uses. The multipliers above are the safe ceilings derived from these samples — not the averages. Letter composition matters: narrow letters like `l`/`i`/`f` push the per-char average down, tall letters like `B`/`J`/`P`/`Q` push it up. | Text | Chars | Weight | Size | Measured width | Per-char | |-----------------------------------------|-------|--------|-------|----------------|----------| | `Authentication Service` | 22 | 500 | 14 px | 167 px | ~7.6 | | `Background Job Processor` | 24 | 500 | 14 px | 201 px | ~8.4 | | `Detects and validates incoming tokens` | 37 | 400 | 14 px | 279 px | ~7.5 | | `forwards request to` | 19 | 400 | 12 px | 123 px | ~6.5 | | `データベースサーバー接続` | 12 | 400 | 14 px | 181 px | ~15.1 | The formula uses **8** for 14px Latin and **7** for 12px Latin because those are the safe ceilings, not the average. A label like "Background Job Processor" at 8.4 px/char would overflow a box sized from a 7.6 px/char average — always round up, never down. **Worked sanity check**: `"Detects and validates incoming tokens"` (37 chars, `ts` class at 12px) → formula gives `37 × 7 + 24 = 283 px`. Measured 279 px + 24 px padding = 303 px. The formula is ~7% conservative, which is the correct direction for rect sizing — bias toward extra padding, never toward clipping. ### Rect sizing formula For a two-line node (title + subtitle) the width must fit whichever line is longer: ``` width_needed = max(title_chars × 8, subtitle_chars × 7) + 24 ``` The `+ 24` is 12px of horizontal padding on each side. If the text is centered, that's 12px of clear air between the text edge and the rect stroke — looks balanced. For a single-line node: ``` width_needed = title_chars × 8 + 24 ``` For CJK content, replace the 8 with 15 and the 7 with 13. **Round up to the nearest 10px** for cleaner coordinates. A box that "needs" 167px becomes 170px. ### Worked examples - `"JWT authentication"` (18 chars, title) → `18 × 8 + 24 = 168` → round to **170** - `"Validates user credentials"` (26 chars, subtitle) in a two-line box with title `"Login service"` (13 chars, 8-wide) → `max(13 × 8, 26 × 7) + 24 = max(104, 182) + 24 = 206` → round to **210** - `"微服务架构"` (5 CJK chars, title) → `5 × 15 + 24 = 99` → round to **100** - `"发送 HTTP 请求 forwards request"` (mixed, treat CJK chars as 15px and Latin as 8px) → `3×15 + 4×8 + 13×8 = 45 + 32 + 104 = 181 + spaces + 24 ≈ 220` **Rule of thumb**: if you find yourself adding characters to a label, recheck the width. It's easy to write "Authentication" once, size the box to fit, then expand it to "Authentication service" and forget to resize. ## viewBox height calculation After placing every element, compute max_y: 1. For every ``, compute `y + height`. 2. For every ``, the bottom is `y + 4` (roughly — 4px descent below the baseline). 3. For every ``, it's `cy + r`. 4. `max_y` is the maximum across all of these. 5. `H = max_y + 20` **Don't guess.** A 20px buffer below the last element keeps things tight without clipping descenders. **Don't leave huge empty space at the bottom.** If the diagram ends at y=280, H should be 300 — not 560. Excess whitespace below the content feels like a rendering bug. ## Tier packing (horizontal rows) When placing N boxes in a horizontal row, check total width before you start writing coordinates: ``` total = N × box_width + (N - 1) × gap ``` - `box_width`: chosen to fit the longest label in that row - `gap`: 20px minimum between adjacent boxes Required: `total ≤ 600` (the usable width). If not, one of three things must give: 1. **Shrink box_width** — cut subtitles, shorten titles 2. **Wrap to 2 rows** 3. **Split into an overview diagram + detail diagrams** (one per sub-flow) ### Worked example — four consumer boxes - 4 boxes, each containing `"Consumer N"` (10 chars title) and `"Processes events"` (16 chars subtitle) - Needed width per box: `max(10×8, 16×7) + 24 = max(80, 112) + 24 = 136` → round to **140** - 4 boxes × 140 + 3 gaps × 20 = 560 + 60 = **620** → **overflows** by 20 - Fix: shrink to **130** each (check: `max(10×8, 16×7) + 24 = 136`, so 130 is tight — cut subtitle to 14 chars or drop it entirely) - Or: drop to 3 boxes per row, stack the fourth below - Or: 4 × 130 + 3 × 20 = 580, centered at `x = 40 + (600-580)/2 = 50` → boxes at x = 50, 200, 350, 500 ## Swim-lane (sequence) layout Sequence diagrams have their own coordinate system — actor columns ("lanes") along the top, dashed lifelines running down, numbered horizontal messages between them. The numbers below are fixed conventions so every sequence diagram in the skill feels consistent. ### Lane widths and centers N = number of actors. Lane width `lane_w`, header rect width `header_w = lane_w − 20`, lifeline x = lane center. Gap between lanes = 20. | N | lane_w | header_w | First offset | Lane centers (x) | |---|--------|----------|--------------|--------------------------------| | 2 | 290 | 270 | 40 | 185, 495 | | 3 | 186 | 166 | 40 | 133, 340, 547 | | 4 | 140 | 120 | 30 | 100, 260, 420, 580 | | 5 | 112 | 92 | 26 | 82, 214, 346, 478, 610 | | 6 | 100 | 80 | 30 | 80, 190, 300, 410, 520, 630 | Hard cap: **4 actors** (comfortable), **6 actors** (maximum). 5 is allowed only when every actor title is ≤12 characters and has no subtitle. 6 requires every title ≤9 characters (`9 × 8 + 24 = 96`, just inside `header_w=100`) and no subtitle. N=6 also collapses the intra-lane gap (the 20px padding between `lane_w` and `header_w` becomes 0 — the headers pack shoulder-to-shoulder) and borrows 10px of left-edge margin and all of the right-edge margin: the rightmost header rect sits flush against x=680. Use N=6 only when every actor is genuinely essential — otherwise merge actors or split the diagram. More than 6 doesn't fit 680px legibly. ### Vertical geometry (fixed for all sequence diagrams) ``` actor header box y = 40..88 (height 48, rx 8) title (th) y = 60 (dominant-baseline="central") role subtitle (ts) y = 78 (optional, one line, ≤16 chars) lifeline start y = 92 first message row arrow_y[0] = 120 message row pitch 44 px → arrow_y[k] = 120 + 44 · k lifeline tail y = last_arrow_y + 24 ``` ### viewBox height ``` header_bottom = 88 first_arrow_y = 120 last_arrow_y = 120 + 44 × (M − 1) # M = number of messages lifeline_bottom = last_arrow_y + 24 note_bottom = lifeline_bottom + 16 + 48 # only if side note is at the bottom H = max(lifeline_bottom, note_bottom) + 20 ``` ### Message arrows Each message is a horizontal `` at `y = arrow_y[k]`, running from the sender lifeline x to the receiver lifeline x with a 6px offset so the arrowhead doesn't touch the dashes: - Left-to-right: `x1 = sender_x + 6`, `x2 = receiver_x − 6` - Right-to-left: `x1 = sender_x − 6`, `x2 = receiver_x + 6` Use `class="arr-{ramp}"` where `{ramp}` matches the sender's actor ramp. Always set `marker-end="url(#arrow)"`. ### Message labels Label sits **above** the arrow at `y = arrow_y − 10` with `text-anchor="middle"`, `class="ts"`, and the number prefix baked into the text (`"1. Click login"`, not separate tspans). Label x is the midpoint of sender and receiver: `(sender_x + receiver_x) / 2`. Max characters that fit in a label between two adjacent lanes (e.g. lanes 1↔2 for N=4): ``` label_chars_max ≈ (|sender_x − receiver_x| − 8) / 7 ``` For N=4 adjacent lanes (|Δx|=160): ~21 chars. For the longest cross-diagram leap (lane 1↔4, |Δx|=480): ~67 chars. In practice, aim for ≤30 chars — short messages read faster, even when there's room. ### Self-messages A self-message is a 16×24 `` using the actor's `c-{ramp}` class, horizontally centered on the lifeline: `rect_x = lifeline_x − 8`, `rect_y = arrow_y − 12`. The label sits **to the left** of the rect (so it stays inside the diagram for the rightmost lane) at `x = rect_x − 8` with `text-anchor="end"`, `class="ts"`, `dominant-baseline="central"`, and the same numbered format. ### Side note A `class="box"` rect with a `th` title and optional `ts` subtitle. Default size 240×48, `rx="10"`. Placement depends on N: - **N ≤ 3**: top-left at `(40, 40)` — tucks beside the first actor header, no overlap because `first_offset ≥ 40`. - **N ≥ 4**: bottom-left at `(40, lifeline_bottom + 16)` — the top is packed tight against the leftmost header, so use the bottom margin. ### Worked example — OAuth 2.0 (N=4, M=10) ``` lane centers 100, 260, 420, 580 (lane_w=140, header_w=120) header_bottom 88 first_arrow_y 120 last_arrow_y 120 + 44×9 = 516 (10 messages) lifeline_bottom 540 note_bottom 540 + 16 + 48 = 604 (note at bottom) H 604 + 20 = 624 viewBox "0 0 680 624" ``` Label budgets: adjacent lanes (|Δx|=160) fit ~21 chars; "Redirect with client_id, scope" at 30 chars needs `|Δx| ≥ 218`, so it must span at least 2 lanes — put it between Client app (260) and Auth server (420), |Δx|=160 — **too tight**. Shorten to "Redirect (client_id + scope)" (28 chars, still over). Shorten further to "Redirect + client_id" (20 chars). Always verify every label against its span before finalizing. ## Centering inside the safe area For N boxes of width `w` with gap `g`: ``` total = N × w + (N - 1) × g offset = 40 + (600 - total) / 2 box[i].x = offset + i × (w + g) ``` Centered content feels deliberate. Left-aligned content feels incomplete. ## Sibling subsystem containers (2-up) The structural subsystem-architecture pattern (see `structural.md` → "Subsystem architecture pattern") places **two sibling dashed-border containers** side by side, each holding a short internal flow. The 2-up layout sacrifices the default 40px horizontal safe margin in exchange for a usable container interior wide enough to hold 3–4 flowchart nodes. Standard 2-up geometry at viewBox width 680: ``` container_w = 315 # each sibling container_gap = 10 # space between siblings left_margin = 20 # leaner than the usual 40 container_A.x = 20 container_B.x = 345 # left_margin + container_w + container_gap rightmost edge = 345 + 315 = 660 # leaves 20px to x=680 interior_w = 315 − 40 = 275 # after 20px padding on each side interior_A.x = 40 interior_B.x = 365 ``` Inside each 275-wide interior, node widths follow the normal text-width formula but should cap at ~235 so the L-bend connectors have clear vertical channels on both sides. Typical nodes are 180 wide centered, or 150 wide for a 2-column row. A **labeled cross-system arrow** bridges the 10px container gap: ``` from B_right_node.right_edge → to A_left_node.left_edge arrow_x1 = interior_B.rightmost_node.x + 10 arrow_x2 = interior_A.leftmost_node.x − 10 arrow_y = matching row y # both nodes on the same tier label at (x_mid, arrow_y − 6), class="ts", text-anchor="middle" ``` The cross-system arrow label is the one place where a flowchart-style connector *must* have a label — the reader cannot infer the relationship from the container titles alone. Keep the label to ≤3 words. Dashed container borders use **inline `stroke-dasharray="4 4"`** on the `` rather than a CSS class — dashed is a one-off container-styling concern, not a reusable shape property, so the template's class list stays stable. ### Worked example — Pi session + Background analyzer Two siblings, each with 3 internal nodes stacked vertically: ``` container A "Pi session" container B "Background analyzer" x=20, y=40, w=315, h=260 x=345, y=40, w=315, h=260 rx=20, stroke-dasharray="4 4" same styling Inside A (x_center = 177): Inside B (x_center = 502): [ User input ] y=96 [ Analyze patterns ] y=96 [ Pi responds ] y=170 [ Write summary ] y=170 [ (session ends) ] y=244 [ Update memory ] y=244 Cross-system arrow: B top row → A top row at y=118 x1 = 347 (container B left edge + 2)? No — from interior node B₁ left edge Actually: from B₁.x − 10 (headed right-to-left pointing at A₁) Details below in structural.md's worked example. ``` viewBox H for 3 stacked rows: last node bottom y = 244 + 44 = 288; container bottom 40 + 260 = 300; H = 300 + 20 = **320**. ## Sub-pattern coordinate tables Fixed coordinate tables for the sub-patterns introduced in `structural.md`, `illustrative.md`, and `sequence.md`. Use these values verbatim — they're tuned so each sub-pattern respects the 680 viewBox and the 40 safe margin. ### Bus topology geometry For `structural.md` → "Bus topology sub-pattern". A central horizontal bar bridges N agents above it and N agents below it. | Element | Coordinates | |----------------------------|------------------------------------------| | Bus bar | `x=40 y=280 w=600 h=40 rx=20` | | Bus label baseline | `y=304`, `text-anchor="middle"` at x=340 | | Top agent row (box top) | `y=80`, `h=60` | | Bottom agent row (box top) | `y=400`, `h=60` | | Default viewBox H | 500 | Agent centers by row count: | Agents per row | Box width | Centers (x) | |----------------|-----------|--------------------------------------------| | 2 | 180 | 180, 500 | | 3 | 140 | 170, 340, 510 | | 4 | 110 | 120, 260, 420, 560 | Publish/subscribe arrow pair: two vertical lines per agent, offset by 8px horizontally (one at `agent_cx − 8`, one at `agent_cx + 8`). Publish goes down from agent to bar; Subscribe goes up from bar to agent. Labels sit at the channel midpoint y, `text-anchor="end"` (Publish on left) and `text-anchor="start"` (Subscribe on right). ### Radial star geometry (3 / 4 / 5 / 6 satellites) For `structural.md` → "Radial star topology sub-pattern". A central hub surrounded by N peripheral satellites, each bidirectionally connected. **Hub (always fixed):** | Element | Coordinates | |---------------|----------------------------| | Hub rect | `x=260 y=280 w=160 h=80 rx=10` | | Hub center | `(340, 320)` | | viewBox H | 560 | **Satellite positions:** | N | Satellites (box top-left → w=160 h=60) | |---|-----------------------------------------------------------------------------------| | 3 | `(60, 120)`, `(460, 120)`, `(260, 460)` | | 4 | `(60, 120)`, `(460, 120)`, `(60, 460)`, `(460, 460)` | | 5 | `(260, 60)`, `(60, 200)`, `(460, 200)`, `(60, 460)`, `(460, 460)` | | 6 | `(20, 120)`, `(260, 60)`, `(500, 120)`, `(20, 460)`, `(260, 520)`, `(500, 460)` | Satellite centers: add `(80, 30)` to the box top-left to get `(cx, cy)`. **Arrow pairs:** each satellite connects to the hub with two single-headed arrows offset by 8 px perpendicular to the arrow direction. For a satellite at `(sx, sy)` and hub center `(340, 320)`, compute the direction vector, normalize to unit length, then offset each line by `(+4dy, −4dx)` and `(−4dy, +4dx)` respectively (where `(dx, dy)` is the unit direction). For straightforward placement, use these pre-computed endpoint offsets for N=4: | Satellite center | Outbound start (satellite → hub) | Outbound end | |------------------|----------------------------------|--------------| | TL `(140, 150)` | `(224, 176)` | `(264, 276)` | | TR `(540, 150)` | `(456, 176)` | `(416, 276)` | | BL `(140, 490)` | `(224, 464)` | `(264, 364)` | | BR `(540, 490)` | `(456, 464)` | `(416, 364)` | Inbound (hub → satellite) is the reverse of each outbound, offset perpendicular by 8. ### Spectrum geometry For `illustrative.md` → "Spectrum / continuum". A 1-D axis with end labels, tick points, option boxes, and italic captions. **Fixed vertical positions:** | Element | Coordinates | |---------------------------|--------------------------------------------| | Eyebrow (optional) | `y=50`, `text-anchor="middle"` at x=340 | | End labels (L / R) | `y=120`, at `x=80` (start) / `x=600` (end) | | Axis line | `x1=80 y1=140 x2=600 y2=140` | | Tick circles | `cy=140`, `r=6` | | Option box top | `y=200`, `h=60` | | Caption line 1 | `y=292`, italic `ts` | | Caption line 2 | `y=308`, italic `ts` | | Default viewBox H | 332 | **Tick and option-box x positions by tick count:** | Ticks | Tick centers (x) | Option box x (w=120) | |-------|-----------------------------|-----------------------------| | 2 | 200, 480 | 140, 420 | | 3 | 160, 340, 520 | 100, 280, 460 | | 4 | 140, 280, 420, 560 | 80, 220, 360, 500 | | 5 | 120, 240, 360, 480, 600 (w=100) | 70, 190, 310, 430, 550 | Axis uses `marker-start` **and** `marker-end` (both ends arrowed to signal no natural direction). Axis stroke class is `arr` (neutral gray) — never a color ramp. Sweet-spot tick and its option box use `c-green` or `c-amber`; all other ticks and boxes stay `c-gray`. Captions cap at 24 characters per line, 2 lines per option. ### Parallel rounds geometry For `sequence.md` → "Parallel independent rounds". Stacked independent call/response rounds without shared lifelines. **Stacked rounds variant (full-width):** | Element | Coordinates | |----------------------|----------------------------------------| | Source actor | `x=60 y=120 w=120 h=60` | | Round k action box | `x=340 y=100 + (k-1)×80 w=160 h=48` | | Round k call arrow | `y = 124 + (k-1)×80` | | Round k response | `y = 134 + (k-1)×80` | | Call label | `y = 115 + (k-1)×80` | | Response label | `y = 145 + (k-1)×80` | | Row pitch | 80 | Arrow x endpoints: `x1 = source_right_edge + 6 = 186`, `x2 = action_left_edge − 6 = 334` (going right); reverse for the return arrow. **Script-wrapper variant:** | Element | Coordinates | |----------------------|----------------------------------------| | Source actor | `x=60 y=120 w=120 h=60` | | Script wrapper | `x=240 y=100 w=140 h=260 rx=6` | | `{ }` label | `(310, 124)`, class `th` | | Divider line | `x1=256 y1=140 x2=364 y2=140` | | Wrapped band k | `x=252 y=152 + (k-1)×60 w=116 h=48` | | External action box | `x=440 y=200 w=160 h=48` (optional) | | Default viewBox H | 380 | Up to 3 wrapped bands inside the script wrapper (with `script_h = 260`). Grow `script_h` by 60 for each additional band. **Inside a 315-wide subsystem container** (half-width), scale the coordinates: | Element | Container A (x=20) | |--------------------|---------------------------------| | Source actor | `x=40 y=120 w=100 h=50` | | Round k action | `x=200 y=104 + (k-1)×64 w=120 h=40` | | Row pitch | 64 | ### Multi-line box body line heights For `structural.md` → "Multi-line box body". A three-line box used for advisor-role labels: title + italic role + meta. | Line | y-offset from `rect_y` | Class | |------------|--------------------------|---------------| | Title | `+22` | `th` | | Role | `+42` | `ts` + italic | | Meta | `+62` | `ts` muted | | Min h | 80 | | Row pitch is 20. For a box at y=140, lines sit at `162, 182, 202`. Box height never drops below 80 — if the role or meta line is absent, use a standard 56-tall two-line box instead, don't shrink a three-line box. ### Annotation circle on connector geometry For `illustrative.md` → "Annotation circle on connector". A labeled circle sitting above a pair of bidirectional arrows between two subjects. | Element | Coordinates | |-----------------------|----------------------------------------------| | Subject A box | `x=80 y=160 w=180 h=56` | | Subject B box | `x=440 y=160 w=180 h=56` | | A → B arrow | `(266, 184) → (434, 184)`, `arr` | | B → A arrow | `(434, 204) → (266, 204)`, `arr` | | Arrow pair y midpoint | 194 | | Annotation circle | outer `c="box"` r=30 at `(cx, cy) = (350, 150)` | | Inner pill | `x=cx−28 y=cy+12 w=56 h=20 rx=10` (uses ramp)| | Connector line | `(cx, cy+30) → (cx, 194)`, `arr` (no marker) | Label goes inside the pill at `(cx, cy+26)`, `th`, centered. Subject A/B labels use `th` at `(subject_cx, subject_cy)`. ### Container loop geometry (simple flowchart) For `flowchart.md` → "Loop container". An outer rounded rect framing a simple flowchart. | Element | Coordinates | |--------------------|--------------------------------------------| | Container rect | `x=20 y=40 w=640 h=H−60 rx=20`, class `box`| | Title | `(340, 72)`, class `th` | | Subtitle | `(340, 92)`, class `ts` | | First inner box y | ≥ 116 | | Inner safe area | `x ∈ [40, 620]` | | Bottom padding | 20px | After placing the inner flow, compute `inner_bottom = max y of any inner element`, then `H = inner_bottom + 40` and `container_h = H − 60`. ## Arrow routing Every arrow has a start point (source box edge) and an end point (target box edge), with an optional bend in between. **Direct arrow** — when source and target are aligned on an axis (same x for vertical, same y for horizontal): ```svg ``` Leave **10px** between the arrow's endpoint and the target box's top edge so the arrowhead doesn't touch the border. **L-bend arrow** — when the direct line would cross another box: ```svg ``` - `ymid` is a horizontal "channel" chosen to thread cleanly between the two levels - Always include `fill="none"` on `` used as a connector — SVG defaults to black fill **Collision check.** Before finalizing every arrow, trace its path against every rect you've already placed: ``` for each rect R in the diagram: if rect R is not the source or target of this arrow: does the arrow's line segment intersect the interior of R? if yes: use an L-bend to route around R ``` This is the #1 diagram failure mode. Arrows slashing through unrelated boxes. **Connection-point convention.** Every arrow anchors to the midpoint of a box edge (top, bottom, left, or right), never to a corner. With `rx="6"` or `rx="12"` rounded corners, the actual corner curve occupies the first ~6–12px of each edge — so stay **≥20px from any corner** when picking a connection point. An arrow that enters a box 4px from its corner reads as "touching the rounded curve" instead of "attached to the edge", and the arrowhead looks smeared. For a 180-wide rect, valid top-edge connection points sit at `x ∈ [rect_x + 20, rect_x + 160]`; the outer 20px on each side belong to the corner radius. **Multi-arrow stagger.** When two or more arrows terminate on the same edge of the same box from different sources (three fan-in arrows hitting a single aggregator; two feedback lines returning to a start node), stagger their arrival points by **≥12px** along the target edge. Example: three arrows fanning into the top edge of a 180-wide box sit at `target_x − 24`, `target_x`, `target_x + 24`. Same rule applies to horizontal arrows converging on a left/right edge (stagger the y by ≥12px). Without the stagger, the arrowheads stack on top of each other and the reader can't tell how many incoming edges there are — a 3-to-1 fan-in reads as a single thick arrow. ## text-anchor safety `text-anchor` controls which point of the text aligns to the `x` coordinate: - `"start"` — x is the left edge. Text extends right. - `"middle"` — x is the horizontal center. Text extends both ways. - `"end"` — x is the right edge. Text extends left. **Danger**: `text-anchor="end"` at low x values. If `x=50` and the label is 200px wide, the text starts at `x = -150` — outside the viewBox. Check: `label_chars × 8 < x` must hold for `text-anchor="end"` labels. **Default to `text-anchor="start"` on the left side and `"middle"` in the center.** Use `"end"` only when you've verified the x coordinate is high enough. ## Vertical text centering inside a rect Every `` inside a rect needs `dominant-baseline="central"`. Without it, SVG treats `y` as the baseline — the glyph body sits ~4px above where you think and descenders hit the next line. ```svg Login ``` For a rect at `(x, y, w, h)`: - Single line centered: `text_x = x + w/2`, `text_y = y + h/2`, `dominant-baseline="central"` - Two lines (44 + 14 layout inside a 56-tall box): - Title: `text_y = y + 20` (center of top 40px) - Subtitle: `text_y = y + 40` (center of bottom 16px) - Both use `dominant-baseline="central"` ## Quick-reference sizes Standard dimensions that work well and keep the diagram rhythm consistent: | Element | Size | |-----------------------------|----------------------| | Single-line node height | 44 px | | Two-line node height | 56 px | | Default node width | 180 px (adjust up) | | Minimum gap between nodes | 20 px horizontal, 60 px vertical | | Container padding | 20 px | | Rect corner radius | `rx="6"` (subtle), `rx="12"` (container), `rx="20"` (outer container) | | Connector stroke width | 1.5 px (set by `.arr`) | | Diagram border stroke | 0.5 px | | Viewport safe margin | 40 px on each edge | Stick to these unless you have a concrete reason to deviate.