# paranoid-passwd — Design System & Tokens This document turns [`brand.md`](./brand.md) §5 (palette, type scale, iconography, layout) into **concrete, named tokens** and **component specs** that three surfaces consume so they read as one product: - the **ratatui** theme module (CLI/TUI — `crates/paranoid-cli`), - the **Slint** styles (GUI — `crates/paranoid-gui/ui/paranoid.slint`), - the **Sphinx** theme (docs/download site — `docs/_static/custom.css`). It is the single source of truth for *values*. [`brand.md`](./brand.md) owns the *meaning* of each value; [`ia.md`](./ia.md) owns *where* each component appears. Nothing here is re-decided against brand.md — every token traces to brand.md §5, cited inline. Every token is **named by intent, not by appearance** (brand.md §5.2; research lens 0: "semantic color slots are named by function, not appearance"). > **Consumption rule.** A surface never hard-codes a hex, a size, or a spacing > step. It reads the token. When a value must change, it changes here once and all > three surfaces move together. The current drift (§7) — where the TUI, Slint, and > CSS carry three slightly different palettes — is exactly the failure this rule > exists to prevent. --- ## 1. Color tokens The canonical values are the ones already correct in `docs/_static/custom.css` and the ratatui constants, promoted here as authoritative (brand.md §5.2). Each token has **one meaning** and is used only for that meaning. A color never carries meaning alone — it always doubles a symbol or label so the monochrome layer works (brand.md §5.1, §5.4). | Token | Value | Meaning (brand.md §5.2) — used ONLY for this | |---|---|---| | `color.bg.base` | `#080c14` · `rgb(8,12,20)` | The ground. Near-black, low-glare; the whole surface. | | `color.bg.panel` | `#0d1119` · `rgb(13,17,25)` | A raised panel or focused region. | | `color.border` | `#17304b` · `rgb(23,48,75)` | Structure and separation between regions. | | `color.text.primary` | `#e4e7f2` · `rgb(228,231,242)` | Primary reading text. | | `color.text.muted` | `#95a0b8` · `rgb(149,160,184)` | Secondary text, labels, footer keys, hints. | | `color.status.verified` | `#34d399` · `rgb(52,211,153)` | **Verified / safe / passed.** Earned only after a real check. | | `color.accent.action` | `#60a5fa` · `rgb(96,165,250)` | **The one next thing to do.** One per screen, used sparingly. | | `color.status.danger` | `#f87171` · `rgb(248,113,113)` | **Danger / failure / irreversible.** A stop, never decoration. | | `color.status.caution` | `#fbbf24` · `rgb(251,191,36)` | **Attention / unverified / in-between** — a state to resolve. | Two hard disciplines from brand.md §5.2, encoded as token-usage rules: - **`color.status.verified` is earned.** No surface may use it as a default "all fine" wash; it appears only after an actual verification (binary matches release, randomness check passed, vault opened and trusted). - **`color.status.danger` is reserved.** Only real danger — verification failed, unlock failed, irreversible destruction. Overuse trains the persona to ignore it. There is **no `accent.secondary` and no decorative color.** The current TUI defines a `PURPLE`/`#a78bfa` constant and the Slint scaffold uses off-palette shades (`#f5d76e`, `#9fd4c9`, `#171d26`); none of these map to a brand.md meaning and all are removed in favor of the nine tokens above (see §7). ### 1.1 Monochrome + 16-ANSI fallbacks (mandatory, brand.md §5.1) Because a locked-down or remote terminal may render no truecolor — and because the real/decoy distinction must never depend on color — every color token declares a **16-ANSI fallback** and every meaning declares a **monochrome carrier** (the symbol/weight that conveys it with zero color). This is the L0 (monochrome) and L1 (16-color) of brand.md's three-layer discipline. | Token | 16-ANSI fallback | Monochrome carrier (the thing that still works with no color) | |---|---|---| | `color.bg.base` / `bg.panel` | default bg / reverse | panel = bordered box vs. unbordered ground | | `color.text.primary` | default fg | normal weight | | `color.text.muted` | bright-black (8) | dim modifier | | `color.status.verified` | green (2) | `✓` glyph + the word *verified/passed* | | `color.accent.action` | bright-blue (12) | `▸` marker + bold on the single next action | | `color.status.danger` | red (1) | `✗` glyph + the word *failed/danger* | | `color.status.caution` | yellow (3) | `!` glyph + the word *unverified/not set* | **Test (brand.md §5.1):** strip all color — the product must remain fully usable and the real/decoy vaults must remain indistinguishable. If a state reads *only* by its color, that is a defect: fix the carrier, not the palette. --- ## 2. Spacing scale One geometric-ish scale, four steps, so all three surfaces share rhythm. Named by role. In the TUI, `space.*` maps to **cells** (ratatui `Margin`/`Layout` constraints); in the GUI/docs, to **pixels/rem** (brand.md §5.5 "spatial consistency"). | Token | TUI (cells) | GUI (px) | Docs (rem) | Role | |---|---|---|---|---| | `space.tight` | 1 | 6 | 0.375 | Between a label and its value; intra-row. | | `space.snug` | 1 | 8 | 0.5 | Between stacked lines in a group. | | `space.base` | 2 | 14 | 0.875 | Between groups; default panel padding. | | `space.loose` | 3 | 22 | 1.375 | Between major regions; around the primary action. | Panel padding is `space.base`; the gap between the two panes of a browse screen is `space.base`; the gap around the single `accent.action` is `space.loose` so the one next move has breathing room (ia.md §1, "the next action is singular and visible"). --- ## 3. Type scale Four steps, from brand.md §5.3. Hierarchy comes from **weight and spacing more than size** (an austere instrument does not shout). The TUI expresses steps as bold/normal/dim modifiers; the GUI/docs as sizes. Monospace is mandatory for anything read character-by-character. | Token | GUI/docs | TUI modifier | Role (brand.md §5.3) | |---|---|---|---| | `type.title` | 20px / 1.25rem, semibold | `BOLD` | Screen title — one per screen, states its one job. | | `type.body` | 15px / 1rem, regular | *(normal)* | Primary content and reading text. | | `type.label` | 13px / 0.85rem, medium, `text.muted` | `DIM` | Field labels, footer keys, hints. | | `type.mono` | 15px / 1rem **monospace** | *(normal, mono cell)* | Secrets, hashes, phrases, generated passwords — anything read or copied character-by-character. | Font families: - **CLI/TUI:** monospace everywhere (the terminal's cell font). - **GUI/docs:** a neutral sans for prose (`type.title/body/label`) paired with a monospace for values (`type.mono`). The docs Sphinx theme and the Slint GUI must pick the *same* monospace so a hash reads identically across surfaces. `type.mono` is not merely a font choice — it is a safety property. A generated password or recovery phrase shown in a proportional font can render `l/1/I` or `O/0` ambiguously; `type.mono` guarantees every character is unambiguous (brand.md §5.3). --- ## 4. Component specs Each component is defined once, in intent terms, then mapped to the primitive each surface uses. This is the "note which token maps to which surface primitive" requirement made explicit. ### 4.1 Panel A raised, bordered region. The fixed layout skeleton (ia.md §1) is built from panels. | Property | Token | ratatui primitive | Slint primitive | Sphinx primitive | |---|---|---|---|---| | background | `color.bg.panel` | `Block.style(bg)` | `Rectangle.background` | `.pp-panel { background }` | | border | `color.border`, 1 | `Borders::ALL` + `border_style` | `border-color` + `border-width:1px` | `border: 1px solid` | | radius | (GUI/docs only) 6px / 0.5rem | *(n/a — cells)* | `border-radius:6px` | `border-radius` | | padding | `space.base` | `Margin` | `padding` | `padding` | The **focused** panel borders in `color.accent.action`; unfocused panels border in `color.border`. This is how the eye finds the active region without the layout moving (ia.md §1, rule 4). ### 4.2 Primary action (the one next move) Exactly one per screen (ia.md §0 rule 5). It is the only element that may use `color.accent.action`. | Property | Token | ratatui | Slint | Sphinx | |---|---|---|---|---| | marker | `▸` glyph | prefix `▸ ` on the line | leading `▸` Text | `::before { content:"▸" }` | | color | `color.accent.action` | `fg(BLUE)` | button accent color | `.pp-action { color }` | | weight | bold | `Modifier::BOLD` | `font-weight:600` | `font-weight:600` | | space around | `space.loose` | `Margin` | `padding`/`spacing` | `margin` | Secondary actions render in `type.body` / `color.text.primary` with **no** accent — so the single blue target is unmistakable (brand.md §5.2 "if everything is blue, nothing is"). ### 4.3 Input / secret field For passphrase entry and item fields. | Property | Token | ratatui | Slint | Sphinx (docs demos only) | |---|---|---|---|---| | text | `type.mono`, `color.text.primary` | mono line, `fg(TEXT)` | `TextInput` mono | `input[type=password]` mono | | label | `type.label`, `color.text.muted` | `DIM` line above | `FieldLabel` | `