STYLE_GUIDE

Transcriber UI Style Guide

This guide is for generating new UI elements that match the existing design system.


Design Language

Minimal, developer-tool aesthetic with dark (default), light, and auto themes. Monospaced type throughout. Accent-yellow for primary actions, accent-blue for secondary/server-related actions. Everything is dense and compact — small font sizes, tight spacing.

Theme is controlled by the data-theme attribute on the root element ("dark" | "light" | "auto"). The theme.js utility in static/js/utilities/ handles reading and writing this value. All colours are defined as CSS custom properties in variables.css — never use raw colour literals.


CSS Custom Properties (always use these, never raw colors)

/* Base palette (dark theme — default) */
--bg: #0d0d0f          /* page background */
--surface: #141417     /* card / panel background */
--surface2: #1c1c21    /* elevated surface: headers, hover states, inputs */
--border: #2a2a32      /* all borders */
--accent: #e8ff47      /* yellow — primary CTA, active state, playhead */
--accent2: #47c8ff     /* blue — secondary CTA, server/network actions, focus */
--text: #e8e8ee        /* body text */
--muted: #6b6b7a       /* labels, metadata, placeholder text */
--waveform: #3a3a48    /* waveform bars */
--waveform-progress: #e8ff47  /* played waveform */
--waveform-hover: rgba(255, 255, 255, 0.85)  /* waveform seek cursor */

/* Semantic alpha variants — use these instead of raw rgba() */
--accent-faint: rgba(232, 255, 71, 0.06)   /* resting tint bg */
--accent-subtle: rgba(232, 255, 71, 0.08)  /* hover / active tint bg */
--accent-border: rgba(232, 255, 71, 0.35)  /* resting accent border */
--accent2-faint: rgba(71, 200, 255, 0.10)  /* resting/hover accent2 tint bg */
--accent2-border: rgba(71, 200, 255, 0.25) /* resting accent2 border */

/* Status colors */
--danger: #ff7777   /* error, destructive */
--success: #47ff8a  /* connected, success */

/* Typography */
--font-mono: 'IBM Plex Mono', monospace

The light theme (applied via [data-theme="light"] or @media (prefers-color-scheme: light) when [data-theme="auto"]) redefines all of these. The semantic roles remain the same but values shift (e.g. --accent becomes #7a7800, --accent2 becomes #0077cc). Always reference variables — the light overrides are transparent when you do.

Raw values still used directly (no variable):

  • Warning/recording: #ff4444
  • Strong accent hover bg: rgba(232, 255, 71, 0.12–0.18) (one-off, not worth a variable)
  • Strong accent hover border: rgba(232, 255, 71, 0.6–0.7)
  • White overlay hover: rgba(255, 255, 255, 0.05–0.08)

Typography

Only two fonts are used:

font-family: var(--font-mono);            /* used for almost everything */
font-family: 'IBM Plex Sans', sans-serif; /* body/app shell only */

Use var(--font-mono) everywhere except the body element's base rule.

Type scale (all in rem):

RoleSizeWeightLetter-spacingTransform
Page title / project title1.1rem6000.08–0.15emuppercase
Panel heading (h2)0.7rem6000.12emuppercase
Section label / sidebar title0.55–0.65rem6000.10–0.12emuppercase
Body / transcript text0.875rem
Track name0.78rem5000.04em
Timecode0.85rem5000.10em
Buttons / labels / meta0.6–0.65rem0.06–0.08em
Small badges / kbd keys0.48–0.55rem0.04–0.05em
Tiny section headers0.5rem0.1emuppercase

Surfaces & Panels

Every panel follows this layered pattern:

--bg (page)
  └── --surface (panel/card)  border: 1px solid var(--border); border-radius: 4px;
        └── --surface2 (panel header, hover bg, inputs)

Panel header pattern:

.panel-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.75rem–0.85rem 1.25rem;
  border-bottom: 1px solid var(--border);
  background: var(--surface2);
  flex-shrink: 0;
}
.panel-header h2 {
  font-family: var(--font-mono);
  font-size: 0.7rem;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted);
}

Scrollable panel body:

.panel-body {
  flex: 1;
  overflow-y: auto;
  padding: 1.25rem;
  scrollbar-width: thin;
  scrollbar-color: var(--border) transparent;
}
.panel-body::-webkit-scrollbar { width: 3–4px; }
.panel-body::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }

