Spaces:
Running
Running
| # Design language | |
| The reference for keeping this app visually coherent as it grows. Read it before | |
| touching `style.css`, `index.html`, or any DOM-building code in `main.js`. Every | |
| rule here is already live in the codebase β this file explains the *why* so changes | |
| extend the system instead of drifting from it. | |
| --- | |
| ## The thesis: color belongs to the voice | |
| This is a voice app. The one thing in the room that should have color is the thing | |
| that is talking. So: | |
| - **The orb** carries saturated color. It glows, and the glow's hue changes with | |
| conversational state. | |
| - **Everything else is monochrome** β a precise cool-grey dark canvas. Surfaces, | |
| borders, buttons, panels, the transcript: all greyscale. | |
| - The **only** exceptions are tiny *role echoes* (a one-word mono label, a small | |
| icon) that borrow the orb's state hue so the transcript reads in the same color | |
| language the orb speaks. They are accents the size of a word, never fills. | |
| - **Brand logos keep their own color.** The Hugging Face mark (`#ffd21e`) and the | |
| Cerebras mark (`#f15a29`) render in their brand colors in the identity credits and | |
| the about panel β a deliberate, owner-approved exception. It applies to those two | |
| logos only; do not generalize it to other chrome. | |
| If you find yourself adding a tinted background, a colored border, or a bright | |
| button anywhere outside the orb, stop β that color almost certainly belongs to the | |
| orb instead, or shouldn't exist. | |
| --- | |
| ## Color tokens | |
| All defined in `:root` in `style.css`. Use the variables, never raw hex in rules. | |
| ### Canvas (the monochrome world) | |
| | Token | Value | Use | | |
| |---|---|---| | |
| | `--bg` | `#0a0b10` | Page background (a cool near-black) | | |
| | `--bg-elev` | `#13151c` | Raised surfaces: bubbles, panels, icon buttons | | |
| | `--bg-elev-2` | `#1b1e29` | Surfaces on surfaces: history bodies, inputs | | |
| | `--border` | `rgba(255,255,255,.08)` | Default hairline | | |
| | `--border-strong` | `rgba(255,255,255,.16)` | Emphasised hairline | | |
| | `--text` | `#f5f6fa` | Primary text; also the *primary button* fill | | |
| | `--text-dim` | `rgba(245,246,250,.65)` | Secondary text | | |
| | `--text-faint` | `rgba(245,246,250,.42)` | Captions, labels, footer | | |
| ### Voice (the only saturated hues) | |
| These are the orb's state colors. They appear on the orb, and as small role echoes | |
| in the transcript β nowhere else. | |
| | Token | Value | Meaning | | |
| |---|---|---| | |
| | `--accent` / `--speaking` | `#8b7dff` violet | Assistant speaking | | |
| | `--accent-2` / `--listening` | `#22d3ee` cyan | You / listening | | |
| | `--processing` | `#f59e0b` amber | Thinking / tool call | | |
| | `--error` | `#ff6a75` | Error | | |
| | `--success` | `#34d399` | Ready / connected | | |
| ### Role echoes (semantic aliases β use these in chat code) | |
| | Token | Maps to | Where it shows | | |
| |---|---|---| | |
| | `--voice-user` | cyan | `YOU` label + user bubble accents | | |
| | `--voice-assistant` | violet | `ASSISTANT` label + assistant accents | | |
| | `--voice-tool` | amber | `TOOL CALL` label, wrench icon | | |
| **Why this mapping:** it mirrors the orb exactly β when *you* speak the orb is cyan | |
| (`state-listening`), when the *assistant* speaks it's violet (`state-ai-speaking`), | |
| when it's working it's amber (`state-processing`). The transcript is a quiet replay | |
| of the orb's color story. | |
| ### Orb state β glow (`.circle.state-*` β `--glow`) | |
| | State | Glow | | |
| |---|---| | |
| | `signed-out` | violet `#8b7dff` | | |
| | `authenticated` / `ready` | green `#34d399` | | |
| | `connecting` / `connected` / `starting` | yellow `#facc15` | | |
| | `listening` / `user-speaking` | cyan (`--listening`) | | |
| | `processing` | amber (`--processing`) | | |
| | `ai-speaking` | violet (`--speaking`) | | |
| | `error` | red (`--error`) | | |
| Adding a new state? Give it a `--glow`, and if it surfaces in the transcript, add a | |
| matching `--voice-*` alias rather than a one-off color. | |
| --- | |
| ## Typography | |
| Two faces, two jobs. Never reach for a third. | |
| - **Inter** β body and UI. Wordmark, buttons, inputs, panel titles, prose, history | |
| message bodies. The workhorse; it should feel neutral and get out of the way. | |
| - **Geist Mono** (`--font-mono`) β the **machine voice**. Reserved for text the | |
| *system* emits or identifiers it reports, never for human prose. | |
| ### When mono is correct | |
| Mono signals "this is the machine talking or naming itself." Use it for: | |
| - the orb's status caption (`.circle-caption`) | |
| - role eyebrows (`YOU` / `ASSISTANT` / `TOOL CALL`) | |
| - tool-call names and argument JSON | |
| - the `Β·WebSocket` transport tag, bitrate readouts, connection identifiers | |
| - the empty-state label | |
| Mono text is set uppercase with `letter-spacing: ~0.1β0.14em` and weight `500`, so it | |
| reads as a typed status line, not a headline. Body copy, button labels, and | |
| explanatory `small` text stay **Inter** β putting prose in mono breaks the metaphor. | |
| The font is loaded in `index.html`; the stack falls back to system mono gracefully if | |
| the CDN is blocked. | |
| --- | |
| ## Layout | |
| - **One continuous canvas.** No dividers under the topbar or above the footer, no | |
| panel chrome competing with content. The topbar and footer float over the stage. | |
| Keep it that way β a new section earns a hairline (`--border`) only if it genuinely | |
| needs separating. | |
| - **The orb is the hero and the center of gravity.** It sits dead-center on the | |
| stage. Controls flank it (mic / stop), captions sit beneath. Don't crowd it. | |
| - **Hairlines, not boxes.** Separation comes from `1px` borders at 8β16% white and | |
| from spacing, not from heavy fills or shadows. Shadows are soft and low | |
| (`0 4px 18px rgba(0,0,0,.32)`), used only to lift floating elements (bubbles, | |
| panels, modal). | |
| - **Radii:** `--radius-sm: 8px` (buttons, inputs, chips), `--radius-md: 14px` | |
| (bubbles, message bodies, modal), `--radius-lg: 22px` (reserved). Pick by element | |
| size; don't invent new values. | |
| - **Two reading surfaces for the transcript:** ephemeral bubbles top-right (desktop | |
| only) that log and fade, and a slide-in history panel for review. On phones the | |
| bubble stream is dropped and the panel goes full-screen β the panel is the single | |
| source of truth there. | |
| --- | |
| ## Components | |
| - **Buttons.** Default (`.btn`) is a neutral elevated surface. The *primary* button | |
| is **near-white on dark** (`--text` fill, `--bg` text) β the highest-contrast thing | |
| on the page that *isn't* the orb. There is no colored button; emphasis comes from | |
| contrast, not hue. | |
| - **Icon buttons** (`.icon-btn`) are `36px`, elevated surface, dim icon that brightens | |
| on hover. Side controls (`.side-btn`) are circular, collapse to zero size until the | |
| session is live (by width on desktop, by height in the mobile column). | |
| - **Chat bubbles & history messages** share one neutral surface. They are | |
| distinguished by **side** (you = left, assistant = right) plus the **mono role | |
| label** in the role-echo hue β not by tinted fills. Tool entries use the wrench icon | |
| + mono + amber, on the same neutral surface. | |
| - **Badge** (new-message dot) is monochrome white β a signal, not a color accent. | |
| - **Focus** is visible and neutral: inputs focus to `--text-dim`; the orb uses a | |
| `--glow`-colored outline (it's the orb, so color is allowed). | |
| --- | |
| ## Motion | |
| - **The orb is audio-reactive, not timer-driven.** Mic RMS (`--audio-level`) and the | |
| assistant output level (`--ai-audio-level`) drive scale/opacity at display rate, so | |
| every syllable moves it. This is the signature animation β keep new motion | |
| subordinate to it. | |
| - **Quiet by default.** Breathing/glow throbs are slow (1.4β2.4s) and low-contrast. | |
| Resist adding scattered micro-animations; an orchestrated moment beats many small | |
| ones, and excess motion reads as AI-generated. | |
| - **First paint is frozen.** `body.booting` disables all transitions until the first | |
| frame commits (stripped after one rAF in `main.js`). Anything new that would | |
| otherwise animate-in on load must respect this. | |
| - Honor `prefers-reduced-motion` for any motion you add. | |
| --- | |
| ## Writing / copy voice | |
| - Sentence case, plain verbs, no filler. Tuned and quiet β match the minimal canvas. | |
| - Name things by what the user controls, not by the system's internals. A button says | |
| exactly what it does, and keeps the same word through the flow. | |
| - **Empty states invite action** ("Tap the orb and start talking"), they don't just | |
| set a mood. | |
| - **Errors state what happened and how to recover**, in the interface's voice β they | |
| don't apologize and are never vague. | |
| - Mono labels are terse identifiers (`YOU`, `TOOL CALL`); prose stays in Inter. | |
| --- | |
| ## Responsive floor (non-negotiable) | |
| Every change ships meeting these: | |
| - Works down to a `360px`-wide phone. The `@media (max-width: 600px)` block already | |
| handles the phone layout β extend it, don't fight it. | |
| - Visible keyboard focus on every interactive element. | |
| - `prefers-reduced-motion` respected. | |
| - Tap targets β₯ `44px`; `touch-action: manipulation` on anything tappable. | |
| --- | |
| ## Before you ship β the mirror check | |
| 1. Is every saturated color either on the orb or a word-sized role echo? If a fill or | |
| border is colored, remove that accessory. | |
| 2. Is mono used only for machine/system text, and Inter for everything human? | |
| 3. Does any new state have both a `--glow` and (if it appears in chat) a `--voice-*` | |
| alias? | |
| 4. Are separations hairlines + spacing, not boxes and heavy shadows? | |
| 5. Did you add motion? Is it quieter than the orb and reduced-motion-safe? | |
| 6. Remove one accessory. The minimal look survives on precision, not addition. | |