---
name: ethos-design
description: Use when building or editing any user interface for an Ethos app (muzick, kdrive, nginx panel, xray manager, kaneo, or any new kvmx.ru app), when authoring or extending components in the @ethos/ui package, or when any frontend/React/HTML/SVG work must carry the Ethos visual identity. Triggers on requests to design a screen, build a component, add an app to the system, restyle existing UI, create icons, or set up per-app theming. Covers the token system, the five laws, the mono/sans split, warm-room depth, per-app accent + motif, the shared shell (desktop + mobile), copy voice, the trap list to refuse, and the render-based verification protocol.
---

# Ethos

Ethos is the shared design language for the kvmx.ru self-hosted apps. One system, one character, many apps. Switching from kdrive to the xray manager should feel like switching tabs in one instrument, not opening a different product.

## The thesis: instruments, not appliances, that don't lie

These apps are tools the operator runs their own infrastructure with — closer to an oscilloscope, a mixing desk, or good anodized audio gear than to a consumer product. Dense where it needs to be, every element earning its place, honest about the machinery instead of papering over it.

But not cold. The move nobody else makes: **warmth AND honesty in one room.** Consumer apps give warmth with no honesty (glossy, hides state behind a spinner). Dev dashboards give honesty with no warmth (cold, dense). Ethos is a calm, warm, well-lit space that still shows real throughput, real queue depth, real bytes. It can do both because the operator is the only user — the apps don't need to sell or hide anything.

Design failure looks like either extreme: a techy grey dashboard, or a glossy blurred-glass consumer screen. Both are the trap.

## The five laws (non-negotiable, shared across every app)

1. **Mono/sans split is law.** Geist Mono for anything the machine owns — ips, ports, hashes, sizes, timestamps, ids, paths, throughput, durations, bitrates, counts, percentages. Geist Sans for anything a human wrote — labels, prose, headings, track/file names. This single rule is the strongest signature; it must read identically across every app. A label is human (sans); its value is machine (mono). `Bitrate` in sans, `1411 kbps` in mono.

2. **Depth from light, not blur.** Elevation comes from soft warm shadows, layered surface steps in the neutral ramp, and 1px hairline borders. Never `backdrop-filter`/frosted glass, never glossy gradients. Reads engineered, not soft, and costs nothing per repaint.

3. **Shared shell.** Identical app frame everywhere — same rail/topbar geometry, same status grammar, same ⌘K command surface. See the shell anatomy below. Per-app difference is only accent + motif, never structure.

4. **Mechanical motion.** 120–180ms, `cubic-bezier(0.2, 0, 0, 1)`, no bounce, no spring. Motion confirms a state change and gets out of the way. Respect `prefers-reduced-motion`.

5. **Show the machinery.** Real numbers over spinners. Queue depth, bytes/sec, buffer %, actual progress, indexed counts. A vague "loading…" is the appliance move — never do it. Empty and error states state what's true and what to do, in the interface's voice.

### The balance principle (how the accent behaves)

**Content is the color. Accent is the signal. Shell is quiet.** The apps hold warm content — album art, files, manga panels — and that content carries the color. The shell stays quiet and warm so the content glows. The accent is NOT fill-everything paint; it marks the one thing that matters: active nav, focus rings, primary action, the played portion of a scrubber. If a whole panel is washed in the accent, it's wrong — pull it back.

## Tokens

Single source of truth is `ethos.tokens.css` — the neutral system, the light-theme block, and the per-app override slots, shipped as CSS custom properties. (Generate typed TS from it later if an app wants typed access; the CSS stays authoritative so nothing drifts.) Values below are the dark-theme defaults.

### Neutral ramp — warm, brown-tinted (NOT cool grey, NOT cream)
```
--bg-0: #14110D   /* deepest room */
--bg-1: #1B1712   /* surface */
--bg-2: #221D17   /* raised */
--bg-3: #2C261D   /* hover / raised */
--bg-4: #372F24   /* pressed / high */
--line:    rgba(244,234,220,0.09)   /* hairline */
--line-hi: rgba(244,234,220,0.16)   /* hairline emphasized */
--text-hi:      #F4EEE4   /* human primary */
--text-mid:     #B4AA98   /* human secondary */
--text-lo:      #756C5C   /* human tertiary / idle */
--text-machine: #9C917D   /* mono default */
```

### Type
```
--sans: 'Geist', -apple-system, system-ui, sans-serif
--mono: 'Geist Mono', ui-monospace, 'SF Mono', Menlo, monospace
```
Scale: display 32–44 / heading 20 / body 14–16 / label 10–11 uppercase 0.08–0.12em tracking / machine data 12–15 mono. Mono always `font-feature-settings: "tnum" 1, "zero" 1` and slight negative tracking. Headings go editorial and large; let type be a memorable part of the design, not a neutral delivery vehicle.

### Motion / radii / shadow
```
--ease: cubic-bezier(0.2, 0, 0, 1)
--fast: 130ms   --med: 170ms
--r-sm: 8px  --r-md: 12px  --r-lg: 18px  --r-xl: 24px
--shadow-soft: 0 2px 8px rgba(0,0,0,.35), 0 12px 32px rgba(0,0,0,.28)
```
Fonts are self-hosted (subset + woff2, immutable caching, served off own nginx). No Google/Vercel font CDN — it phones home and fails the threat model.

## Per-app fingerprint (ask, don't assume)

