# arborist VIZ — Style Guide Legibility and ergonomics first. Hashes, proofs, and audit chains are read under fatigue. Every choice here optimizes for "still readable at 2am after six hours of incident response." Tokens live in `arborist_viz/static/css/viz.css` under `:root`. Treat that file as the single source of truth; this document explains *why* each value is what it is and *when* to break the rules. --- ## 1. Color ### 1.1 Palette | Token | Value | Role | Contrast on `--bg` | | ---------------- | --------- | --------------------------------- | ------------------ | | `--bg` | `#0b0e12` | Canvas. Near-black, not pure. | — | | `--bg-elev` | `#11161c` | Header, sidebars, widget shell. | — | | `--bg-elev-2` | `#161d25` | Widget head, buttons, chips idle. | — | | `--border` | `#1f2934` | Hairline dividers. | decorative only | | `--fg` | `#d6e0ec` | Primary text. | **14.0 : 1 (AAA)** | | `--fg-dim` | `#8aa1b8` | Secondary text, labels. | **6.7 : 1 (AA)** | | `--fg-mute` | `#7a8ea3` | Tertiary, captions, separators. | **5.0 : 1 (AA)** | | `--accent` | `#4dd1c1` | Mint. Roots, links, live state. | **8.9 : 1 (AAA)** | | `--warn` | `#f5b455` | Amber. Stale, waiting, caution. | **9.7 : 1 (AAA)** | | `--bad` | `#e96b6b` | Red. Failed, quarantined. | **5.7 : 1 (AA)** | | `--pulse` | `#7aa2ff` | Blue. Proof pulse, hover halo. | **7.0 : 1 (AA)** | > **Note:** `--fg-mute` was `#5e7081` (3.8 : 1) — below WCAG AA. Lighten to > `#7a8ea3` or do not use it for text. The old value is fine for icons, > decorative borders, or text 16px+ bold. ### 1.2 Color rules - **Never use pure black `#000` or pure white `#fff` on this surface.** Pure-black-on-dark backgrounds vanish; pure-white text on near-black burns the eyes at low ambient light. Stay inside the palette. - **Hue carries semantics, never decoration.** Mint = healthy/live/root. Amber = stale/waiting. Red = failed. Blue = pulse on a proof event. Do not introduce a fourth status hue without updating this table. - **Never encode information in color alone.** Every red badge gets a glyph or word ("fail"). Every amber chip pairs with text ("stale 4h"). Color is reinforcement, not channel. - **Avoid color combinations known to fail.** No saturated red on saturated blue (chromostereopsis). No yellow-on-white. No cyan-on-white. No green next to red without a glyph (~8% of men are red/green color-blind). - **Pills, buttons, chips: idle is `--fg-dim`.** Color (accent/warn/bad) signals state change. A bar full of green chips means nothing because green stops being a signal. ### 1.3 Contrast minimums | Use | Minimum ratio | Source | | --------------------- | ------------- | ---------------- | | Body text < 18px | 4.5 : 1 | WCAG 2.2 AA | | Body text ≥ 18px bold | 3.0 : 1 | WCAG 2.2 AA | | Hashes (monospace) | 5.0 : 1 | project rule | | Focus ring on field | 3.0 : 1 | WCAG 2.2 AA | | Icon / glyph | 3.0 : 1 | WCAG 2.2 AA | Hashes get a stricter rule than WCAG because every character matters. A mistransposed hex char is a different commitment, not a typo. --- ## 2. Typography ### 2.1 Stacks ```css --sans: -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif; --mono: ui-monospace, "JetBrains Mono", "Fira Code", Consolas, monospace; ``` System fonts only. No web-font network round-trip, no FOUT, no GDPR disclosure for a remote font CDN. ### 2.2 Sizes | Role | Size | Family | Weight | Notes | | ----------------------------- | ---------- | -------- | ------ | -------------------------------- | | Body | **14 px** | sans | 400 | was 13 — bump for fatigue. | | Widget body text | 14 px | sans | 400 | | | Hash, code, audit row | **13 px** | mono | 400 | was 11.5 — hashes need bulk. | | Section label, table header | 12 px | sans | 500 | uppercase 0.08em letter-spacing. | | Chip, pill, button caption | 12 px | mono | 500 | was 10.5 — too small to skim. | | Footer meta, timestamps | 12 px | mono | 400 | | | H1 / page title | 18 px | sans | 600 | | **Floor:** nothing below 12px. Hashes never below 13px. Audit chain rows get monospace so columnar scanning works. ### 2.3 Line and letter - `line-height: 1.5` for body, `1.4` for dense tables. - `letter-spacing: 0.05em` for uppercase tags. Uppercase without tracking reads as a wall. - **Never `word-break: break-all`.** Atomic identifiers do not wrap mid-token. See §4.4 for the correct hash-display modes. --- ## 3. Spacing A 4-pixel base unit. Multiples only. No `0.27rem` magic numbers. | Step | Value | Use | | ---- | -------- | ------------------------------------ | | 1 | 4 px | Icon-to-text gap, chip padding-y. | | 2 | 8 px | Default inline gap. | | 3 | 12 px | Widget body padding. | | 4 | 16 px | Section padding, sidebar padding. | | 6 | 24 px | Page-level gutters. | | 8 | 32 px | Headline blocks. | Stack values vertically with the larger of the two adjacent margins; do not collapse margins manually. --- ## 4. Components ### 4.1 Buttons and clickable chips - Minimum hit target: **32 × 32 px** (WCAG 2.5.8 AA). The Merkle command center is dense; do not go below this. - Visible focus ring on every focusable element. Use `outline: 2px solid var(--pulse); outline-offset: 2px`. Do not `outline: none` without a replacement. - Hover ≠ focus. Hover may add a border-color change; focus must add a ring visible to keyboard users. - Cursor: `pointer` only on actually clickable elements. Never on text. ### 4.2 Inputs (hash-drop bar, search, filter) - 36 px height minimum. - Placeholder color: `--fg-mute`. Never lighter — placeholders that disappear in low light are a known failure mode. - Focus: border becomes `--accent`, ring becomes `--pulse`. Two signals. - Invalid (`:invalid`) gets `border-color: var(--bad)` *only after first blur*. Live red-on-keystroke is hostile. ### 4.3 Widgets - Card: `--bg-elev` background, `--border` hairline, 6 px radius. - Header strip: `--bg-elev-2`, monospace type tag in `--accent`, sans title in `--fg`. - Footer: `--bg`, monospace meta in `--fg-mute`. Collapsible `
` for query/source attribution (every widget exposes the roots it derived from — §19.3 of ticket). - Empty state: italic `--fg-mute` body text. Never a blank box. ### 4.4 Atomic identifiers (hashes, CIDs, signatures, tokens) An **atomic identifier** is any opaque string where every character is load-bearing and the whole value is read as one symbol. This includes: - SHA-256, SHA-512, BLAKE3, SHA-1 hex digests - IPFS CIDs, multihashes - Ed25519 / RSA signatures, public keys, SSH fingerprints - UUIDs, ULIDs, KSUIDs - JWTs, OAuth tokens (when shown to a human) - Git commit SHAs, content-addressed blobs #### The core rule **Atomic identifiers never wrap mid-token.** A SHA-256 split across lines at column 22 in one place and column 17 in another defeats the only useful thing humans do with hashes: visual diff. If two hashes wrap at different column positions, the eye cannot align them — every comparison degrades to a character-by-character scan. Pick exactly one of the three display modes below. Do not mix. #### Mode A — one-line scroll (preferred for inline use) ```css .id-inline { font-family: var(--mono); white-space: pre; /* no wrap, no collapse */ overflow-x: auto; /* horizontal scroll if narrow */ overflow-wrap: normal; word-break: normal; } ``` The hash is one continuous string. If the container is narrower than the hash, the user scrolls horizontally. Always pair with a visible click-to-copy button — scrolling to read 64 chars is worse than copying. Use for: table cells, list rows, inline references in prose. #### Mode B — grouped chunks (preferred for prominent display) ``` 38f82216 97ee54dd b195a13f b9a375d5 6aab5deb 3eee9213 523b4224 b1042270 ``` Render the hash as 8 chunks of 8 hex chars separated by two spaces. Wrap only at chunk boundaries. Two hashes displayed this way **line up column-for-column**, which is the entire point. ```css .id-block { font-family: var(--mono); font-size: 13px; line-height: 1.5; white-space: pre-wrap; /* wrap at whitespace only */ word-break: keep-all; /* never split a chunk */ overflow-wrap: normal; padding: 0.5rem; background: var(--bg); border: 1px solid var(--border); } ``` Render server-side as `chunks.join(" ")` with two spaces (or one en-space ` `). Do not use single space — single-space-separated hex still looks like one token at a glance. Use for: the `.root-hash-block` card, proof page hero, anywhere the hash itself is the page subject. #### Mode C — truncate with explicit reveal ``` 38f82216…b1042270 [copy] [show full] ``` First 8 + ellipsis + last 8 hex chars. Always pair with: - `title=""` for hover (desktop) - a visible click-to-copy button - a "show full" affordance (`
` or panel expansion) Use for: dense lists where Mode A would still overflow, chrome bar pills, breadcrumbs, sidebar nav entries. #### Floor rules - **Never truncate below 8 + 8 = 16 hex chars displayed.** Below that, visual collision risk gets real (a malicious near-collision can match the first 6 chars). - **Never truncate without a copy affordance.** Truncated hashes are not selectable into a useful value. - **Never show only first-N truncation** (`38f82216…`). Always show first AND last segments — first-only loses suffix entropy for diff. - **Click-to-copy uses the navigator.clipboard API** with a visible toast on success. No silent copy. #### What this rule applies to outside this app The reasoning generalizes — any system showing opaque identifiers to a human should follow these modes: - Block explorers (TX hashes, addresses) - Git interfaces (commit SHAs, blob hashes) - Container registries (image digests) - Cert viewers (SHA-256 fingerprints) - Package managers (integrity hashes) If you find yourself writing `word-break: break-all` on an identifier, you have chosen comfort for the layout engine over legibility for the human. Reverse the choice. ### 4.5 Status dots 8 × 8 px circle. Always accompanied by text label within 8px to its right. Glow (`box-shadow: 0 0 6px `) only on `dot-on` to make "live" pop without animating. ### 4.6 Tables (audit chains, leaf strips) - Monospace. - Zebra striping `rgba(255,255,255,0.02)` on odd rows — almost invisible but enough to anchor the eye on long lists. - Sticky header on scroll. - Per-row hover background `var(--bg-elev-2)`. Click target is the whole row, not just a link in one column. --- ## 5. Motion Default: **none.** - Status changes are step-functions. A chip flips color instantly. - Acceptable motion: a 200ms ease on `:hover` border-color changes; one-shot 400ms pulse on a fresh proof event. - No spinners on SSE streams — use a dot-on glow plus the last-event timestamp instead. Spinning indicates "we are waiting"; this app is always waiting on the audit chain. - Respect `prefers-reduced-motion`. Wrap every transition or animation in `@media (prefers-reduced-motion: no-preference) { … }`. --- ## 6. Accessibility checklist Run before merging any view template: - [ ] Every interactive element reachable by Tab. - [ ] Visible focus ring on every focusable element. - [ ] No information conveyed by color alone. - [ ] All text passes its required contrast ratio (§1.3). - [ ] Hit targets ≥ 32 × 32 px. - [ ] `
`, `