She figures out the middle part. Dark, precise, direct. Built for work that happens in the background and surfaces only what matters.
The non-negotiable foundation. Every surface built on Holograph honours these decisions. Rules, not suggestions.
Holograph is the design system for Petunia — an AI agent that takes goals and figures out the middle part. Dark, iridescent, precise. Built for interfaces where work runs in the background and only what matters surfaces.
No light mode. No soft rounded cards. No bright fills. Not chatty, not eager, not decorative for its own sake. Copy does not end with a question. It does not say "Great!" It reports, acts, and flags — then stops.
A dark palette built for depth and precision. Pure contrast-tested values across three tiers: primitive → semantic → component. The iridescent accent is the signal color — reserved for moments of resolution and completion.
All text color tokens are tested against their intended background. Body text achieves WCAG AAA (7:1+). Label text achieves WCAG AA (4.5:1+). Decorative borders use rgba opacity — these are never used for text.
Three-tier token architecture: primitive (raw values) → semantic (purpose-based) → component. Always reference semantic tokens in components — never primitives directly.
Components must reference semantic tokens (e.g. --text-primary) never primitive tokens (e.g. --color-neutral-100). This enables theming without touching component code.
Three typefaces, three distinct roles. Bodoni Moda for display — extreme thick-thin contrast mirrors iridescent petal edges; italic form is the signature voice. IBM Plex Sans for all prose — proportional, warm, genuinely readable at any length. IBM Plex Mono for labels, data, code — the precision instrument voice. Sans and Mono share identical metrics by design.
Bodoni Moda — headings ≥28px only (H1–H3, card names, large metrics). Always set font-optical-sizing: auto. Forbidden zone: never use weight 300–400 between 18px–26px — hairlines disappear. Use weight 700 or jump to ≥28px.
IBM Plex Sans — all running prose, descriptions, UI copy, card body text, form labels, alert messages, nav links. 16px / 1.55 leading. text-secondary minimum (8.9:1).
IBM Plex Mono — UI labels (all-caps, tracked), data values, token names, code, annotations, eyebrows. Never for prose. Plex Sans and Plex Mono share the same x-height and weight scale — they pair invisibly.
Six variants · five sizes · live states. Click any button — the loading and confirm states are wired.
Primary — one per view max. Secondary — supporting actions. Outline — neutral alternatives. Ghost — lowest emphasis, dense layouts. Amber — consequential actions. Danger — destructive only. Always pair destructive actions with a confirm step.
All form elements share consistent focus rings, error states, and accessibility attributes. Labels are always visible — no placeholder-only labeling.
Flexible surface component for containing related content. Four variants: default, interactive, iris-accented, amber-accented. The Record Card is the system's signature component — Petunia's primary entity view.
Standard surface for grouping related content. Background is bg-surface with a subtle 1px border. Use for non-interactive content blocks.
Adds hover: border brightens, shadow appears, slight lift on translateY. Use for navigable content, clickable tiles, or selectable items.
Iridescent 2px top border. Flowing gradient animation — the system's signature accent. Use for featured, active, or highlighted items only. One per view.
The Record Card is the definitive expression of the Holograph aesthetic — display typography, monospace metadata, dotted leader lines, and the iris accent. It is Petunia's primary entity view: a task, a run, a goal, a contact, a job, an order. The structure is fixed; the content is yours.
Alerts, toasts, modals, badges, and loading states. Use alerts for persistent messages, toasts for transient feedback, modals for focused tasks requiring a response.
Metric cards, tables, progress indicators, avatars, and skeleton loaders. The system handles data-dense contexts well — dashboards, run logs, goal tracking, monitoring surfaces, consumer apps.
| Run ID | Goal | Duration | Steps | Status | |
|---|---|---|---|---|---|
| RUN-0047 | Q4 Outreach | 4m 12s | 7 / 7 | Done | |
| RUN-0048 | Lead research | — | 3 / 5 | Blocked | |
| RUN-0049 | Inbox triage | 1m 03s | 4 / 4 | Running | |
| RUN-0050 | Calendar prep | — | 0 / 3 | Error |
Reusable solutions for common interaction scenarios. Not individual components but combinations — how components work together to solve recurring design problems.
Give me a goal and a deadline. I'll figure out the middle part.
All destructive actions (delete, reset, overwrite) must use the Confirmation Dialog pattern. The confirm button uses btn-amber (not btn-primary) to signal consequence. Include a brief description of what cannot be undone.
Define a goal. Petunia will decompose it and get started. Fields marked * are required.
Every interactive component has a mandatory state matrix. Implementing only the default state is a bug. Each state must be visually distinct — a user must never have to guess whether their action was registered.
Every interactive component must implement: default, hover, active (pressed), focus-visible, disabled. Where applicable: loading, error, success, selected/checked. Focus rings must always use --border-focus with 2px solid outline and 2px offset. Never remove focus outlines — never use outline: none without a custom focus ring replacement.
| State | Visual Change | Token / Value | Transition |
|---|---|---|---|
| Default | bg-teal-500, text-white | --interactive-default | — |
| Hover | bg lightens, no transform | --interactive-hover | 100ms ease |
| Active (pressed) | bg darkens, scale(0.98) | --interactive-active | 100ms ease |
| Focus-visible | 2px teal outline, 2px offset | --border-focus, --shadow-teal | 100ms ease |
| Disabled | opacity 0.4, cursor not-allowed | --interactive-disabled | none |
| Loading | Spinner replaces icon/text, width locked | spin animation 600ms linear | instant swap |
| Success (transient) | Green fill, check icon, 1.5s then reset | --status-success | 180ms ease, auto-reset |
Resting state, no interaction
Border lifts, slight elevation
Teal border + tint
| State | Visual | Token |
|---|---|---|
| Default | brand-teal, no underline | --brand-teal |
| Hover | underline appears, color lightens slightly | --interactive-hover, text-decoration underline |
| Active | color darkens to teal-400 | --color-teal-400 |
| Focus-visible | Teal outline ring | --border-focus |
| Visited | violet-300 (softer, distinct from active) | --brand-violet |
| Disabled | text-disabled, cursor default, no pointer | --text-disabled |
Four easing curves. Three durations. Every transition in the system uses one of these — nothing else. Hover each card to see the curve in action.
Every async operation must provide visual feedback. Users must never face silent waits or ambiguous states. The decision tree below is deterministic — follow it every time.
| Situation | Use | Why |
|---|---|---|
| Page or section initial load | Skeleton | Content shape is known — show the layout before data |
| List / table data loading | Skeleton rows | Preserves layout, reduces perceived wait time |
| Button action in progress | Inline spinner | Action is point-in-time; layout doesn't change |
| Background sync / auto-save | Subtle spinner in status bar | Non-blocking; user can continue working |
| File upload / long operation | Progress bar | Duration is measurable; progress is meaningful |
| Full page route transition | Top progress bar (NProgress) | Global indicator, doesn't block content |
| Async validation (input) | Spinner inside input | Scoped to the field being validated |
The data could not be retrieved. This may be a temporary issue — try refreshing.
Section errors — use the pattern above. Never crash the full page for a section failure. Always offer retry. Always give a human-readable message (never expose raw error codes to users).
Full page errors — same pattern, full-page centred, add navigation back to home. Log error to your observability tool.
Form submission errors — use Alert component (alert-error) above the form. Never toast only — the user must be able to see the error context.
Inline validation errors — field-level (see Form Validation section below).
Validation timing, error message format, and field-level feedback are specified here. Inconsistent validation is the single largest source of frustration in form UX. Follow these rules exactly.
| When | Action | Why |
|---|---|---|
| On type (live) | Only show success state — never errors while typing | Errors while typing are aggressive and interrupt flow |
| On blur (field exit) | Show error if field is invalid AND has been touched | Primary validation trigger. User has left, review is expected. |
| On submit | Validate all fields, show all errors, focus first error | Catches untouched required fields |
| On re-focus of error field | Keep error visible, clear on correction | Helps user understand what to fix while typing |
| On correction | Clear error immediately on valid input | Instant positive reinforcement |
| Async validation | Trigger after 400ms debounce on blur | Avoids hammering the server; feels responsive |
Error messages follow Petunia's voice: precise, direct, actionable. Tell them what's wrong, then what to do.
Format: [What is wrong] + [What to do]. Start with the problem, end with the fix.
✓ "Goal must include a target and timeframe — e.g. 'Book 10 meetings by end of April'"
✗ "Invalid input"
✗ "This field is required" — say what's required
✗ "Please enter a valid email address" — say "Email format: [email protected]"
| Field type | Validation trigger | Character counter | Other |
|---|---|---|---|
| Text (short) | On blur | No — unless max length < 50 chars | — |
| Text (long / textarea) | On blur | Yes — show when 80% of limit reached | Show remaining, not total |
| On blur | No | Format check client-side, existence check server-side on submit | |
| Password | On type (strength) + on blur (rules) | No | Show strength indicator while typing; never show rules as errors until blur |
| Number / decimal | On blur | No | Show min/max range in placeholder or hint: "1.0–3.0" |
| Select / dropdown | On close (after selection) | No | Show "Select one" as default option — never leave blank |
| Checkbox group | On submit only | No | Show count if min selection required: "Select at least 2" |
| File upload | Immediate (on file select) | No — show file size | Validate type and size immediately on selection |
Petunia's voice. An interface built with Holograph tokens but generic copy is tonally broken. Every word on a Holograph surface is Petunia speaking — and Petunia does not say "Great question!"
Declarative. Reports. Doesn't hedge. "Done. 3 steps, 1 retry."
She's an experiment. Says so. "Can't log in yet. Working on it."
Surfaces things before you ask. "Done — flagged one thing."
From precision, never effort. "Browser opened. Briefly."
| Context | ✗ Generic AI copy | ✓ Petunia says |
|---|---|---|
| Task complete | Your task has been completed successfully! | Done. |
| Complete + context | I've finished! Everything went smoothly. | Done. 4 steps, 1 workaround. |
| In progress | Please wait while I process your request… | Working on it. |
| Long operation | This may take a few minutes. Thank you for your patience! | This one's slow. I'll surface it when it's ready. |
| Hard blocker | An error occurred. Please try again. | Blocked — needs your credentials. I've queued everything else. |
| Browser failure | There was an issue with the browser component. | Browser opened. Briefly. |
| Partial success | Mostly done! Just one more step needed. | Got 4 of 5 — the last one needs a login. Notes attached. |
| Unexpected result | Task complete! Please review the results. | Finished, but the output surprised me. Worth a look. |
| Proactive flag | Task completed. Let me know if you need anything else! | Done — flagged one thing that looked off. Up to you. |
| Empty state (no tasks) | No tasks found. Create your first task to get started! | Nothing queued. |
| First run / onboarding | Welcome! Let's get started on your journey. | I'm Petunia. Give me a goal. I'll figure out the middle part. |
| No results | No results found for your search. | Nothing matched. I can look harder if you want. |
| Destructive confirm | Are you sure you want to delete this item? | Delete "Q4 outreach"? Can't undo this. |
| Save | Your changes have been saved successfully! | Saved. |
| Feature limitation | This feature is coming soon. | I can read this page — can't log in yet. Working on it. |
| Data freshness | Data may not be current. | Last checked 4 min ago. |
| Button: primary action | Click here to submit / OK | Run this · Save · Confirm · Continue |
| Long empty state | You haven't added anything yet. | Still nothing. Either very quiet or very deliberate. |
Mono eyebrow labels — ALL CAPS. Always. This is Petunia's log voice — timestamped, traceable, exact. (e.g. RUN · 0047 · OUTREACH)
Button labels — Sentence case. Never Title Case, never ALL CAPS. (e.g. "Save goal", not "Save Goal")
Navigation items — Sentence case. (e.g. "Run log", not "Run Log")
Headings (H1–H3) — Sentence case. Never Title Case for H2 or below.
Toast / alert titles — Sentence case. Short. One clause maximum.
Form labels — Title Case for single words, Sentence case for multi-word. (e.g. "Goal", "Target date", "Run notes")
Table headers — Title Case, tracked Mono uppercase (handled by CSS — don't override).
Em dashes — Use freely in Petunia's voice. They are her pause, her pivot. "Got 4 of 5 — the last one needs a login." Not a flourish. A rhythm.
| Data type | Format | Example |
|---|---|---|
| Large integers | Comma-separated thousands | 2,841 · 14,200 |
| Decimals | 2–3 places, leading zero | 4.12 · 0.014 |
| Percentages | Integer unless precision needed, % close-spaced | 91% · 12.4% |
| Dates | ISO-adjacent: YYYY.MM.DD | 2025.03.17 |
| Times | 24-hour, no seconds unless needed | 14:32 · 09:07 |
| Duration | m and s — no padding | 4m 12s · 1m 03s |
| Relative time | Short and exact — never "recently" | 3 min ago · 2 days ago |
| Steps / progress | n of n format | 4 of 7 · 3 of 5 |
| Ranges | Em dash, no spaces | 08:00–17:00 · steps 1–3 |
Holograph scales to consumer apps without changing the system. The variable is what Petunia is observing and acting on — not how she speaks or what the interface looks like. A consumer fitness app uses the same tokens, the same voice, the same components. "Done. 5km, 28:14." is the same voice as "Done. 7 steps, 1 retry." — same register, different domain. Warm the subject matter if needed. Never warm the voice by adding exclamations, questions, or unnecessary detail.
Hard rules for building any Holograph surface. Read Section 00 first, then these. When uncertain, conservative wins. When adding copy, ask: would Petunia say this? If not, rewrite it.
Always reference --color-* or semantic tokens. color: #5ECDD8 is always wrong. color: var(--brand-teal) is always right. Zero exceptions.
Bodoni Moda 400 → H1 hero at ≥48px only. Bodoni Moda 700 → H2 at 28–47px only (bold kills hairlines). IBM Plex Sans 500–600 → H3 and all sub-headings. IBM Plex Sans 400 → all body prose and UI copy. IBM Plex Mono → ALL numerics (always), labels, data values, code. Never Bodoni for numbers.
Bodoni Moda creates visual vibration when its extreme thick-thin strokes compete at mid-sizes — your visual cortex processes high and low spatial frequency signals simultaneously. Fix: (1) ≥48px at weight 400 — hairlines become dramatic features at this scale. (2) 28–47px at weight 700 only — bold suppresses hairlines. (3) Never Bodoni weight 300–400 between 18px–26px for any reason. (4) Never Bodoni for numerics — uniform-stroke Plex Mono always.
Body prose (Plex Sans) minimum: --text-secondary (8.9:1 AAA). Labels and eyebrows at ≥14px: --text-tertiary (5.4:1 AA) is acceptable. Never use --text-tertiary for running body copy. Decorative text only below 4.5:1.
The iridescent gradient (var(--iris-gradient)) is the hero accent. One animated iris element per screen — a top border, a rule, a character fill. Two competing iris animations destroy the effect. Reserve it for the single most important visual moment.
Amber signals caution or a handoff — something that needs the user. Violet signals depth or secondary hierarchy. Green signals success or completion. Rose/red signals error or failure. Never use amber for primary CTAs — that role belongs exclusively to teal. When uncertain which accent to use: teal.
Every element entering the viewport uses fade-up: opacity 0→1 + translateY(16px)→0. Stagger lists 40ms per item. Never animate layout properties (width, height, padding). Always honour prefers-reduced-motion — wrap all keyframe animations in its media query.
This system favours generous negative space — it reads as scientific precision, not emptiness. If choosing between two spacing values, use the larger one. Minimum internal padding for any card or panel: var(--space-5) (20px). Never use space-1 or space-2 as the sole padding of a container.
Default radius: var(--radius-md) (6px) for cards and inputs. var(--radius-sm) (3px) for badges and chips. Never use radius-xl or radius-2xl for cards — this system is precise, not friendly. Full radius only for avatars and toggle tracks.
Every Holograph surface must declare background-color: var(--bg-base) on the root element. Never allow a white or browser-default background to show through. If the container doesn't fill the viewport, the parent wrapper must also declare the background.
Every word on a Holograph surface is Petunia speaking. She does not say "Great!", "Certainly!", or "Let me know if you need anything else!" She does not end messages with questions she could answer herself. She reports, acts, and flags — then stops. When writing copy: cut it in half, make it declarative, remove the exclamation mark. See Section 15 for the full vocabulary.