Each app gets exactly two things of its own — one accent hue and one motif — set in a single `[data-app]` block in `ethos.tokens.css`. Everything else is inherited.

**Do not pick these silently.** When a new app joins the system, run a short intake with the operator before writing any override:

- Ask for the **accent**: a name and a hex. Bring **2–3 of your own suggestions** grounded in what the app *does* — reason from its function and content, not from a palette wheel — and say why each fits. The operator decides; your suggestions are there to react to.
- Ask for the **motif**: the recurring geometric signature tied to the app's function. Again, offer a couple of options with a one-line rationale each. A motif shows up in the app mark, empty states, loading, the favicon, and one hero moment.
- Once chosen, fill the `[data-app]` TEMPLATE in `ethos.tokens.css` (accent + the derived `-hi/-dim/-line/-glow`) and seed the motif (a symbol in the icon set, plus any gradient hooks).

The one worked example that already exists is **muzick — honey amber `#EDA24E` · waveform**; use it as the reference for how an override block and a motif are shaped, not as a set to copy from. Same skeleton, different soul.

## Shared shell anatomy

**Desktop (≥ 640px):** vertical `Rail` (64px, icon nav, active = accent icon + `accent-dim` bg + 3px accent edge) · `TopBar` (56px, wordmark + ⌘K search + right-aligned machine readouts) · scrollable `Main` · optional docked bar (e.g. muzick's player). Command palette (⌘K) is the shared command surface.

**Mobile (≤ 640px) — shell reflow, same law for every app:**
- Rail → full-width bottom tab bar (thumb reach). Active = accent icon + short top edge mark. Keep to **≤ 5 primary tabs**; fold secondary nav into a parent and push settings into the header. More than 5 is past the thumb ceiling.
- TopBar sheds what it can't afford — the wide machine readouts drop from the header and relocate to where there's room (detail views, per-row values). Honesty is relocated, never deleted.
- Large docked bars collapse to a compact mini (thumb + title + primary action), with any waveform/scrubber condensed to a thin accent progress line. The motif is expressed at whatever scale the form allows.
- Heroes restack to single column: content-art centered and fluid (`aspect-ratio: 1`), heading down a step, spec rows wrap.

## Icons

The set is `ethos-icons.svg` — a `<symbol>` sprite. Reference a glyph with `<use href="/ethos-icons.svg#i-NAME"/>`; color and size come from the consumer. One 24px grid, one stroke width (~1.7), one corner radius, `stroke-linecap/linejoin: round`, `fill: none` line style (filled only for transport glyphs — play, pause, prev, next, more). **Extend by adding a `<symbol>`** on the same grid and hand; never diverge, and never import lucide or a generic pack — that breaks the single-hand rule. Two motif seeds ship in the set (`i-wave`, `i-grid`) to start apps from.

## Copy voice

Words are design material. Name things by what the person controls, not how the system is built — but Ethos still shows machine values, so: the **label** is human (sans, plain), the **value** is honest (mono). Active voice, sentence case, one name per action through the whole flow (a button that says Publish yields a toast that says Published). Errors don't apologize and are never vague — they say what happened and how to fix it. Empty states are invitations to act. Register is terse and plain, no filler.

## Traps to refuse

These are the defaults that make a design generic. Do not ship them, even if asked casually:

- **Frosted glass / `backdrop-filter` over a blurred hero photo** — the 2023 AI-premium tell, and a repaint cost. Depth comes from light instead (law 2).
- **Cream (#F4F1EA) + high-contrast serif + terracotta accent** — the AI-cream default; terracotta near #D97757 also reads as an Anthropic tell.
- **Near-black + one acid-green/vermilion accent** — the other AI default.
- **Accent as wash** — accent filling a whole panel. It's a signal, not paint (balance principle).
- **Spinners / vague "loading…"** — hiding real state. Show the number (law 5).
- **Shadows/glass for elevation instead of hairlines + surface steps.**
- **Absolute-positioned `inset: 0` fill divs for backgrounds.** They stay contained only by a positioned overflow-hidden parent, and escape to the whole viewport in stricter renderers. Put the gradient/background directly on the sized element (the 260px art box, the 52px thumb), not on an inner fill layer. This is a real bug that has shipped.
- **Google/Vercel font CDN** — self-host (law-adjacent, threat model).
- **`localStorage`/`sessionStorage` in sandboxed artifact demos** — fails silently; use in-memory state for demos, sqlite for real apps.

## Build process

Plan → critique → build → **screenshot** → critique → fix. Match complexity to the vision; spend boldness in one place (the motif) and keep everything around it quiet. Before calling something done, remove one accessory.

## Verification protocol (do not skip)

**"Can't see it" means unverified. Never sign off on computed values as a substitute for a render.** Reading back a color or a token value confirms the value parsed; it does NOT confirm the layout, containment, stacking, or overflow. A screen can be completely broken while every computed color is correct.

Before declaring any screen done:
1. Render it and **actually look at the pixels.** Screenshot, view, critique.
2. **Geometry check:** no horizontal overflow (`scrollWidth === clientWidth`); key elements sized and contained (an art box is its own dimensions, not the viewport); nothing painting the full screen that shouldn't.
3. Check **both** desktop and mobile (cross the 640px line) before "done."
4. If you genuinely cannot screenshot, **say so plainly and hand visual sign-off to the human.** Do not fill the gap with confidence. State what you verified (geometry) and what you couldn't (appearance).