Overlay / modal pattern:

.overlay {
  position: fixed;
  inset: 0;
  z-index: 4000;
  background: rgba(0, 0, 0, 0.6);
  display: flex;
  align-items: center;
  justify-content: center;
}
.modal {
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: 6px;
  box-shadow: 0 8px 40px rgba(0, 0, 0, 0.7);
  font-family: var(--font-mono);
}

Popup / popover pattern (smaller floating panels):

  • border-radius: 6px, box-shadow: 0 6px 24px rgba(0,0,0,0.5–0.6)
  • Same background: var(--surface), border: 1px solid var(--border)

Buttons

Standard button (.btn)

.btn {
  background: none;
  border: 1px solid var(--border);
  color: var(--text);
  border-radius: 2px;
  cursor: pointer;
  transition: all 0.15s;
  font-family: var(--font-mono);
  font-size: 0.65rem;
  letter-spacing: 0.08em;
  display: flex;
  align-items: center;
  gap: 0.35rem;
}
.btn:hover { border-color: var(--accent); color: var(--accent); }
.btn:active { background: var(--accent-subtle); }
.btn-sm { padding: 0.4rem 0.65rem; height: 30px; }
.btn-active { border-color: var(--accent) !important; color: var(--accent) !important; background: var(--accent-subtle); }

Accent (yellow) confirm button

background: var(--accent-subtle);
border: 1px solid var(--accent-border);
color: var(--accent);
/* hover: */ background: rgba(232, 255, 71, 0.18); border-color: rgba(232, 255, 71, 0.7);

Secondary (blue) action button

border: 1px solid var(--border);
background: transparent;
color: var(--accent2);
/* hover: */ background: var(--accent2-faint); border-color: var(--accent2);

Cancel / ghost button

background: transparent;
border: 1px solid var(--border);
color: var(--muted);
/* hover: */ background: var(--surface2); color: var(--text);

Destructive button

color: #ff9999;
border-color: rgba(255, 153, 153, 0.3);
/* hover: */ background: rgba(255, 119, 119, 0.1); border-color: var(--danger);

Play button (circular)

width: 42px; height: 42px;
border-radius: 50%;
border: 1.5px solid var(--accent);
color: var(--accent);
/* hover: */ background: rgba(232, 255, 71, 0.1); box-shadow: 0 0 16px rgba(232, 255, 71, 0.2);

All buttons: font-family: var(--font-mono), border-radius: 2–3px, transition: all 0.15s, cursor: pointer


Inputs

Text input

font-family: var(--font-mono);
font-size: 0.65rem;
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 3px;
color: var(--text);
padding: 0.3rem 0.5rem;
outline: none;
transition: border-color 0.15s;
/* focus: */ border-color: var(--accent2);
/* placeholder: */ color: var(--muted); opacity: 0.6;

Inline editable text (click-to-edit pattern)

  • Display state: cursor: text, hover shows background: rgba(255,255,255,0.05), border-radius: 2–3px, negative margin trick to keep layout stable
  • Edit state: border-bottom: 1.5px solid var(--accent), background: transparent, border: none (sides), outline: none

Range input

-webkit-appearance: none;
height: 2px;
background: var(--border);
border-radius: 1px;
/* thumb: */ width: 10px; height: 10px; border-radius: 50%; background: var(--accent);

Select

background: none;
border: 1px solid var(--border);
color: var(--muted);
font-family: var(--font-mono);
font-size: 0.65rem;
letter-spacing: 0.08em;
padding: 0.35rem 0.5rem;
border-radius: 2px;
height: 30px;
/* hover: */ border-color: var(--accent2); color: var(--accent2);

Badges & Tags

State badge (inline)

font-family: var(--font-mono);
font-size: 0.48–0.58rem;
letter-spacing: 0.05–0.06em;
padding: 2px 6px;
border-radius: 2–3px;
border: 1px solid;
white-space: nowrap;

Variants:

  • local: color: var(--muted); border-color: rgba(107,107,122,0.3);
  • server: color: var(--accent2); border-color: rgba(71,200,255,0.3); background: rgba(71,200,255,0.06);
  • modified: color: var(--accent); border-color: rgba(232,255,71,0.3); background: rgba(232,255,71,0.06);

