Captures every decision made during the studio.html sandbox iteration so the same visual language can be applied to the rest of the app (and to new components later) without re-deriving it. Sections: - Vision + reference instruments - Lighting model (single rule: light from above) - Color tokens with semantic naming - The 3-layer "metal" language as shared CSS custom properties - Surface hierarchy (3 depth layers with progressively brighter bases) - Component anatomies: rack, bars, screen, sub-panels, knobs, faders, step LEDs, buttons, screws, engraved labels, numeric displays - Typography, spacing scale, interaction conventions - "What is intentionally NOT here" — rejected ideas to keep the language disciplined - A "how to add a new component" checklist Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
381 lines
16 KiB
Markdown
381 lines
16 KiB
Markdown
# code-sinth — design system
|
||
|
||
The studio hardware emulation language. Anything we add to the right-pane
|
||
control surface should follow this. The goal is consistency: a new component
|
||
should look like it belongs on the same physical rack as everything else.
|
||
|
||
The reference implementation lives in [web/sandbox/studio.html](../web/sandbox/studio.html).
|
||
|
||
## Vision
|
||
|
||
> Vintage studio rack module. Dark anodised aluminium plates mounted on a
|
||
> darker frame, lit from above by a softbox. Metal parts catch the light.
|
||
> Engraved labels on the metal. Amber LEDs and amber readouts where
|
||
> something is "active".
|
||
|
||
References that pull this together: Moog Voyager, Roland TR-808, Korg MS-20,
|
||
Universal Audio 1176. Skeuomorphic but **disciplined** — every effect
|
||
exists to model a physical material under a specific lighting setup, not
|
||
for decoration.
|
||
|
||
## Lighting model
|
||
|
||
**One single rule, never broken**: light comes from **above**, slightly
|
||
in front. Every reflective surface lights its top and shadows its bottom.
|
||
This applies regardless of where the surface sits within the rack.
|
||
|
||
Practical consequences:
|
||
- Vertical gradients on metal (lighter top, darker bottom).
|
||
- Specular bands on the upper portion of any large flat surface.
|
||
- Bevels: bright on top edge, dark on bottom edge.
|
||
- Top-edge gloss crescent on circular pieces (knob disc, indicator dot).
|
||
- Subtle warm rim light at the very bottom — bounce light from the surface
|
||
the rack sits on (typical of softbox-lit photos of studio gear).
|
||
|
||
When in doubt, ask: "if this were aluminium under a softbox lamp, where
|
||
would the highlight go?"
|
||
|
||
## Color tokens
|
||
|
||
All colors live as CSS custom properties on `:root`. Semantic naming, not
|
||
literal — adding a new theme later means changing the values, not the
|
||
selectors.
|
||
|
||
```css
|
||
/* surfaces (warm-neutral dark, faint warm tint to feel like metal not plastic) */
|
||
--bg: #131210; /* page bg, behind the rack */
|
||
--hw-bg: #1a1916; /* outer rack base */
|
||
--hw-bg-hi: #232220;
|
||
--hw-bg-lo: #0e0d0b;
|
||
--hw-edge: #050505; /* sharp dark edges between surfaces */
|
||
--hw-screen: #161412; /* code-area glass — slightly darker than the rack */
|
||
|
||
/* text */
|
||
--hw-fg: #a8a39a; /* general body text */
|
||
--hw-fg-hi: #d8d0c0; /* numeric values, labels at attention */
|
||
--hw-fg-dim: #6c6660; /* secondary status text */
|
||
--hw-engrave: #8a847a; /* engraved labels on metal */
|
||
|
||
/* accent — amber. ONLY for active indicators: arcs, LEDs, running label */
|
||
--hw-amber: #e8a050;
|
||
--hw-amber-hi: #f4c890;
|
||
--hw-amber-mut: rgba(232, 160, 80, 0.18);
|
||
--hw-amber-glow: rgba(232, 160, 80, 0.45);
|
||
--hw-amber-off: #28201a;
|
||
|
||
/* status LEDs */
|
||
--hw-led-red: #c84838; /* stop / alert */
|
||
--hw-led-green: #6aca8a; /* play / running */
|
||
|
||
/* code syntax — warm desaturated palette, never bright primary colors */
|
||
--hw-syn-com: #5a554f;
|
||
--hw-syn-kw: #d68868;
|
||
--hw-syn-num: #d6a268;
|
||
--hw-syn-fn: #c8a878;
|
||
--hw-syn-id: #b8b0a0;
|
||
--hw-syn-op: #6c6660;
|
||
```
|
||
|
||
**The amber accent is precious**. Used for: knob value arcs, fader scale
|
||
indication, step LEDs when on, the running label, numeric readouts on
|
||
amber LCDs (we don't currently have those). Never for plain text, plain
|
||
borders, or decorative accents — overuse kills its meaning.
|
||
|
||
## The "metal" language (3 reusable layers)
|
||
|
||
Any frame that should read as a brushed-aluminium plate composes three
|
||
shared CSS variables:
|
||
|
||
```css
|
||
/* (1) horizontal specular band on the upper portion */
|
||
--metal-specular: linear-gradient(180deg,
|
||
rgba(255, 248, 230, 0.08) 0%,
|
||
rgba(255, 248, 230, 0.03) 28%,
|
||
rgba(255, 248, 230, 0) 45%,
|
||
rgba(0, 0, 0, 0) 70%,
|
||
rgba(0, 0, 0, 0.10) 100%);
|
||
|
||
/* (2) very fine repeating horizontal stripes (brushed steel feel) */
|
||
--metal-brush: repeating-linear-gradient(0deg,
|
||
rgba(255, 250, 235, 0.018) 0px,
|
||
rgba(255, 250, 235, 0.018) 1px,
|
||
rgba(0, 0, 0, 0.030) 1px,
|
||
rgba(0, 0, 0, 0.030) 2px);
|
||
|
||
/* (3) 4-edge bevel via inset shadows */
|
||
--metal-bevel:
|
||
inset 0 1px 0 rgba(255, 248, 230, 0.18), /* top edge highlight */
|
||
inset 0 -1px 0 rgba(0, 0, 0, 0.70), /* bottom edge shadow */
|
||
inset 1px 0 0 rgba(255, 248, 230, 0.06), /* left edge faint highlight */
|
||
inset -1px 0 0 rgba(0, 0, 0, 0.45); /* right edge dark */
|
||
```
|
||
|
||
A frame plugs them in and adds its own base color and drop shadow:
|
||
|
||
```css
|
||
.some-panel {
|
||
background:
|
||
var(--metal-specular),
|
||
var(--metal-brush),
|
||
linear-gradient(180deg, /* base color top → bottom */ );
|
||
box-shadow:
|
||
var(--metal-bevel),
|
||
/* element-specific drop shadow */;
|
||
}
|
||
```
|
||
|
||
This is the single most important rule: **every metal surface uses these
|
||
three layers**. Don't reinvent them per component.
|
||
|
||
## Surface hierarchy
|
||
|
||
Three depth layers, each visually brighter than the one beneath so they
|
||
read as plates physically mounted on top of each other:
|
||
|
||
| Layer | Element | Base gradient | Drop shadow |
|
||
|------------------|-------------------------------|-------------------------------------------|----------------------------------------------|
|
||
| L0 (deepest) | `.hardware` (outer rack) | `#181513` → `#080604` | `0 8px 32px rgba(0,0,0,0.65)` |
|
||
| L1 (mid) | `.top-bar`, `.bottom-bar` | `#25211d` → `#16130f` | `0 2px 4px rgba(0,0,0,0.4)` |
|
||
| L2 (brightest) | `.knobs`, `.faders`, `.seq` | `#2c2823` → `#1a1612` → `#110d09` | `0 2px 4px + 0 6px 14px` (two-layer) |
|
||
|
||
The progressively brighter base + progressively bolder drop shadow makes
|
||
the L2 plates feel "lifted" off the rack, while the bars feel mounted
|
||
flush. The code "screen" is **outside** this hierarchy — it's a recess,
|
||
not a surface (see below).
|
||
|
||
## Components
|
||
|
||
### Outer rack frame (`.hardware`)
|
||
|
||
- L0 of the surface hierarchy. Largest border-radius (10px) — the overall
|
||
unit feels like a single piece.
|
||
- Has 4 corner screws, **only on the outermost frame** — sub-panels do not
|
||
carry screws.
|
||
- Padding 14px. Grid: editor column on the left, fixed-width control
|
||
column (220px) on the right.
|
||
|
||
### Top / bottom bars (`.top-bar`, `.bottom-bar`)
|
||
|
||
- L1. Twin styling — they read as a matched pair anchoring the screen.
|
||
- Height 30px. Border-radius 4px (lower than rack).
|
||
- Top bar: transport button group + status text. Bottom bar: running
|
||
label with pulsing amber dot. Same height/style/border, different
|
||
contents — visual symmetry is the point.
|
||
|
||
### Code "screen" (`.screen`)
|
||
|
||
- Not a panel — a **recess**. No raised surface, no bevel highlights.
|
||
- Achieved with deep inset shadows on a slightly darker background:
|
||
```css
|
||
background: var(--hw-screen);
|
||
box-shadow:
|
||
inset 0 2px 4px rgba(0,0,0,0.7),
|
||
inset 0 0 0 1px var(--hw-edge),
|
||
inset 0 6px 14px rgba(0,0,0,0.5);
|
||
```
|
||
- Reads as a hole cut into the rack with a glass screen behind it.
|
||
- Sits between top-bar and bottom-bar to complete the "header / screen /
|
||
footer" feel of a single rack module.
|
||
|
||
### Sub-panels (`.knobs`, `.faders`, `.seq`)
|
||
|
||
- L2 of the hierarchy. Border-radius 6px.
|
||
- Use `.knobs, .seq, .faders` shared selector for the metal language;
|
||
each adds its own internal layout (grid for knobs, flex row for faders,
|
||
flex column for seq).
|
||
|
||
### Knobs
|
||
|
||
The most elaborate component. Eight layers in canvas, in this order:
|
||
|
||
1. **Drop shadow** — black disc fill at slight Y-offset with `shadowBlur`.
|
||
Sits the knob "on" the panel.
|
||
2. **Amber value arc** — outside the knob's outer edge. Faint full-track
|
||
stroke + bright glowing active arc (length proportional to value).
|
||
3. **Dark rim** — thin (~2.5px) outer ring of the knob body. Vertical
|
||
gradient `#2a2826 → #0a0908 → #040404`.
|
||
4. **Big metallic disc** — fills most of the knob's area. This is the
|
||
centerpiece. Six sub-layers stacked:
|
||
- **Base** radial gradient (matte foundation, slightly darker at the edge).
|
||
- **Concentric brushing** — concentric circles every 0.55px with
|
||
pseudo-random alpha alternating bright/dark. Imitates lathe-turned
|
||
aluminium. Deterministic seed so it doesn't shimmer between frames.
|
||
- **Vertical lighting** overlay (white wash 32% on top, black wash 45%
|
||
on bottom).
|
||
- **Horizontal specular band** on the upper third (white-warm 42% peak
|
||
with soft falloff).
|
||
- **Bottom rim light** — warm cream wash 14% at the very bottom.
|
||
- **Bevel** — bright crescent on the top edge (55% white-warm) +
|
||
dark crescent on the bottom edge (50% black).
|
||
5. **Inset shadow** between disc and rim, gives the disc a "sunk-in" feel.
|
||
6. **Indicator notch** — small dark triangle on the disc, base near the
|
||
center, tip toward the rim, rotated to the value angle. Engraved
|
||
appearance: dark fill + thin bright lower-edge highlight catching
|
||
light on the inside of the groove.
|
||
|
||
Fixed pixel sizes: `totalR = 26`, `rimW = 2.5`, `discR = 23.5`, notch
|
||
half-base = 1.5. Canvas is `72×72` — the extra ~10px margin lets the
|
||
amber arc and drop shadow render without clipping.
|
||
|
||
Indicator angle range: `Math.PI * 0.78` (lower-left) to `Math.PI * 2.22`
|
||
(lower-right) — a 7:30 → 4:30 sweep, matching how real synth knobs end
|
||
their throw.
|
||
|
||
### Faders
|
||
|
||
Vertical sliders, sit in their own sub-panel arranged in a row.
|
||
|
||
- **Track** (`.fd-track`) — 5px wide × 78px tall. Deep slot cut into the
|
||
panel. Heavy `inset 0 1px 3px rgba(0,0,0,0.85)` shadow + thin warm
|
||
highlight at the bottom edge for chamfer feel.
|
||
- **Tick marks** (`.fd-ticks`) — 16px wide pseudo-element on each side
|
||
of the track. `repeating-linear-gradient` 1px on / 6px off. Engraved
|
||
scale alongside the slot.
|
||
- **Cap** (`.fd-cap`) — 26×14px rounded-corners metallic block. Same
|
||
metal language as the knob disc, condensed for a small element:
|
||
- Brushed stripes via `repeating-linear-gradient` (4% / 5% alpha).
|
||
- Vertical light gradient `#b6b0a4 → #807a6e → #3a352e → #1a1612`.
|
||
- Horizontal grip line — dark band at 46-54% Y for the "thumb groove".
|
||
- 4-edge inset bevel (top bright, bottom dark, left subtle highlight,
|
||
right dark).
|
||
- Drop shadow `0 2px 3px rgba(0,0,0,0.6)`.
|
||
|
||
Interaction: click anywhere on the track jumps the cap there + starts a
|
||
drag; drag follows pointer; pointerCapture is on the track itself so the
|
||
cap follows even past the bounds.
|
||
|
||
### Step LEDs (`.led`)
|
||
|
||
- Off: `linear-gradient(180deg, #100c08 → #28201a)` — looks unlit but
|
||
not flat black (so the off LEDs have a subtle "physical" feel).
|
||
- On: `radial-gradient(circle at 50% 25%, #f4c890 → #e8a050 → #a06820)`
|
||
+ `0 0 5px var(--hw-amber-glow)` outer glow.
|
||
- Hover: brighter dim variant. Click toggles.
|
||
- 16-cell row. Beat dividers every 4 cells via a pseudo-element on the
|
||
left edge of cells where `i % 4 === 0`.
|
||
- Playhead: `outline: 1px solid rgba(232, 160, 80, 0.6)` + 1px offset.
|
||
|
||
### Buttons (`.hw-btn`)
|
||
|
||
- Vertical gradient `#2c261f → #14110d`.
|
||
- Border `1px solid var(--hw-edge)`.
|
||
- Inset highlight on top edge + dark on bottom edge.
|
||
- Letter-spacing 0.12em, uppercase, font-size 10px.
|
||
- Hover: text changes to amber. Active: `transform: translateY(1px)` +
|
||
inset darkening (the "press" feel).
|
||
- **Button groups** in the top bar (STOP+RUN): adjacent, no gap, share
|
||
borders. First child has left-rounded corners only, last child has
|
||
right-rounded only. The middle border merges. Gives the feel of a
|
||
single rocker switch.
|
||
|
||
### Screws (`.screw`)
|
||
|
||
- 8px diameter. **Only on the outermost rack** — sub-panels don't carry
|
||
screws (real rack panels are usually pop-mounted, not bolted).
|
||
- `radial-gradient(circle at 30% 25%, #8a7d6a → #3a3025 → #100c08)` for
|
||
the metal head.
|
||
- `::after` adds a slot mark with a 45° linear-gradient hard-edge.
|
||
- Inset shadow + small drop shadow for "sunk into the metal".
|
||
|
||
### Engraved labels
|
||
|
||
The `.label` class on knob/fader cells, the engraved text inside bars.
|
||
|
||
- `font-size: 9-10px`, `letter-spacing: 0.16-0.18em`, `text-transform:
|
||
uppercase`.
|
||
- Color `var(--hw-engrave)` — the desaturated warm gray.
|
||
- Double text-shadow for the engraved feel:
|
||
```css
|
||
text-shadow:
|
||
0 1px 0 rgba(0, 0, 0, 0.7), /* deep shadow below */
|
||
0 -1px 0 rgba(255, 220, 180, 0.04); /* faint highlight above */
|
||
```
|
||
- The shadow below + highlight above creates the optical illusion of
|
||
text incised into metal lit from above.
|
||
|
||
### Numeric value displays
|
||
|
||
- Below knobs and faders. Cream color (`var(--hw-fg-hi)`), tabular nums.
|
||
- **No frame, no inset background** — flat text on the panel. We
|
||
experimented with LCD-style amber inset readouts and it felt too busy.
|
||
The amber is reserved for active indicators; values are passive.
|
||
- Width matches the knob (~46px min) so the layout doesn't shift when
|
||
the digit count changes.
|
||
|
||
## Typography
|
||
|
||
Single font stack: `'JetBrains Mono', 'Cascadia Code', Consolas, monospace`.
|
||
Everything monospace — keeps the "instrument firmware" feel.
|
||
|
||
Sizes:
|
||
- Body / labels: 10-11px
|
||
- Code: 12-13px
|
||
- Headings (when needed): 12-14px
|
||
|
||
Letter-spacing: `0.06em` for normal text, `0.16-0.18em` for engraved
|
||
labels and button text. Wider tracking gives the "carved into metal"
|
||
feel.
|
||
|
||
Tabular numerals (`font-variant-numeric: tabular-nums`) for any value
|
||
display so they don't wobble when digits change.
|
||
|
||
## Interaction
|
||
|
||
- **Drag**: knobs and faders use `pointerdown` + `setPointerCapture`.
|
||
Always change `cursor` to `grabbing` on body during the drag (so the
|
||
cursor doesn't flicker if the pointer leaves the element bounds).
|
||
- **Click track to jump**: faders accept a click anywhere on the track
|
||
to jump the cap there, then a drag continues from that point.
|
||
- **Double-click to recenter**: knobs reset to their default value.
|
||
- **Shift-drag for fine adjust**: knobs accept `ev.shiftKey` for ¼-speed
|
||
adjustment.
|
||
- **`pointercancel`** is always handled alongside `pointerup` — covers
|
||
edge cases where the browser cancels a drag mid-gesture.
|
||
- Always remove all listeners and reset cursors on the up/cancel handler.
|
||
|
||
## Spacing & proportions
|
||
|
||
- Outer rack padding: 14px.
|
||
- Outer rack gap (editor ↔ control column): 12px.
|
||
- Right-stack inner gap (knobs / faders / seq): 8px.
|
||
- Sub-panel padding: 8-14px depending on density.
|
||
- Element gap inside sub-panels: 4-12px.
|
||
|
||
The unifying scale is multiples of 2 (2/4/8/12/14). Avoid 5px, 7px,
|
||
13px — they look like accidents.
|
||
|
||
## What is intentionally NOT here
|
||
|
||
To keep the language disciplined, these were considered and rejected:
|
||
|
||
- **Skeuomorphic photo textures** (real wood, real brushed metal photos).
|
||
Pure CSS gradients keep the file small and stay sharp at any DPI.
|
||
- **3D rotation / perspective transforms**. The lighting model already
|
||
conveys depth; transforms add visual noise.
|
||
- **Animated reflections / moving highlights**. Static metal looks
|
||
premium; moving highlights look like a screensaver.
|
||
- **Multiple accent colors**. Amber is the only "active" hue. A second
|
||
accent would dilute the meaning.
|
||
- **Decorative dots / flourishes / brand marks**. The rack is
|
||
minimalist — every visible element is functional.
|
||
- **Bright primaries in syntax highlighting**. The code area is
|
||
"behind glass" with warm light passing through; saturated greens
|
||
and blues break the illusion.
|
||
|
||
## How to add a new component
|
||
|
||
1. Decide its surface hierarchy layer (L0 / L1 / L2). Almost everything
|
||
new is L2 (a sub-panel).
|
||
2. Apply the metal language: `var(--metal-specular)`, `var(--metal-brush)`,
|
||
`var(--metal-bevel)` plus a base color and drop shadow appropriate
|
||
to the layer.
|
||
3. Use `--hw-engrave` for any label text, with the double-text-shadow
|
||
formula.
|
||
4. Use `--hw-amber` only if the new component has an "active" indicator
|
||
to show.
|
||
5. Drag interactions: pointerdown + setPointerCapture, body cursor =
|
||
`grabbing`, listen for pointermove/up/cancel, clean up on up/cancel.
|
||
6. Numbers: tabular-nums, `--hw-fg-hi` color, no inset frame.
|
||
7. If it's reflective (metal disc, cap, sphere), light it from above.
|
||
No exceptions.
|