<kbd> key display

background: rgba(255, 255, 255, 0.06);
border: 1px solid var(--border);
border-radius: 3px;
padding: 1px 4px;
color: var(--accent2);
font-family: var(--font-mono);
font-size: 0.55rem;

Color swatch dot

width: 8–10px; height: 8–10px;
border-radius: 50%;
flex-shrink: 0;

List / Table Items

Clickable list item

display: flex;
align-items: center;
padding: 0.45–0.65rem 1–1.25rem;
border-bottom: 1px solid rgba(42, 42, 50, 0.5);
cursor: pointer;
transition: background 0.1s;
font-family: var(--font-mono);
/* hover: */ background: var(--surface2);
/* active: */ background: var(--accent-faint); border-left: 2px solid var(--accent);

Table

border-collapse: collapse;
/* th: */ font-size: 0.6rem; font-weight: 600; letter-spacing: 0.1em; text-transform: uppercase;
          color: var(--muted); padding: 0.5rem 1.25rem; border-bottom: 1px solid var(--border);
          background: var(--surface); position: sticky; top: 0;
/* td: */ padding: 0.55rem 1.25rem; font-size: 0.7rem; color: var(--text);
/* tr hover: */ background: var(--surface2);
/* tr border: */ border-bottom: 1px solid rgba(42, 42, 50, 0.6);

Reveal-on-hover action buttons (delete, push, etc.)

opacity: 0;
transition: opacity 0.1s;
/* parent:hover > this: */ opacity: 1;
/* destructive hover: */ color: var(--danger);

Status Indicators

Status dot

width: 6px; height: 6px;
border-radius: 50%;
background: var(--muted); /* default/unknown */
/* .connected:  */ background: var(--success);
/* .connecting: */ background: var(--accent); animation: pulse 1s ease-in-out infinite;
/* .error:      */ background: var(--danger);

Pulse animation

@keyframes pulse {
  0%, 100% { opacity: 1; }
  50%       { opacity: 0.3; }
}

Loading bar

height: 2px;
background: var(--border);
overflow: hidden;
/* ::after: */ background: var(--accent); width: 40%; animation: loading 1s ease-in-out infinite;
@keyframes loading { 0% { left: -40%; } 100% { left: 100%; } }

Interaction & Transition Conventions

SituationValue
Background / border color changetransition: background 0.1s or all 0.15s
Opacity revealtransition: opacity 0.1s or 0.2s
Expand/collapse (sidebar, chevron)transition: width/transform 0.2s ease
Subtle scale on hover (swatch)transform: scale(1.3); transition: transform 0.1s
Box shadow on play button0 0 16px rgba(232, 255, 71, 0.2)

Empty States

display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
gap: 0.75rem;
color: var(--muted);
font-family: var(--font-mono);
font-size: 0.7rem;
letter-spacing: 0.06em;
text-align: center;
opacity: 0.5;

Z-Index Stack

Layerz-index
Hue picker popover1000
Context menu2000
Split popup3000
Confirm dialog overlay4000

Do / Don't

Do:

  • Use var(--*) for all colors, never hardcode palette colors
  • Use var(--font-mono) for all UI chrome; 'IBM Plex Sans', sans-serif for the body element only
  • Use the named alpha variables (--accent-faint, --accent-subtle, --accent-border, --accent2-faint, --accent2-border) instead of raw rgba() where they apply
  • Use --danger and --success for status colors instead of #ff7777 / #47ff8a
  • Keep border-radius small: 2px for inline elements, 3–4px for cards, 5–6px for floating panels/modals
  • Use low-opacity tint backgrounds for active/selected states, never solid fills
  • Use opacity: 0 + transition for reveal-on-hover actions (not display: none)
  • var(--danger) only appears on hover, never as a resting state color

Don't:

  • Don't use saturated or bright backgrounds — all surfaces are near-black
  • Don't use font-weight other than 500 or 600 for Mono (regular is 400, only use 500/600 for emphasis)
  • Don't use box-shadow for depth except on floating overlays/popovers
  • Don't use color for decorative purposes — color encodes state (accent=active, accent2=server, danger=error/destructive, success=connected)
  • Don't hardcode rgba(232, 255, 71, …) or rgba(71, 200, 255, …) when a named alpha variable covers the value