Files
VoiceStudio/frontend/src/index.css
T

7702 lines
269 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
@layer theme, base, components, utilities;
@import "tailwindcss/theme.css" layer(theme);
@import "tailwindcss/utilities.css" layer(utilities);
/* tw-animate-css — keyframes + animate-* utilities the shadcn primitives use
for enter/exit transitions (Dialog/Dropdown/etc.). Foundation-only for now:
Button/Input don't reference it, but it's part of the shadcn v4 baseline so
future `npx shadcn add` components render their animations out of the box. */
@import "tw-animate-css";
/* ════════════════════════════════════════════════════════════════════
CONSOLIDATED FOUNDATION (P5 — single foundation file)
Folded in from the former src/ui/tokens.css + src/ui/themes.css so the
design-token foundation lives in ONE file.
ORDER IS LOAD-BEARING — the cascade's correctness depends on it:
1. The default token / bridge layer FIRST — the token-scale `:root`,
the `@theme` base + `@theme inline` shadcn bridge, and the default
legacy/chrome `:root`.
2. ALL `[data-theme=…]` per-theme overrides LAST — after every default
`:root`.
Why themes MUST come after the default `:root` blocks: in the real app
`data-theme` is set on <html>, and <html> IS `:root`
(documentElement === :root). So a plain `:root {…}` and a
`[data-theme="x"] {…}` BOTH match the SAME element at EQUAL specificity
(0,1,0) — SOURCE ORDER is the only tiebreaker, and the later block wins.
The `--chrome-*` (and `--color-*`) tokens are declared in BOTH, so if a
default `:root` sits AFTER the themes it clobbers every theme's overrides
and the app chrome (Settings hub, header, footer) stops recoloring. That
was the P5-consolidation regression: the default chrome `:root` had been
inlined AFTER the [data-theme] blocks, so Midnight/Catppuccin/Nord/…
never recolored the chrome on the real <html> app.
Keep the `@theme` + `@theme inline` blocks ADJACENT and BEFORE the
[data-theme] blocks: Tailwind v4 compiles them, and moving a `:root`
between them (or a theme block across them) changes the GENERATED CSS
(e.g. `@theme inline` stops inlining, so shadcn utilities lose their
brand color). The fix keeps that Tailwind region byte-for-byte intact and
instead places the [data-theme] blocks last, after the default
legacy/chrome `:root`.
Do NOT "fix" a recurrence by bumping specificity to
`:root[data-theme="x"]` — that stops matching the visual harness, which
sets `data-theme` on a WRAPPER div (a closer ancestor that wins by
proximity, not source order). Order is the correct lever; specificity is
not. Guarded by src/test/themeCascade.test.js (source-order cascade sim)
+ the tokenParity + visual suites.
════════════════════════════════════════════════════════════════════ */
/* ────────────────────────────────────────────────────────────────────
OmniVoice design tokens — single source of truth.
Extends the legacy vars in index.css :root. Do not duplicate here.
New tokens should land in this file, not in index.css.
──────────────────────────────────────────────────────────────────── */
:root {
/* ── Color scale (semantic) ─────────────────────────────────────── */
/* The semantic --color-* base tokens are the single source of truth in
index.css's `@theme` block (so Tailwind v4 owns them and they can't
drift). `[data-theme]` overrides in themes.css are unlayered, so they
still win over the @theme base. Only tokens NOT in @theme live here. */
/* legacy aliases live in index.css: --primary, --accent, --bg, etc. */
--color-muted-mono: #d5c4a1;
/* ── Spacing (4px base) ─────────────────────────────────────────── */
--space-0: 0;
--space-1: 2px;
--space-2: 4px;
--space-3: 6px; /* alias: --gap */
--space-4: 8px;
--space-5: 12px;
--space-6: 16px;
--space-7: 24px;
--space-8: 32px;
--space-9: 44px;
/* ── Settings layout ────────────────────────────────────────────────
One source of truth for the Settings shell so the content can't sprawl
edge-to-edge (the "too spread out" report). The measure caps the content
column at a readable width — like macOS System Settings — instead of the
old 1280px; the rail token keeps the nav width in lockstep with the grid. */
--settings-measure: 660px;
--settings-rail: 168px;
/* ── Radius ─────────────────────────────────────────────────────── */
/* --radius-xs..xl are the single source of truth in index.css's @theme.
Only --radius-pill (not in @theme) lives here. */
--radius-pill: 999px;
/* ── Typography (Option A — Codex-inspired stack) ──────────────────
Inter for UI/body (closest OSS Söhne analog), IBM Plex Mono for chrome
labels + data + logs (warmer than Geist Mono, pairs with OpenAI's feel),
Source Serif 4 for hero moments only. Variable faces where possible so
weight 400–700 comes from a single file download.
--font-sans / --font-mono / --font-serif are the single source of truth
in index.css's @theme (full stacks). Only the aliases below live here. */
--font-display: var(--font-serif);
--font-ui: var(--font-sans);
--text-2xs: 0.58rem;
--text-xs: 0.62rem;
--text-sm: 0.68rem;
--text-base: 0.72rem;
--text-md: 0.78rem;
--text-lg: 0.88rem;
--text-xl: 1.05rem;
--text-2xl: 1.4rem;
--weight-regular: 400;
--weight-medium: 500;
--weight-semibold: 600;
--weight-bold: 700;
--weight-black: 800;
/* ── Elevation ──────────────────────────────────────────────────── */
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.2);
--shadow-md: 0 2px 8px rgba(0, 0, 0, 0.4); /* alias: --glass-shadow */
--shadow-lg: 0 8px 24px -12px rgba(0, 0, 0, 0.5);
--shadow-xl: 0 20px 40px -10px rgba(0, 0, 0, 0.6);
--shadow-glow: 0 0 12px var(--color-brand-glow);
--shadow-inset: inset 0 1px 2px rgba(0, 0, 0, 0.3);
/* ── Motion ─────────────────────────────────────────────────────── */
--dur-instant: 0.08s;
--dur-fast: 0.12s; /* alias: --transition-fast */
--dur-base: 0.2s; /* alias: --transition-smooth duration */
--dur-slow: 0.3s;
--dur-xslow: 0.5s;
--ease-out: cubic-bezier(0.4, 0, 0.2, 1);
--ease-in-out: cubic-bezier(0.4, 0, 0.6, 1);
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
--ease-bounce: cubic-bezier(0.175, 0.885, 0.32, 1.275);
/* ── Z-index scale ──────────────────────────────────────────────── */
--z-base: 1;
--z-docked: 10;
--z-sticky: 100;
--z-overlay: 1000;
--z-dialog: 1100;
--z-toast: 1200;
--z-max: 9999;
/* ── Focus ring (unified) ──────────────────────────────────────── */
--focus-ring:
0 0 0 2px var(--color-bg),
0 0 0 4px rgba(211, 134, 155, 0.45);
/* ── Glass effects ──────────────────────────────────────────────── */
--glass-blur-sm: blur(6px);
--glass-blur-md: blur(16px) saturate(140%); /* alias: --blur */
--glass-blur-lg: blur(24px) saturate(160%);
}
/* ── Launch animation ─────────────────────────────────────────── */
@keyframes ui-app-launch {
from { opacity: 0; transform: translateY(6px) scale(0.995); filter: blur(6px); }
to { opacity: 1; transform: translateY(0) scale(1); filter: blur(0); }
}
@keyframes ui-app-launch-reduced {
from { opacity: 0; }
to { opacity: 1; }
}
#root {
animation: ui-app-launch 520ms cubic-bezier(0.22, 1, 0.36, 1) both;
}
@media (prefers-reduced-motion: reduce) {
#root { animation: ui-app-launch-reduced 180ms ease-out both; }
}
/* Skeleton shimmer utility for first-load placeholders */
@keyframes ui-skel-shimmer {
from { background-position: -200% 0; }
to { background-position: 200% 0; }
}
.ui-skel {
background:
linear-gradient(90deg, rgba(255, 255, 255, 0.02), rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0.02));
background-size: 200% 100%;
animation: ui-skel-shimmer 1.6s linear infinite;
border-radius: var(--radius-md);
}
/* ── Map design tokens → Tailwind v4 theme ──────────────────────────── */
@theme {
/* Colors — semantic */
--color-fg: #ebdbb2;
--color-fg-muted: #a89984;
--color-fg-subtle: #7c6f64;
--color-fg-inverse: #1d2021;
--color-bg: #1d2021;
--color-bg-elev-1: rgba(50, 48, 47, 0.85);
--color-bg-elev-2: rgba(0, 0, 0, 0.30);
--color-bg-elev-3: rgba(0, 0, 0, 0.18);
--color-border: rgba(255, 255, 255, 0.07);
--color-border-strong: rgba(255, 255, 255, 0.15);
--color-border-warm: rgba(243, 165, 182, 0.08);
--color-brand: #d3869b;
--color-brand-hover: #b16286;
--color-brand-glow: rgba(211, 134, 155, 0.4);
--color-accent: #fabd2f;
--color-success: #8ec07c;
--color-warn: #fe8019;
--color-danger: #fb4934;
--color-info: #83a598;
--color-chrome-bg: #0f1011;
--color-chrome-fg: #d5c4a1;
/* Radius */
--radius-xs: 2px;
--radius-sm: 3px;
--radius-md: 4px;
--radius-lg: 6px;
--radius-xl: 10px;
/* Fonts — full font stacks. These are the single source of truth; the
identical declarations were removed from the former ui/tokens.css :root
(now consolidated above; being unlayered, that copy used to win — so
these literals carry the exact stacks that were computed before the
de-dup, preserving every resolved value). */
--font-sans: 'Inter Variable', 'Inter', -apple-system, BlinkMacSystemFont, 'Söhne', 'Helvetica Neue', Arial, sans-serif;
--font-mono: 'IBM Plex Mono', ui-monospace, 'SF Mono', 'Cascadia Code', Menlo, monospace;
--font-serif: 'Source Serif 4 Variable', 'Source Serif 4', 'Söhne Breit', Georgia, serif;
}
/* ── shadcn/ui semantic token bridge ─────────────────────────────────────
Maps shadcn's component token vocabulary onto OmniVoice's EXISTING palette
so shadcn primitives (src/components/ui/*) inherit the current look instead
of shadcn's default grayscale.
`@theme inline` emits Tailwind utilities (bg-background, text-foreground,
bg-primary, bg-card, text-muted-foreground, border-input, ring-ring,
bg-destructive, …) whose values reference the OmniVoice `--color-*` tokens
*by name*. Because those tokens are re-declared per theme in the
[data-theme] blocks BELOW (they must stay after this bridge so they win
by source order on <html> — see the file header), theme switching
recolors every shadcn component automatically — there is no separate
per-theme shadcn block to keep in sync.
Notes:
• `bg-accent`/`text-accent` and `border-border` already exist (the base
@theme above declares --color-accent and --color-border), so shadcn's
`accent` and `border` reuse them unchanged — re-emitting them here would be
a self-referential no-op, so they're intentionally omitted.
• The --radius-* scale (xs..xl) is left untouched; only the new shadcn
`--radius` base is added (→ --radius-lg = 6px). shadcn's rounded-md picks
up OmniVoice's existing --radius-md (4px), staying on-palette.
• --primary-foreground / --accent-foreground / --destructive-foreground use
--color-fg-inverse (dark) — matches the existing Button primary, which
paints dark text on the bright brand/accent fill across every theme. */
:root {
--radius: var(--radius-lg);
}
@theme inline {
--color-background: var(--color-bg);
--color-foreground: var(--color-fg);
--color-card: var(--color-bg-elev-1);
--color-card-foreground: var(--color-fg);
--color-popover: var(--color-bg-elev-1);
--color-popover-foreground: var(--color-fg);
--color-primary: var(--color-brand);
--color-primary-foreground: var(--color-fg-inverse);
--color-secondary: var(--color-bg-elev-2);
--color-secondary-foreground: var(--color-fg);
--color-muted: var(--color-bg-elev-2);
--color-muted-foreground: var(--color-fg-muted);
--color-accent-foreground: var(--color-fg-inverse);
--color-destructive: var(--color-danger);
--color-destructive-foreground: var(--color-fg-inverse);
--color-input: var(--color-border);
--color-ring: var(--color-brand);
}
/* Font imports removed — the chrome token system uses only system monospace
(ui-monospace, SFMono-Regular, Menlo, Consolas). Fraunces / Nunito / Outfit
/ Inter are no longer referenced by any rule. */
:root {
/* Default theme is Gruvbox Dark (--bg #1d2021 / --chrome-bg #0f1011).
Declaring `color-scheme: dark` (NOT the same as the prefers-color-scheme
media query below) makes UA-rendered form chrome — native <select> option
popups, scrollbars, autofill, spin-buttons — render in the dark scheme so
they match the app instead of defaulting to the OS light scheme. Every
[data-theme] block re-asserts the scheme matching its own bg lightness. */
color-scheme: dark;
--primary: #d3869b;
--primary-hover: #b16286;
--accent: #fabd2f;
--bg: #1d2021;
--glass-bg: rgba(50, 48, 47, 0.85);
--glass-border: rgba(235, 219, 178, 0.08);
--glass-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.4);
--blur: blur(16px) saturate(140%);
--text-primary: #ebdbb2;
--text-secondary: #a89984;
--radius: 6px;
--gap: 6px;
--transition-fast: 0.12s ease;
--transition-smooth: 0.2s cubic-bezier(0.4, 0, 0.2, 1);
/* ── Chrome tokens ──
Introduced with the LogsFooter status bar. Surfaces that want the
same tight dark-chrome look reference these (Header topbar now,
future: Sidebar edges, Settings tab bar, NavRail). Keeping values
in one place means style tweaks flow everywhere in sync. */
--chrome-bg: #0f1011;
--chrome-border: rgba(255, 255, 255, 0.08);
--chrome-border-strong: rgba(255, 255, 255, 0.12);
--chrome-fg: #d5c4a1;
--chrome-fg-muted: #a89984;
--chrome-fg-dim: #7c6f64;
/* Accent family aliases the THEMED brand token (--color-brand, re-declared per
[data-theme]) so every accent surface — donate/support/commercial CTAs,
active tabs, .btn-primary, status pills, GoalBar/Pip — tracks the active
theme instead of the old fixed pink. (--chrome-accent-border stays zeroed by
the app-wide border-removal block below; the visible accent cue is the fg
color + the -bg tint.) */
--chrome-accent: var(--color-brand);
--chrome-accent-bg: color-mix(in srgb, var(--color-brand) 12%, transparent);
--chrome-accent-border: color-mix(in srgb, var(--color-brand) 35%, transparent);
--chrome-severity-err: #fb4934;
--chrome-severity-warn: #fabd2f;
--chrome-severity-ok: #8ec07c;
--chrome-radius-pill: 3px;
--chrome-hover-bg: rgba(255, 255, 255, 0.04);
/* Native <select> caret. One token consumed by both select.input-base and
.ui-select so the chevron color lives in a single place and is overridden
per [data-theme] to track that theme's muted foreground (was a hardcoded
gray %23a1a1aa that ignored theme + accent). Stroke color = --chrome-fg-muted
baked into the data-URI (a background-image SVG can't read a CSS var, so the
color is inlined; each theme block re-declares this with its own fg-muted). */
--select-caret: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2210%22%20height%3D%226%22%3E%3Cpath%20d%3D%22M1%201l4%204%204-4%22%20fill%3D%22none%22%20stroke%3D%22%23a89984%22%20stroke-width%3D%221.5%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%2F%3E%3C%2Fsvg%3E");
/* Chrome-label face is IBM Plex Mono now — defined in the @theme block as
--font-mono. Keep this alias so existing `var(--chrome-font-mono)` rules
don't need to change; it just picks up the new face automatically. */
--chrome-font-mono: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
--chrome-label-size: 11px;
--chrome-label-track: 0.06em;
--chrome-bar-h: 28px;
--chrome-pill-h: 20px;
--chrome-icon-btn: 22px;
}
/* ────────────────────────────────────────────────────────────────────
OmniVoice theme definitions.
Each theme overrides the semantic tokens defined in the default `:root` +
`@theme` blocks ABOVE. These [data-theme] blocks MUST stay LAST — after
every default `:root` (incl. the legacy/chrome `:root`) — so they win by
source order when `data-theme` is set on <html> (=== :root). See the file
header for the full rationale; guarded by src/test/themeCascade.test.js.
The active theme is set via a `data-theme` attribute on <html>.
Default (no attribute) = Gruvbox Dark.
Usage:
document.documentElement.setAttribute('data-theme', 'midnight');
shadcn/ui note: the shadcn semantic tokens (--color-background, -primary,
-card, -muted-foreground, -destructive, -ring, …) are bridged in index.css
to the OmniVoice --color-* tokens ABOVE (e.g. --color-primary → --color-brand).
So every [data-theme] block here recolors shadcn components automatically —
overriding --color-brand/-bg/-danger/etc. is all that's needed; there is no
separate shadcn token block to maintain per theme.
──────────────────────────────────────────────────────────────────── */
/* ── Midnight Blue ─────────────────────────────────────────────────── */
[data-theme="midnight"] {
--color-fg: #e2e8f0;
--color-fg-muted: #94a3b8;
--color-fg-subtle: #64748b;
--color-fg-inverse: #0f172a;
--color-bg: #0f172a;
--color-bg-elev-1: rgba(30, 41, 59, 0.85);
--color-bg-elev-2: rgba(15, 23, 42, 0.40);
--color-bg-elev-3: rgba(30, 41, 59, 0.30);
--color-border: rgba(148, 163, 184, 0.10);
--color-border-strong: rgba(148, 163, 184, 0.20);
--color-border-warm: rgba(139, 92, 246, 0.12);
--color-brand: #8b5cf6;
--color-brand-hover: #7c3aed;
--color-brand-glow: rgba(139, 92, 246, 0.35);
--color-accent: #f59e0b;
--color-success: #10b981;
--color-warn: #f97316;
--color-danger: #ef4444;
--color-info: #3b82f6;
/* Chrome overrides for components using legacy vars */
--chrome-bg: #1e293b;
--chrome-fg: #e2e8f0;
--chrome-fg-muted: #94a3b8;
--chrome-fg-dim: #475569;
--chrome-border: #334155;
/* Midnight Blue — bg #0f172a is dark. */
color-scheme: dark;
--select-caret: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2210%22%20height%3D%226%22%3E%3Cpath%20d%3D%22M1%201l4%204%204-4%22%20fill%3D%22none%22%20stroke%3D%22%2394a3b8%22%20stroke-width%3D%221.5%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%2F%3E%3C%2Fsvg%3E");
}
/* ── Nord ──────────────────────────────────────────────────────────── */
[data-theme="nord"] {
--color-fg: #eceff4;
--color-fg-muted: #d8dee9;
--color-fg-subtle: #4c566a;
--color-fg-inverse: #2e3440;
--color-bg: #2e3440;
--color-bg-elev-1: rgba(59, 66, 82, 0.85);
--color-bg-elev-2: rgba(46, 52, 64, 0.40);
--color-bg-elev-3: rgba(59, 66, 82, 0.25);
--color-border: rgba(216, 222, 233, 0.08);
--color-border-strong: rgba(216, 222, 233, 0.15);
--color-border-warm: rgba(136, 192, 208, 0.10);
--color-brand: #88c0d0;
--color-brand-hover: #81a1c1;
--color-brand-glow: rgba(136, 192, 208, 0.3);
--color-accent: #ebcb8b;
--color-success: #a3be8c;
--color-warn: #d08770;
--color-danger: #bf616a;
--color-info: #5e81ac;
--chrome-bg: #3b4252;
--chrome-fg: #eceff4;
--chrome-fg-muted: #d8dee9;
--chrome-fg-dim: #4c566a;
--chrome-border: #434c5e;
/* Nord — bg #2e3440 is dark. */
color-scheme: dark;
--select-caret: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2210%22%20height%3D%226%22%3E%3Cpath%20d%3D%22M1%201l4%204%204-4%22%20fill%3D%22none%22%20stroke%3D%22%23d8dee9%22%20stroke-width%3D%221.5%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%2F%3E%3C%2Fsvg%3E");
}
/* ── Solarized Dark ────────────────────────────────────────────────── */
[data-theme="solarized"] {
--color-fg: #839496;
--color-fg-muted: #657b83;
--color-fg-subtle: #586e75;
--color-fg-inverse: #fdf6e3;
--color-bg: #002b36;
--color-bg-elev-1: rgba(7, 54, 66, 0.90);
--color-bg-elev-2: rgba(0, 43, 54, 0.40);
--color-bg-elev-3: rgba(7, 54, 66, 0.25);
--color-border: rgba(131, 148, 150, 0.10);
--color-border-strong: rgba(131, 148, 150, 0.18);
--color-border-warm: rgba(181, 137, 0, 0.10);
--color-brand: #268bd2;
--color-brand-hover: #2aa198;
--color-brand-glow: rgba(38, 139, 210, 0.3);
--color-accent: #b58900;
--color-success: #859900;
--color-warn: #cb4b16;
--color-danger: #dc322f;
--color-info: #6c71c4;
--chrome-bg: #073642;
--chrome-fg: #93a1a1;
/* 10x P4 contrast: was #657b83 (2.92:1 on --chrome-bg) → #899da4 (4.59:1).
Same hue (hsl 196°), lightness raised 45%→59% to clear WCAG AA 4.5:1. */
--chrome-fg-muted: #899da4;
--chrome-fg-dim: #586e75;
--chrome-border: #073642;
/* Solarized Dark — bg #002b36 is dark. */
color-scheme: dark;
--select-caret: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2210%22%20height%3D%226%22%3E%3Cpath%20d%3D%22M1%201l4%204%204-4%22%20fill%3D%22none%22%20stroke%3D%22%23899da4%22%20stroke-width%3D%221.5%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%2F%3E%3C%2Fsvg%3E");
}
/* ── Rose Pine ─────────────────────────────────────────────────────── */
[data-theme="rose-pine"] {
--color-fg: #e0def4;
--color-fg-muted: #908caa;
--color-fg-subtle: #6e6a86;
--color-fg-inverse: #191724;
--color-bg: #191724;
--color-bg-elev-1: rgba(30, 27, 42, 0.90);
--color-bg-elev-2: rgba(25, 23, 36, 0.40);
--color-bg-elev-3: rgba(38, 35, 58, 0.30);
--color-border: rgba(144, 140, 170, 0.10);
--color-border-strong: rgba(144, 140, 170, 0.18);
--color-border-warm: rgba(235, 188, 186, 0.10);
--color-brand: #ebbcba;
--color-brand-hover: #eb6f92;
--color-brand-glow: rgba(235, 188, 186, 0.3);
--color-accent: #f6c177;
--color-success: #9ccfd8;
--color-warn: #f6c177;
--color-danger: #eb6f92;
--color-info: #c4a7e7;
--chrome-bg: #1f1d2e;
--chrome-fg: #e0def4;
--chrome-fg-muted: #908caa;
--chrome-fg-dim: #6e6a86;
--chrome-border: #26233a;
/* Rose Pine — bg #191724 is dark. */
color-scheme: dark;
--select-caret: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2210%22%20height%3D%226%22%3E%3Cpath%20d%3D%22M1%201l4%204%204-4%22%20fill%3D%22none%22%20stroke%3D%22%23908caa%22%20stroke-width%3D%221.5%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%2F%3E%3C%2Fsvg%3E");
}
/* ── Catppuccin Mocha ──────────────────────────────────────────────── */
[data-theme="catppuccin"] {
--color-fg: #cdd6f4;
--color-fg-muted: #a6adc8;
--color-fg-subtle: #6c7086;
--color-fg-inverse: #1e1e2e;
--color-bg: #1e1e2e;
--color-bg-elev-1: rgba(49, 50, 68, 0.85);
--color-bg-elev-2: rgba(30, 30, 46, 0.40);
--color-bg-elev-3: rgba(49, 50, 68, 0.25);
--color-border: rgba(166, 173, 200, 0.08);
--color-border-strong: rgba(166, 173, 200, 0.16);
--color-border-warm: rgba(245, 194, 231, 0.10);
--color-brand: #cba6f7;
--color-brand-hover: #b4befe;
--color-brand-glow: rgba(203, 166, 247, 0.3);
--color-accent: #f9e2af;
--color-success: #a6e3a1;
--color-warn: #fab387;
--color-danger: #f38ba8;
--color-info: #89b4fa;
--chrome-bg: #313244;
--chrome-fg: #cdd6f4;
--chrome-fg-muted: #a6adc8;
--chrome-fg-dim: #585b70;
--chrome-border: #45475a;
/* Catppuccin Mocha — bg #1e1e2e is dark. */
color-scheme: dark;
--select-caret: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2210%22%20height%3D%226%22%3E%3Cpath%20d%3D%22M1%201l4%204%204-4%22%20fill%3D%22none%22%20stroke%3D%22%23a6adc8%22%20stroke-width%3D%221.5%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%2F%3E%3C%2Fsvg%3E");
}
/* ── Light (Solarized Light) ───────────────────────────────────────── */
[data-theme="light"] {
--color-fg: #073642;
--color-fg-muted: #586e75;
--color-fg-subtle: #4a5a60;
--color-fg-inverse: #fdf6e3;
--color-bg: #fdf6e3;
--color-bg-elev-1: rgba(238, 232, 213, 0.85);
--color-bg-elev-2: rgba(238, 232, 213, 0.40);
--color-bg-elev-3: rgba(238, 232, 213, 0.25);
--color-border: rgba(7, 54, 66, 0.12);
--color-border-strong: rgba(7, 54, 66, 0.20);
--color-border-warm: rgba(181, 137, 0, 0.15);
--color-brand: #1d6b9f;
--color-brand-hover: #144b70;
--color-brand-glow: rgba(29, 107, 159, 0.25);
--color-accent: #8c6b00;
--color-success: #667500;
--color-warn: #9c3a11;
--color-danger: #a82624;
--color-info: #515594;
--chrome-bg: #eee8d5;
--chrome-fg: #073642;
--chrome-fg-muted: #4d5f66;
--chrome-fg-dim: #4a5a60;
--chrome-border: #d5ccb7;
color-scheme: light;
--select-caret: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2210%22%20height%3D%226%22%3E%3Cpath%20d%3D%22M1%201l4%204%204-4%22%20fill%3D%22none%22%20stroke%3D%22%23586e75%22%20stroke-width%3D%221.5%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%2F%3E%3C%2Fsvg%3E");
}
/* ── System-preference sync ────────────────────────────────────────── */
/* When theme is "auto", the light theme activates on light-mode OS. */
@media (prefers-color-scheme: light) {
[data-theme="auto"] {
--color-fg: #073642;
--color-fg-muted: #586e75;
--color-fg-subtle: #4a5a60;
--color-fg-inverse: #fdf6e3;
--color-bg: #fdf6e3;
--color-bg-elev-1: rgba(238, 232, 213, 0.85);
--color-bg-elev-2: rgba(238, 232, 213, 0.40);
--color-bg-elev-3: rgba(238, 232, 213, 0.25);
--color-border: rgba(7, 54, 66, 0.12);
--color-border-strong: rgba(7, 54, 66, 0.20);
--color-border-warm: rgba(181, 137, 0, 0.15);
--color-brand: #1d6b9f;
--color-brand-hover: #144b70;
--color-brand-glow: rgba(29, 107, 159, 0.25);
--color-accent: #8c6b00;
--color-success: #667500;
--color-warn: #9c3a11;
--color-danger: #a82624;
--color-info: #515594;
--chrome-bg: #eee8d5;
--chrome-fg: #073642;
--chrome-fg-muted: #4d5f66;
--chrome-fg-dim: #4a5a60;
--chrome-border: #d5ccb7;
color-scheme: light;
--select-caret: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2210%22%20height%3D%226%22%3E%3Cpath%20d%3D%22M1%201l4%204%204-4%22%20fill%3D%22none%22%20stroke%3D%22%23586e75%22%20stroke-width%3D%221.5%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%2F%3E%3C%2Fsvg%3E");
}
}
/* ═══ ANIMATIONS ═══ */
@keyframes mesh {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
@keyframes spin { to { transform: rotate(360deg); } }
@keyframes pulse { 0%, 100% { opacity: 1; box-shadow: 0 0 0 0 rgba(251,73,52,0.4); } 50% { opacity: 0.85; box-shadow: 0 0 12px 4px rgba(251,73,52,0.3); } }
@keyframes fadeIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
/* ═══ FOUNDATION ═══ */
*, *::before, *::after { box-sizing: border-box; }
::selection {
background: color-mix(in srgb, var(--chrome-accent) 35%, transparent);
color: #fff;
}
/* Root rendering — applies to everything unless overridden. */
html {
font-optical-sizing: auto;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-synthesis: none;
/* Inter OT flags that nudge it toward Söhne:
cv11 – single-story `a`
ss01 – alt digits (straight 4, balanced 6/9)
ss03 – alt `l` with serif-foot (legibility win at 10–12 px) */
font-feature-settings: "cv11", "ss01", "ss03";
/* Make sure kerning is on — some WebKit builds leave it off by default. */
font-kerning: normal;
}
body {
margin: 0; padding: 0;
background-color: var(--chrome-bg);
color: var(--chrome-fg);
font-family: var(--font-sans);
font-size: 13px; /* was 12px — +1 to give Inter room to breathe */
line-height: 1.5;
overflow-x: hidden;
min-height: 100vh;
}
/* Default headings — sans, tight leading. Overrides live per page. */
h1, h2, h3, h4 {
font-family: var(--font-sans);
margin: 0;
letter-spacing: -0.005em;
}
/* Tabular numbers + slashed zero wherever columns of digits need to line up.
Anything already on `--font-mono` inherits mono + these features. */
.segment-time,
.segment-table code,
.ui-table-toolbar__meta,
.logs-footer__count,
[data-tabular] {
font-variant-numeric: tabular-nums slashed-zero;
}
/* Mono rule of thumb — IBM Plex Mono ships with slashed zero in its `zero`
OT feature, tabular by default. Make both explicit so a browser that
ignores the feature-settings still gets it. */
code,
kbd,
pre,
samp,
[class*="chrome-font-mono"],
[style*="chrome-font-mono"] {
font-variant-numeric: tabular-nums slashed-zero;
font-feature-settings: "zero", "ss02";
}
#root {
display: flex;
/* Anchor top-left so the full-viewport .app-container renders from the corner. */
justify-content: flex-start;
padding: 0;
width: 100%;
min-height: 100vh;
height: 100vh;
overflow: hidden;
}
/* ═══ DICTATION WIDGET WINDOW ═══
The widget (`label: "widget"`, tauri.conf.json) is declared
`transparent: true` + `decorations: false` so the dictation pill floats as
a rounded capsule over whatever the user is dictating into. But it loads
the same index.html as the main window, so the chrome background above
applied to it too and painted an opaque, square-cornered rectangle across
the whole 300x64 window — visible as a dark block sitting on the desktop
whenever the pill itself wasn't drawing (see CaptureWidget's idle state).
Window transparency only shows through if every ancestor box is
transparent, so all three of html/body/#root have to opt out.
`data-window` is set in main-app.jsx once the window label is known. */
html[data-window='widget'],
html[data-window='widget'] body,
html[data-window='widget'] #root {
background: transparent;
background-color: transparent;
}
html[data-window='widget'] #root {
/* Let clicks through the empty area around the capsule rather than
swallowing them on an invisible full-window box. */
pointer-events: none;
}
html[data-window='widget'] #root > * {
pointer-events: auto;
}
/* ═══ LAYOUT ═══ */
.app-container {
/* UI scale (#504 clipping fix + #21 black-band fix): the layout box is
shrunk by the scale factor, then `zoom` magnifies it back to the viewport.
This guarantees the scaled UI never overflows the window and never leaves
black bands:
• Chromium (macOS/Windows): `zoom` magnifies the shrunk layout box back
to exactly `100vw × 100vh`, so content fits with no clipping.
• Older WebKitGTK (Linux): `zoom` is a layout no-op, so the shrunk
`100vw/scale` box would leave a black band on the right/bottom AND push
the bottom Generate/Settings CTAs off-screen (#523/#524). The App.jsx
zoom-layout probe sets `data-zoom-layout=off` on those engines, and the
override below renders the shell at 1.0 filling the window (no band, no
clip). Newer WebKitGTK that honors zoom keeps the scaled path.
The old `transform: scale(var(--ui-scale))` approach is what caused black
bands on WebKitGTK because the transform didn't magnify the shrunk shell.
⚠️ Do NOT replace `zoom` with `transform: scale(var(--ui-scale))` — a
regression test (src/test/appShellScale.test.js) fails CI if that pattern
returns. */
width: calc(100vw / var(--ui-scale, 1));
height: calc(100vh / var(--ui-scale, 1));
zoom: var(--ui-scale, 1);
max-width: none;
display: grid;
/* [rail 48px] [sidebar 180–220] [main 1fr] */
grid-template-columns: 48px minmax(180px, 220px) minmax(0, 4fr);
/* Rows: [header] [content 1fr] [GlobalAudioPlayer] [LogsFooter]. The footer
is a REAL grid row (not a fixed overlay): content in row 2 physically ends
at the footer's top edge, so bottom buttons can never render underneath it
— at any window height, collapsed or expanded. This retires the whole
padding-bottom/--logs-footer-height reservation class of clipping bugs
(#476, #504, and the small-window "buttons hidden under footer" report).
Row 3 is the global audio mini-player's dock (auto → collapses to 0 while
idle); it gets the same in-flow guarantee, so the fixed-overlay overlap
the #1032 stop pill had at 1440×900 cannot recur. The nav rail spans rows
2/-1 so it still runs top-to-bottom beside both bars, exactly like the
old fixed-inset look. */
grid-template-rows: auto 1fr auto auto;
gap: 0px;
overflow: hidden;
transition: grid-template-columns var(--transition-smooth);
}
/* Older WebKitGTK (Linux), where the App.jsx probe found `zoom` is a layout
no-op → html[data-zoom-layout=off]: the calc(100vw/scale) box can't be
magnified back, so it leaves a black band and clips the bottom Generate/
Settings CTAs (#523/#524). Fill the viewport at 1.0 instead — no band, no
clip. The probe leaves the zoom path in place on engines that honor zoom. */
html[data-zoom-layout='off'] .app-container {
width: 100vw;
height: 100vh;
zoom: 1;
}
/* Tauri applies UI scale to the webview itself. Keep the document shell at a
true viewport size there; combining native zoom with this CSS zoom contract
would double-scale and recreate the right/bottom dead bands. */
html[data-ui-scale-engine='native'] .app-container {
width: 100vw;
height: 100vh;
zoom: 1;
}
/* No padding-bottom footer reservation here anymore: the LogsFooter occupies
grid row 3 (see grid-template-rows above), so row-2 content stops at its
top edge by construction. The footer still writes --logs-footer-height for
the fixed toasts/previews that anchor above it (VoicePreview, ExportModal,
etc.). */
.app-container.sidebar-collapsed {
grid-template-columns: 48px 46px minmax(0, 4fr);
}
.app-container.sidebar-hidden {
grid-template-columns: 48px minmax(0, 1fr);
}
.app-container.sidebar-hidden > .history-panel {
display: none;
}
.app-container.sidebar-hidden > .main-content {
grid-column: 2; /* rail col1, main col2 */
}
/* Rail on right side */
.app-container.rail-right {
grid-template-columns: minmax(180px, 220px) minmax(0, 4fr) 48px;
}
.app-container.rail-right.sidebar-collapsed {
grid-template-columns: 46px minmax(0, 4fr) 48px;
}
.app-container.rail-right.sidebar-hidden {
grid-template-columns: minmax(0, 1fr) 48px;
}
.app-container.rail-right > .nav-rail { grid-column: 3; }
.app-container.rail-right > .history-panel { grid-column: 1; }
.app-container.rail-right > .main-content { grid-column: 2; }
.app-container.rail-right.sidebar-hidden > .main-content { grid-column: 1; }
/* sidebar-hidden collapses the template to 2 columns — the rail must move to
column 2, or it overflows into a phantom column and leaves the reserved
48px slot as a dead black gap beside it. */
.app-container.rail-right.sidebar-hidden > .nav-rail { grid-column: 2; }
/* …and the same off-by-one under sidebar-collapsed's 3-column template. */
.app-container.rail-right.sidebar-collapsed > .history-panel { grid-column: 1; }
.app-container > .nav-rail {
grid-column: 1;
/* Span the footer row too: the rail runs top-to-bottom beside the footer
(same look as the old fixed-footer 48px inset). */
grid-row: 2 / -1;
}
.app-container > .history-panel {
grid-column: 2;
grid-row: 2;
}
.app-container > .main-content {
display: flex;
flex-direction: column;
overflow-y: auto;
min-height: 0;
grid-column: 3;
grid-row: 2;
/* Flat chrome — the shared warm radial + paper grain wash was the
root cause of the "too warm" feel across Clone / Design / Dub /
Settings. Every tab now sits on the same neutral chrome bg. */
background: var(--chrome-bg);
position: relative;
}
.app-container > .main-content::before { display: none; }
.app-container > .main-content > * { position: relative; z-index: 1; }
.app-container > div { min-width: 0; }
/* Responsive collapse driven by the shell's OWN width (`shell-narrow`/`shell-mini`
set in App.jsx from app-container.clientWidth = 100vw/--ui-scale), NOT a
viewport @media query. A `@media (max-width)` fires on raw 100vw and so
collapses at the wrong threshold whenever --ui-scale ≠ 1 (the layout box is
100vw/scale), which crammed the content into a left sliver with a black band.
These class rules respond to the actual scaled layout width on every engine. */
/* ≤1100 effective: auto-collapse the projects/history panel, keep nav + main */
.app-container.shell-narrow,
.app-container.shell-mini { grid-template-columns: 48px minmax(0, 1fr) !important; }
.app-container.shell-narrow > .history-panel,
.app-container.shell-mini > .history-panel { display: none !important; }
.app-container.shell-narrow > .main-content,
.app-container.shell-mini > .main-content { grid-column: 2 !important; }
.app-container.shell-narrow.rail-right,
.app-container.shell-mini.rail-right { grid-template-columns: minmax(0, 1fr) 48px !important; }
.app-container.shell-narrow.rail-right > .main-content,
.app-container.shell-mini.rail-right > .main-content { grid-column: 1 !important; }
/* ≤600 effective: hide the nav rail too, full-width main */
.app-container.shell-mini { grid-template-columns: minmax(0, 1fr) !important; }
.app-container.shell-mini > .nav-rail { display: none; }
.app-container.shell-mini > .main-content { grid-column: 1 !important; }
/* ═══ PANELS — flat chrome ═══ */
.glass-panel {
background: var(--chrome-bg);
backdrop-filter: none;
-webkit-backdrop-filter: none;
border: 1px solid var(--chrome-border);
border-radius: 0;
padding: 12px;
position: relative;
overflow: hidden;
margin-bottom: 0;
}
.glass-panel::before {
/* Decorative top-highlight hairline removed as part of the app-wide
border/divider removal. */
display: none;
}
/* ═══ HEADER — warm + cute ═══ */
.header-area {
display: grid;
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
align-items: center;
gap: 12px;
margin-bottom: 0;
flex-shrink: 0;
/* Matches the LogsFooter's chrome: flat bg, hairline bottom border,
no radial glow, no blur, no decorative SVG ribbon. The pulsing
accent dot + italic logo still carry the brand. */
padding: 3px 8px 3px 16px;
background: var(--chrome-bg);
border-bottom: 1px solid var(--chrome-border);
user-select: none;
position: relative;
z-index: 100;
grid-column: 1 / -1;
grid-row: 1;
cursor: default;
}
/* The macOS platform config enables native overlay traffic lights. Complete
the header's 16px padding to the same 64px safe area as titlebar tabs.
Header applies this only to the native Mac window, never browser content. */
.header-area__left--mac-inset {
padding-left: 48px;
}
@keyframes flush-slide {
from { opacity: 0; transform: translateY(-4px); }
to { opacity: 1; transform: translateY(0); }
}
/* Header status dot (Header.jsx, via the `[animation:hqPulse_…]` Tailwind
arbitrary utility) — purely decorative, so it opts out under reduced
motion via `motion-reduce:[animation:none]` in the JSX (#1857), the same
mechanism LogsFooter.jsx uses for heart-glow/donate-pop-in above. */
@keyframes hqPulse {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.15); opacity: 0.7; }
}
/* Responsive header — hide low-priority elements as width tightens.
Priority (least → most important):
reload → wave → kicker+chevron → project name → stats pill → S/M/L picker
→ dot → logo wordmark. Label + icon always visible.
*/
/* ≤1500: drop dev reload (kicker + first chevron now handled by utilities on
the elements themselves in Header.jsx) */
@media (max-width: 1500px) {
.header-area button[title="Force Reload UI"] { display: none; }
}
/* ≤720: hide logo wordmark, keep icon only */
@media (max-width: 720px) {
.header-area > div:nth-child(2) span { display: none; }
}
/* ≤600: minimal header — just breadcrumb label */
@media (max-width: 600px) {
.header-area { padding-left: 12px; padding-right: 4px; }
.header-area > div:nth-child(2) { display: none; }
}
/* Prevent clusters overlapping: clip content inside grid cells */
.header-area > div { min-width: 0; overflow: hidden; }
.header-area > div:nth-child(2),
.header-area > div:nth-child(3) { overflow: visible; }
@keyframes hqBounce {
0%, 100% { transform: scaleY(0.6); opacity: 0.75; }
50% { transform: scaleY(1.25); opacity: 1; }
}
.header-area h1 {
font-size: 1.05rem; font-weight: 700; line-height: 1.15;
background: linear-gradient(135deg, #fff 0%, #a1a1aa 100%);
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
letter-spacing: -0.01em;
}
.header-area p { margin: 0; color: var(--text-secondary); font-size: 0.7rem; }
/* ═══ TITLEBAR TABS ═══
The opt-in navigation skin (Settings → Appearance → Navigation style):
the workspace switcher leaves the icon rail and becomes browser-style tabs
in the title bar. `navStyle: 'tabs'` in the ui store drives both the
`.nav-tabs` shell class and `.header-area--tabs` (Header.jsx / TitleTabs.jsx).
The shape is the point. A tab is the content plane pulled up into the bar:
same background, small top radius, and two `box-shadow` corner curls that
sweep it back down into the page — so the active workspace reads as one
continuous surface with what you're looking at, and the others read as
sitting behind it. Radii stay tight (6px against the app's 3px pills) so
the browser silhouette doesn't turn soft next to the rest of the chrome. */
/* Grid: no 48px rail column to reserve. */
.app-container.nav-tabs { grid-template-columns: minmax(180px, 220px) minmax(0, 4fr); }
.app-container.nav-tabs.sidebar-collapsed { grid-template-columns: 46px minmax(0, 4fr); }
.app-container.nav-tabs.sidebar-hidden { grid-template-columns: minmax(0, 1fr); }
.app-container.nav-tabs > .history-panel { grid-column: 1; }
.app-container.nav-tabs > .main-content { grid-column: 2; }
.app-container.nav-tabs.sidebar-hidden > .main-content { grid-column: 1; }
/* Narrow shells drop the projects/history panel; with no rail, main is alone. */
.app-container.nav-tabs.shell-narrow,
.app-container.nav-tabs.shell-mini { grid-template-columns: minmax(0, 1fr) !important; }
.app-container.nav-tabs.shell-narrow > .main-content,
.app-container.nav-tabs.shell-mini > .main-content { grid-column: 1 !important; }
.header-area.header-area--tabs {
grid-template-columns: minmax(0, 1fr) auto;
align-items: end;
gap: 8px;
/* Flush bottom: the tabs themselves have to touch the content plane.
The 64px left inset keeps them clear of the macOS traffic lights. */
padding: 0 8px 0 64px;
/* The shelf lifts AWAY from the page rather than sinking below it: every
theme's --chrome-bg is already near-black, so a darker shelf would have
no contrast left to spend. Lighter shelf + page-coloured active tab is
also exactly the browser's own dark-mode arrangement. */
background: color-mix(in srgb, var(--chrome-bg) 90%, #fff);
/* No hairline: the active tab has to flow into the page uninterrupted.
The shelf's own darker fill draws the line for the rest of the bar. */
border-bottom: none;
min-height: 34px;
}
/* The corner curls hang outside each tab — the default cell clipping eats them. */
.header-area.header-area--tabs > div { overflow: visible; }
.tabstrip {
display: flex;
align-items: flex-end;
min-width: 0;
height: 100%;
/* Room for the first tab's left curl. */
padding-left: 6px;
}
.tabstrip__gap { flex: 1 1 auto; min-width: 8px; }
.tabstrip__tab {
position: relative;
display: inline-flex;
align-items: center;
gap: 6px;
flex: 0 1 auto;
min-width: 0;
max-width: 190px;
height: 27px;
padding: 0 10px;
border: 0;
border-radius: 6px 6px 0 0;
background: transparent;
color: var(--chrome-fg-dim);
font-family: var(--font-sans);
font-size: 11.5px;
font-weight: 500;
letter-spacing: 0.01em;
white-space: nowrap;
cursor: pointer;
transition: background 0.14s ease, color 0.14s ease;
}
/* Resting tabs read as muted, not as disabled: the shelf is a LIGHTER plane
than the page, so --chrome-fg-dim (tuned against the dark page) loses too
much contrast here. */
.tabstrip__tab { color: var(--chrome-fg-muted); }
.tabstrip__tab:hover { background: color-mix(in srgb, var(--chrome-bg) 82%, #fff); color: var(--chrome-fg); }
.tabstrip__tab:focus-visible {
outline: 2px solid var(--tab-accent, var(--chrome-accent));
outline-offset: -2px;
}
.tabstrip__icon { flex-shrink: 0; }
/* Resting tabs drop their label whole (is-compact) rather than shrinking into
stubs, so this ellipsis is a safety net, not the plan: it only shows during
the frame between a window drag and the next measurement — and a stub for
one frame beats tabs overlapping the status readout for one frame. */
.tabstrip__label { overflow: hidden; text-overflow: ellipsis; }
.tabstrip__tab.is-active {
background: var(--chrome-bg);
color: var(--chrome-fg);
/* The tab you're in never gives up its name to make room for the others. */
flex-shrink: 0;
/* Accent rule along the top edge — the same "this one is live" signal the
rail spends on its indicator bar. */
box-shadow: inset 0 2px 0 var(--tab-accent, var(--chrome-accent));
z-index: 1;
}
.tabstrip__tab.is-active .tabstrip__icon { color: var(--tab-accent, var(--chrome-accent)); }
/* The curls: a 6px corner nub either side of the active tab, filled with the
tab's own colour by an offset+spread shadow and notched by the radius. */
.tabstrip__tab.is-active::before,
.tabstrip__tab.is-active::after {
content: '';
position: absolute;
bottom: 0;
width: 6px;
height: 6px;
pointer-events: none;
}
.tabstrip__tab.is-active::before {
left: -6px;
border-bottom-right-radius: 6px;
box-shadow: 3px 3px 0 3px var(--chrome-bg);
}
.tabstrip__tab.is-active::after {
right: -6px;
border-bottom-left-radius: 6px;
box-shadow: -3px 3px 0 3px var(--chrome-bg);
}
/* Hairline between neighbouring resting tabs (TitleTabs decides where one
belongs); the whole set fades out while the strip is under the pointer. */
.tabstrip__tab.has-separator::after {
content: '';
position: absolute;
right: 0;
top: 8px;
bottom: 8px;
width: 1px;
background: var(--chrome-border-strong);
transition: opacity 0.14s ease;
}
.tabstrip:hover .tabstrip__tab.has-separator::after { opacity: 0; }
/* Labels are all-or-nothing (TitleTabs measures; see useCompactLabels).
`is-compact` — the full set doesn't fit: resting tabs go to icons, and only
the tab you're in keeps its word. Nine ellipsised stubs read worse than
nine icons, and cost more room doing it. */
.tabstrip.is-compact .tabstrip__tab:not(.is-active) .tabstrip__label { display: none; }
.tabstrip.is-compact .tabstrip__tab:not(.is-active) { padding: 0 9px; }
/* `is-measuring` — applied for one synchronous read: force the full layout
and stop the tabs shrinking, so scrollWidth reports what the strip WANTS
rather than what it was last squeezed into (a measurement that depended on
the current state would oscillate). Never painted. */
/* !important, deliberately: this has to out-rank `.is-compact`'s label rule
below. Without it the read inherits the state it is trying to re-decide —
once compact, the strip measures its own compact width, agrees with itself
and can never expand again (widen the window, turn the metrics cluster off:
the labels would never come back). */
.tabstrip.is-measuring .tabstrip__label { display: inline !important; }
.tabstrip.is-measuring .tabstrip__tab {
flex-shrink: 0;
max-width: none;
padding: 0 10px;
}
/* The spacer that pushes Settings to the tail grows into whatever is left,
which would make the strip report "full" at every width. Collapse it for
the read: what's being measured is the TABS, not the slack. */
.tabstrip.is-measuring .tabstrip__gap { display: none; }
/* ═══ TABS — migrated to ui/Tabs primitive; legacy .tabs/.tab removed. ═══ */
/* ═══ GRID SYSTEMS ═══ */
/* .grid-2 / .grid-4 → Tailwind utilities (grid + grid-cols + responsive
max-[…] variants) inline in AudioMethodPanel.jsx / clone/ActionBar.jsx (P4). */
/* Shared section-label style across workspace pages (Clone / Design /
Dub) — rhymes with the status bar's "LOGS" label and the Sidebar
section titles. Monospace + uppercase + tracked + muted so labels
feel like chrome, leaving content rows to carry the colour. */
.label-row {
display: flex; align-items: center; gap: 6px; margin-bottom: 4px;
color: var(--chrome-fg-muted);
font-family: var(--chrome-font-mono);
font-size: var(--chrome-label-size);
font-weight: 600;
letter-spacing: var(--chrome-label-track);
text-transform: uppercase;
}
.label-icon { color: var(--chrome-fg-muted); }
/* ═══ INPUTS ═══ */
.input-base {
width: 100%;
/* Borders are removed app-wide; a recessed surface fill keeps text
fields / selects / textareas perceivable as inputs. */
background: var(--color-bg-elev-2);
border: 1px solid transparent;
border-radius: var(--chrome-radius-pill);
padding: 5px 8px;
color: var(--chrome-fg);
font-family: var(--font-sans);
font-size: 0.78rem;
transition: background var(--dur-fast), border-color var(--dur-fast);
box-sizing: border-box;
}
.input-base:focus {
outline: none;
border-color: transparent;
box-shadow: none;
/* Subtle elevation shift on focus (keyboard focus also gets the global
:focus-visible ring). */
background: var(--color-bg-elev-1);
}
.input-base::placeholder { color: var(--chrome-fg-dim); }
textarea.input-base { min-height: 60px; resize: vertical; line-height: 1.5; }
/* Keep the script prominent without pushing the voice setup below the fold. */
.studio-script-input { min-height: 168px !important; }
.shell-narrow .studio-script-input { min-height: 144px !important; }
.shell-mini .studio-script-input { min-height: 120px !important; }
select.input-base {
appearance: none;
/* Caret color tracks the theme via --select-caret (see :root / [data-theme]). */
background-image: var(--select-caret);
background-repeat: no-repeat;
background-position: right 7px top 50%;
background-size: 0.5rem auto;
padding-right: 20px;
cursor: pointer;
}
select.input-base:hover {
border-color: transparent;
}
/* Native option popup: paint from theme tokens so engines that honor <option>
styling (Chromium on Windows/Linux) match the app; on macOS WebKit the popup
is UA-drawn and instead follows `color-scheme` (set per theme above). */
select.input-base option,
select.input-base optgroup {
background-color: var(--color-bg-elev-1);
color: var(--chrome-fg);
}
/* ═══ CHECKBOX STYLING ═══ */
/* Checked fill uses the themed brand token so checkboxes recolor with every
[data-theme] and match the primary/active affordances (sliders, segmented,
toggles) — not the non-themed chrome-accent. */
input[type="checkbox"] {
accent-color: var(--color-brand);
width: 13px; height: 13px;
border-radius: var(--radius-sm);
}
/* ═══ RANGE INPUTS ═══ */
input[type="range"] {
-webkit-appearance: none; width: 100%; height: 3px;
background: var(--color-bg-elev-2); border-radius: var(--radius-xs); outline: none; margin-top: 3px;
cursor: pointer;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none; width: 10px; height: 10px; border-radius: 50%;
background: var(--color-fg); cursor: pointer;
box-shadow: var(--shadow-sm);
transition: transform var(--dur-fast);
}
input[type="range"]::-webkit-slider-thumb:hover {
transform: scale(1.3);
}
input[type="range"]::-webkit-slider-thumb:active {
transform: scale(1.1);
background: var(--color-brand);
}
/* .val-bubble → Tailwind utilities inline in clone/ActionBar.jsx (P4). */
/* ═══ TAGS ═══ */
.tags-container { display: flex; flex-wrap: wrap; gap: 3px; margin: 4px 0; }
/* .tag-btn → Tailwind utilities inline in clone/ScriptPanel.jsx (P4). */
/* .file-drag (+ :hover/.is-dragging/p) → Tailwind utilities inline in
clone/AudioMethodPanel.jsx (P4). is-dragging stays a JS-toggled marker. */
/* ═══ PROGRESS — migrated to ui/Progress primitive. ═══ */
/* ═══ BUTTON ═══ */
/* Flat chrome primary — accent = text + border, no gradient / shimmer.
Matches the dub footer button family so the whole app reads as one
chrome typography row. */
.btn-primary {
width: 100%;
background: var(--chrome-accent-bg);
color: var(--chrome-accent);
border: 1px solid var(--chrome-accent-border);
padding: 6px 12px;
border-radius: var(--chrome-radius-pill);
font-size: 0.78rem; font-weight: 500; cursor: pointer;
display: flex; justify-content: center; align-items: center; gap: 6px;
margin-top: 6px;
transition: background var(--dur-fast), border-color var(--dur-fast), color var(--dur-fast);
font-family: var(--font-sans);
letter-spacing: 0.02em;
position: relative;
overflow: hidden;
box-shadow: none;
}
.btn-primary::before { display: none; }
.btn-primary:hover:not(:disabled) {
filter: none;
background: color-mix(in srgb, var(--chrome-accent) 20%, transparent);
box-shadow: none;
transform: none;
}
.btn-primary:active:not(:disabled) {
transform: none;
filter: none;
box-shadow: none;
}
.btn-primary:disabled { opacity: 0.4; cursor: not-allowed; filter: none; }
.spinner { animation: spin 1s linear infinite; }
/* ═══ SIDEBAR / HISTORY ═══ */
.history-panel {
height: 100%;
max-height: none;
overflow-y: auto;
box-sizing: border-box;
border-right: 1px solid transparent;
}
.history-item {
background: rgba(0,0,0,0.18); border: 1px solid transparent;
border-radius: 5px; padding: 6px 8px; margin-bottom: 4px;
transition: all var(--transition-smooth);
animation: fadeIn 0.2s ease-out;
}
.history-item:hover {
background: rgba(0,0,0,0.28);
border-color: transparent;
}
/* .history-header / .history-badge / .history-time / .history-text migrated to Badge + Sidebar.css. */
.project-active {
/* Selection cue via background tint (not a border) so the active project
stays perceivable after the app-wide border removal. */
border-color: transparent !important;
background: rgba(184,187,38,0.14) !important;
box-shadow: none;
}
/* ═══ AUDIO PLAYER ═══ */
audio {
height: 28px; width: 100%; outline: none; border-radius: 4px;
}
audio::-webkit-media-controls-enclosure {
background-color: rgba(40,38,37,0.95);
border: 1px solid transparent;
border-radius: 4px;
}
audio::-webkit-media-controls-play-button,
audio::-webkit-media-controls-mute-button {
background-color: rgba(255,255,255,0.06); border-radius: 50%; color: var(--chrome-accent);
}
audio::-webkit-media-controls-current-time-display,
audio::-webkit-media-controls-time-remaining-display { color: var(--chrome-fg); font-size: 0.65rem; }
/* ═══ OVERRIDES / MODALS ═══ */
.override-toggle {
display: flex; align-items: center; justify-content: space-between;
background: rgba(0,0,0,0.18); padding: 4px 8px; border-radius: 4px;
font-size: 0.72rem; color: var(--text-primary); cursor: pointer;
border: 1px solid transparent; margin-top: 6px;
transition: all var(--transition-smooth);
}
.override-toggle:hover { background: rgba(0,0,0,0.3); border-color: transparent; }
.override-content {
background: rgba(0,0,0,0.1); border: 1px solid transparent; border-top: none;
padding: 6px 8px; border-radius: 0 0 4px 4px; margin-bottom: 6px;
}
.preset-grid {
display: grid; grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); gap: 4px; margin-bottom: 6px;
}
/* .preset-btn removed (P4) — the class had zero remaining usages in the app
(the shadcn Button `preset` variant covers preset buttons now). */
/* ═══ SEGMENT TABLE ═══ */
.segment-table {
/* No max-height cap — the list is virtualised via react-window and measures
its own body container. Capping at 320 px left a huge gap below. */
border: 1px solid transparent; border-radius: 4px; margin-top: 4px;
}
.segment-header {
display: flex; align-items: center; gap: 4px;
padding: 3px 6px; background: rgba(0,0,0,0.35);
font-size: 0.62rem; font-weight: 600; color: var(--text-secondary);
position: sticky; top: 0; z-index: 1;
border-bottom: 1px solid transparent;
letter-spacing: 0.03em;
text-transform: uppercase;
}
.segment-row {
display: flex; align-items: center; gap: 3px;
padding: 2px 4px;
box-sizing: border-box;
border-bottom: 1px solid transparent;
transition: background var(--transition-fast);
}
.segment-row:hover { background: var(--chrome-hover-bg); }
.segment-row.segment-active {
background: var(--chrome-accent-bg);
border-left: 2px solid var(--chrome-accent);
box-shadow: none;
animation: none;
z-index: 10;
position: relative;
}
.segment-row.segment-done { opacity: 0.5; }
.segment-row.segment-selected {
background: var(--chrome-accent-bg);
box-shadow: inset 2px 0 0 var(--chrome-accent);
}
.segment-time {
/* Timing controls and status badges wrap within the segment's content rail. */
font-size: 0.62rem; font-family: var(--chrome-font-mono); color: var(--chrome-fg-muted);
font-variant-numeric: tabular-nums;
}
.segment-input {
flex: 1;
padding: 2px 4px !important;
font-size: 0.7rem !important;
border-radius: 3px !important;
min-height: auto !important;
}
.segment-del {
width: 20px; height: 20px; flex-shrink: 0;
background: none; border: 1px solid rgba(251,73,52,0.12);
color: #fb4934; border-radius: 3px; cursor: pointer;
display: flex; align-items: center; justify-content: center;
transition: all var(--transition-smooth);
opacity: 0.6;
}
.segment-del:hover { background: rgba(251,73,52,0.12); opacity: 1; }
.segment-del:disabled { opacity: 0.2; cursor: not-allowed; }
.profile-active {
background: rgba(142,192,124,0.12) !important;
border-color: #8ec07c !important;
color: #8ec07c !important;
}
.segment-play {
width: 20px; height: 20px; flex-shrink: 0;
background: transparent; border: 1px solid var(--chrome-border);
color: var(--chrome-fg-muted); border-radius: var(--chrome-radius-pill); cursor: pointer;
display: flex; align-items: center; justify-content: center;
transition: background var(--dur-fast), color var(--dur-fast), border-color var(--dur-fast);
}
.segment-play:hover { background: var(--chrome-hover-bg); color: var(--chrome-fg); border-color: var(--chrome-border-strong); }
/* ═══ WAVEFORM TIMELINE ═══ */
/* .waveform-container is kept as a styling hook for the WaveSurfer-generated
region DOM ([data-id^="wavesurfer-region"]) below — those elements aren't
rendered in JSX, so they can't carry Tailwind utilities. The rest of the
waveform-* family migrated to utilities on WaveformTimeline.jsx (P4). */
.waveform-container {
position: relative;
}
.waveform-container [data-id^="wavesurfer-region"] {
border-radius: 3px !important;
}
/* ═══ MULTI-TRACK (WaveSurfer region DOM) ═══ */
.waveform-container [data-id^="wavesurfer-region"] {
top: 52px !important;
height: 26px !important;
border-radius: 3px !important;
border: 1px solid transparent !important;
display: flex !important; align-items: center !important;
padding: 0 3px !important; color: white !important;
font-size: 0.58rem !important;
text-shadow: 0 1px 2px rgba(0,0,0,0.7);
overflow: hidden; white-space: nowrap;
}
/* ═══ SCROLLBAR ═══ */
::-webkit-scrollbar { width: 5px; height: 5px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb {
background: rgba(255,255,255,0.08); border-radius: 3px;
border: 1px solid transparent;
}
::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.16); }
::-webkit-scrollbar-corner { background: transparent; }
/* Firefox scrollbar */
* {
scrollbar-width: thin;
scrollbar-color: rgba(255,255,255,0.08) transparent;
}
/* ═══ TOOLTIP HINT ═══ */
[title] { position: relative; }
/* ═══ FOCUS VISIBLE (keyboard nav) ═══ */
:focus-visible {
outline: 2px solid color-mix(in srgb, var(--chrome-accent, #d3869b) 65%, transparent);
outline-offset: 2px;
box-shadow: 0 0 0 4px color-mix(in srgb, var(--chrome-accent, #d3869b) 15%, transparent);
}
button:focus:not(:focus-visible),
a:focus:not(:focus-visible),
input:focus:not(:focus-visible),
select:focus:not(:focus-visible) { outline: none; box-shadow: none; }
/* Selects take the themed accent ring (--color-ring → --color-brand), matching
the buttons/checkboxes tokenized last phase, instead of the non-theme
-tracking --chrome-accent the base :focus-visible rule uses. */
select:focus-visible,
select.input-base:focus-visible,
.ui-select:focus-visible {
outline: 2px solid color-mix(in srgb, var(--color-ring) 70%, transparent);
outline-offset: 2px;
box-shadow: 0 0 0 4px color-mix(in srgb, var(--color-ring) 16%, transparent);
}
/* 10x P4 (spec §3 a11y gate): one consistent, fully-opaque ring on the
studio's 10x controls — shared rule, not per-component restyles. */
.personality-chip:focus-visible,
.chip:focus-visible,
.wh__chip:focus-visible,
.wf-player__btn:focus-visible {
outline: 2px solid var(--chrome-accent);
outline-offset: 1px;
}
/* ═══ LAUNCHPAD — chrome frame + restrained motion ═══ */
.launchpad {
flex: 1; display: flex; flex-direction: column; overflow-y: auto;
background: var(--chrome-bg);
position: relative;
isolation: isolate;
}
/* Paper grain removed — chrome surfaces read flat, not textured. */
.launchpad::before { display: none; }
/* ── Aurora backdrop ───────────────────────────────────────
Three soft blobs in the three card accent colors, drifting on
independent long cycles. Behind everything at z:0. Blurred heavily
so the motion reads as ambient light, not moving shapes. */
.lp-aurora {
position: absolute;
inset: 0;
overflow: hidden;
pointer-events: none;
z-index: 0;
opacity: 0.55;
}
.lp-aurora__blob {
position: absolute;
border-radius: 50%;
filter: blur(90px);
will-change: transform;
}
.lp-aurora__blob--pink {
width: 420px; height: 420px;
background: radial-gradient(circle, rgba(211,134,155,0.55), transparent 60%);
top: -80px; left: -40px;
animation: lpDrift1 22s ease-in-out infinite;
}
.lp-aurora__blob--green {
width: 360px; height: 360px;
background: radial-gradient(circle, rgba(142,192,124,0.40), transparent 60%);
top: 40%; left: 55%;
animation: lpDrift2 28s ease-in-out infinite;
}
.lp-aurora__blob--amber {
width: 480px; height: 480px;
background: radial-gradient(circle, rgba(254,128,25,0.32), transparent 60%);
bottom: -120px; right: -80px;
animation: lpDrift3 32s ease-in-out infinite;
}
@keyframes lpDrift1 {
0%, 100% { transform: translate(0, 0) scale(1); }
33% { transform: translate(120px, 60px) scale(1.15); }
66% { transform: translate(-40px, 100px) scale(0.9); }
}
@keyframes lpDrift2 {
0%, 100% { transform: translate(0, 0) scale(1); }
50% { transform: translate(-180px, -90px) scale(1.1); }
}
@keyframes lpDrift3 {
0%, 100% { transform: translate(0, 0) scale(1); }
40% { transform: translate(-100px, -60px) scale(1.05); }
75% { transform: translate(60px, -30px) scale(0.95); }
}
/* Hero H1 halo + sweep — eternal glow behind the (now utility-styled) <h1>
and an animated accent line underneath. Kept here because they are pure
::pseudo/@keyframes decoration on absolutely-positioned spans, and the
sweep is reused cross-page by ContactPage/SupportPage/DonatePage. */
.lp-hero__halo {
position: absolute;
inset: -16px -24px;
background: radial-gradient(
ellipse at 50% 60%,
color-mix(in srgb, var(--chrome-accent) 20%, transparent) 0%,
transparent 65%
);
filter: blur(16px);
z-index: -1;
pointer-events: none;
animation: lpHeroHalo 6s ease-in-out infinite;
}
@keyframes lpHeroHalo {
0%, 100% { opacity: 0.55; transform: scale(1); }
50% { opacity: 0.95; transform: scale(1.06); }
}
.lp-hero__sweep {
position: absolute;
left: 0; right: 0;
bottom: -6px;
height: 2px;
background: linear-gradient(90deg,
transparent 0%,
color-mix(in srgb, var(--chrome-accent) 70%, transparent) 30%,
var(--chrome-accent) 50%,
color-mix(in srgb, var(--chrome-accent) 70%, transparent) 70%,
transparent 100%
);
transform-origin: center;
animation: lpHeroSweep 5.4s ease-in-out infinite;
pointer-events: none;
border-radius: 2px;
}
@keyframes lpHeroSweep {
0%, 100% { transform: scaleX(0.35); opacity: 0.35; }
50% { transform: scaleX(1); opacity: 0.85; }
}
/* Action cards — quiet, borderless tiles.
The app-wide token zeroing at the bottom of this file makes every
`--chrome-border` transparent, so a card's structure can't come from a
frame; it comes from a whisper-faint surface plus internal rhythm. Colour
is spent sparingly: `--card-hue` (inline style) tints the glyph at rest and
only takes over the surface, the count and the arrow once the card is
raised. `isolation: isolate` keeps the raised card's shadow in its own
stacking context. (The `.lp-cards` grid is utility-styled on the component;
the card keeps a class here for its hover/focus choreography.) */
.lp-action-card {
background: color-mix(in srgb, var(--chrome-fg) 3.5%, transparent);
border: 0;
border-radius: var(--chrome-radius-pill);
padding: 15px 16px 16px;
cursor: pointer;
display: flex; flex-direction: column;
transition:
background var(--dur-base) var(--ease-out),
transform var(--dur-base) var(--ease-out),
box-shadow var(--dur-base) var(--ease-out);
position: relative; overflow: hidden;
box-shadow: none;
isolation: isolate;
/* Native <button> reset */
font: inherit; color: inherit; text-align: left; width: 100%;
}
/* Hover OR keyboard focus raises the card forward — the `--raised` class is
set from React state so pointer and keyboard share one path (`:hover` is
kept as a no-JS fallback). A 2px lift and a soft neutral shadow: enough to
read as "this one", not a bounce. */
.lp-action-card:hover,
.lp-action-card--raised {
transform: translateY(-2px);
background: color-mix(in srgb, var(--card-hue, var(--chrome-accent)) 7%, transparent);
box-shadow: 0 14px 28px -22px rgba(0, 0, 0, 0.9);
z-index: 2;
}
.lp-action-card:active { transform: translateY(0); }
.lp-action-card:focus-visible {
outline: 2px solid color-mix(in srgb, var(--card-hue, var(--chrome-accent)) 70%, transparent);
outline-offset: 2px;
}
/* Head row — bare glyph left, count right, on one optical line. No chip, no
ring around the icon: the glyph itself is the accent. */
.lp-action-card .card-head {
display: flex; align-items: center; justify-content: space-between;
gap: 8px; margin-bottom: 14px;
}
.lp-action-card .card-icon {
display: inline-flex; align-items: center; justify-content: center;
color: var(--card-hue, var(--chrome-accent));
opacity: 0.78;
transition: opacity var(--dur-base) var(--ease-out);
}
.lp-action-card:hover .card-icon,
.lp-action-card--raised .card-icon { opacity: 1; }
/* Respect reduced motion. */
@media (prefers-reduced-motion: reduce) {
.lp-aurora__blob,
.lp-hero__halo,
.lp-hero__sweep,
.lp-wave-bar,
.lp-animate {
animation: none !important;
}
.lp-action-card:hover,
.lp-action-card--raised { transform: none; }
}
/* Title row — name left, open affordance parked at the right margin. */
.lp-action-card .card-title-row {
display: flex; align-items: center; justify-content: space-between;
gap: 8px; margin-bottom: 5px;
}
.lp-action-card h3 {
font-family: var(--font-sans);
font-size: 0.8rem; font-weight: 500; margin: 0;
color: var(--chrome-fg);
letter-spacing: -0.005em;
line-height: 1.3;
min-width: 0;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* The arrow is pure decoration (aria-hidden in the JSX) — it drifts into
place on raise rather than sitting there as permanent furniture. */
.lp-action-card .card-go {
display: inline-flex; flex-shrink: 0;
color: var(--chrome-fg-dim);
opacity: 0;
transform: translate(-3px, 3px);
transition:
opacity var(--dur-base) var(--ease-out),
transform var(--dur-base) var(--ease-out),
color var(--dur-base) var(--ease-out);
}
.lp-action-card:hover .card-go,
.lp-action-card--raised .card-go {
opacity: 1;
transform: translate(0, 0);
color: var(--card-hue, var(--chrome-accent));
}
.lp-action-card .card-desc {
font-family: var(--font-sans);
font-size: 0.71rem; color: var(--chrome-fg-muted); margin: 0; line-height: 1.55;
font-weight: 400;
min-height: 3.1em;
/* Clamp to two lines so every card in a grid row is the same height. */
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
}
/* Count is a plain tabular numeral, not a badge — no pill, no ring, no
negative offset hanging off the corner. */
.lp-action-card .card-count {
font-family: var(--chrome-font-mono);
font-size: var(--chrome-label-size); font-weight: 500;
letter-spacing: var(--chrome-label-track);
font-variant-numeric: tabular-nums;
color: var(--chrome-fg-dim);
transition: color var(--dur-base) var(--ease-out);
}
.lp-action-card:hover .card-count,
.lp-action-card--raised .card-count { color: var(--card-hue, var(--chrome-accent)); }
/* ── Personality picker strip ────────────────────────────── */
.personality-strip {
display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 10px;
}
/* .personality-chip / .personality-chip__icon → Tailwind utilities inline in
clone/DesignMethodPanel.jsx (P4). */
.chip-auto { display: inline-flex; align-items: center; gap: 3px; }
.personality-label {
font-family: var(--chrome-font-mono);
font-size: var(--chrome-label-size);
font-weight: 600; text-transform: uppercase;
letter-spacing: var(--chrome-label-track);
color: var(--chrome-fg-muted);
margin-bottom: 6px;
}
/* Staggered fade-in */
@keyframes lpFadeUp {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0); }
}
/* `backwards`, deliberately NOT `both`: a forwards-filling animation keeps
owning `transform` after it ends, and animation-origin declarations outrank
normal ones — which would silently kill `.lp-action-card`'s hover lift.
Backwards fill still holds the from-state through the stagger delay. */
.lp-animate {
animation: lpFadeUp 0.4s cubic-bezier(0.4, 0, 0.2, 1) backwards;
/* Stagger the deal across all cards via each card's own --lp-i (set inline
in LaunchpadDeck) — degrades to 0s where --lp-i is absent. */
animation-delay: calc(var(--lp-i, 0) * 0.05s);
}
/* Wave bars — each one reads its own --bar-h / --bar-dur / --bar-delay so
the group breathes organically instead of marching in lockstep. Colour
shifts between chrome fg + accent through the cycle so the visualiser
feels like it's tuning into a real signal. */
@keyframes lpWaveBeat {
0%, 100% {
transform: scaleY(0.45);
background: var(--chrome-fg-muted);
box-shadow: none;
}
35% {
transform: scaleY(1.15);
background: var(--chrome-accent);
box-shadow: 0 0 6px color-mix(in srgb, var(--chrome-accent) 55%, transparent);
}
65% {
transform: scaleY(0.8);
background: color-mix(in srgb, var(--chrome-accent) 60%, var(--chrome-fg-muted));
box-shadow: none;
}
}
.lp-wave-bar {
display: inline-block;
width: 3px;
height: var(--bar-h, 12px);
border-radius: 2px;
background: var(--chrome-fg-muted);
transform-origin: center;
animation: lpWaveBeat var(--bar-dur, 2s) ease-in-out infinite;
animation-delay: var(--bar-delay, 0s);
}
/* ── Launchpad feature cards — full-width responsive grid ──────────
PR #904 fanned these seven cards into a fixed ~780px deck, which left dead
margins in a maximized window. This fills the content width instead:
`repeat(auto-fit, minmax(--lp-card-min, 1fr))` lets the browser derive the
column count from the GRID'S OWN width — which, under the shell's
`zoom: --ui-scale` model, equals the shell's own width (the same
container-driven mechanism as the recent-files grid, never a viewport @media
that would fire at the wrong width when --ui-scale ≠ 1). Columns reflow
7→…→1 from a maximized ~2560px display down to the 900×600 minimum, every
column stretching (1fr) so there are no dead margins and no horizontal
scroll. `--lp-card-min` is set inline from useShellNarrow (a wider floor on
narrow shells → fewer, comfier columns). The cards are the .lp-action-card
tiles above. */
.lp-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(var(--lp-card-min, 200px), 1fr));
gap: 10px;
width: 100%;
}
/* ── Searchable combobox ─────────────────────────────────── */
/* .ss-* (wrap/trigger/label/chev/sm/md/pop/search/list/group-label/option/
hl/sel/kind-icon/check/empty/more) → Tailwind utilities inline in
components/SearchableSelect.jsx (P4). The `ss-wrap` class name is retained
there (no longer styled here) only as a marker for residual.css's
`.voice-selector > .ss-wrap` cross-file child combinator. */
/* ═══ PERSONALITY PASS — shared controls ═══ */
/* Personality overrides for .btn-primary removed — the base rule above is
now flat chrome, and the overrides added gradient / bounce back. */
.btn-primary {
font-family: var(--font-sans);
font-weight: 500;
border-radius: var(--chrome-radius-pill) !important;
transition: background var(--dur-fast), border-color var(--dur-fast), color var(--dur-fast);
}
.btn-primary:hover:not(:disabled) { transform: none; }
.btn-primary:active:not(:disabled) { transform: none; }
/* Soft focus ring on inputs */
.input-base:focus {
border-color: var(--chrome-border-strong);
box-shadow: 0 0 0 3px rgba(243, 165, 182, 0.12), inset 0 1px 2px rgba(0,0,0,0.25);
}
/* Preset chips — flat chrome pills. No rotation, no gradient, no squircle.
The `.preset-btn` rules were removed in P4 (zero usages); only the grid
layout remains. */
.preset-grid {
display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 6px;
}
/* Tag / special-token chips — flat chrome, mono, no rotation */
.tags-container {
display: flex; flex-wrap: wrap; gap: 4px;
}
/* .tag-btn → Tailwind utilities inline in clone/ScriptPanel.jsx (P4). */
/* Chip strips → Tailwind utilities inline in clone/DesignMethodPanel.jsx
(P4). The Gender/Age/Pitch/Style/Accent-or-Dialect chip radiogroups moved
to native <select>s in the #1771 voice-design redesign; the "Starting
points" personality row is the only chip strip left. Its buttons carry a
`data-chip-nav` attribute purely as a JS hook — CloneDesignTab's
roving-tabindex keyboard nav does `closest('[role="group"]')
.querySelectorAll('[data-chip-nav]')`; layout is flex utilities on the div. */
/* Textarea — flat chrome surface for design/clone prompt box */
textarea.input-base {
/* Recessed fill (not --chrome-bg, which equals the panel background and
would be invisible once the border is gone). */
background: var(--color-bg-elev-2);
font-family: var(--font-sans);
font-size: 0.82rem; line-height: 1.55;
color: var(--chrome-fg);
border: 1px solid transparent;
border-radius: var(--chrome-radius-pill);
padding: 12px 14px;
}
textarea.input-base:focus {
border-color: var(--chrome-border-strong);
}
textarea.input-base::placeholder {
color: var(--chrome-fg-dim);
font-style: normal;
}
/* ═══ GLOBAL POLISH — dub, modals, dropdowns, segments ═══ */
/* Segment table — chrome header, subtle chrome hover on rows */
.segment-table {
background: var(--chrome-bg);
border: 1px solid var(--chrome-border);
overflow: hidden;
}
.segment-header {
font-family: var(--chrome-font-mono) !important;
font-weight: 600 !important;
font-size: var(--chrome-label-size) !important;
text-transform: uppercase;
letter-spacing: var(--chrome-label-track);
color: var(--chrome-fg-muted) !important;
background: var(--chrome-bg) !important;
border-bottom: 1px solid var(--chrome-border) !important;
padding: 6px 8px !important;
}
.segment-row {
transition: background var(--dur-fast);
}
.segment-row:hover {
background: var(--chrome-hover-bg) !important;
}
.segment-row.segment-active {
background: var(--chrome-accent-bg) !important;
box-shadow: inset 2px 0 0 var(--chrome-accent);
}
.segment-row.segment-done {
background: color-mix(in srgb, var(--chrome-severity-ok) 5%, transparent) !important;
}
/* Segment inner inputs — chrome focus */
.segment-input {
font-family: var(--font-sans) !important;
background: transparent !important;
border: 1px solid transparent !important;
transition: border-color var(--dur-fast), background var(--dur-fast) !important;
}
.segment-input:focus {
border-color: var(--chrome-border-strong) !important;
background: var(--chrome-hover-bg) !important;
}
.segment-play, .segment-del {
border-radius: var(--chrome-radius-pill) !important;
transition: background var(--dur-fast), color var(--dur-fast) !important;
}
.segment-play:hover, .segment-del:hover {
transform: none;
}
/* Override toggle + content */
.override-content {
background: var(--chrome-bg);
border: 1px solid var(--chrome-border);
border-radius: var(--chrome-radius-pill);
padding: 12px 14px;
margin-top: 4px;
}
/* .ss-popover/.ss-menu/.ss-dropdown/.ss-item/.ss-highlighted removed (P4):
dead rules — SearchableSelect.jsx renders its own popup markup and never
emitted these class names (zero JSX usages). */
/* Transcript block */
.override-toggle + div {
background: var(--chrome-bg) !important;
border-color: var(--chrome-border) !important;
border-radius: 0 0 var(--chrome-radius-pill) var(--chrome-radius-pill) !important;
}
/* Loader / spinner color to peach */
.spinner { color: var(--chrome-accent) !important; }
/* Range inputs — chrome-accent thumb, no glow halo. */
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none; appearance: none;
width: 12px; height: 12px; border-radius: 50%;
background: var(--chrome-accent);
border: 2px solid var(--chrome-bg);
box-shadow: none;
cursor: pointer;
}
input[type="range"]::-moz-range-thumb {
width: 12px; height: 12px; border-radius: 50%;
background: var(--chrome-accent); border: 2px solid var(--chrome-bg); cursor: pointer;
}
/* Modal overlay — neutral backdrop */
[style*="backdropFilter"][style*="rgba(0,0,0,0.8"],
[style*="backdrop-filter"][style*="rgba(0,0,0,0.85"] {
background: rgba(0, 0, 0, 0.7) !important;
}
.glass-panel select.input-base {
font-family: var(--font-sans);
font-weight: 500;
}
/* AudioTrimmer modal interior glass panel — flat chrome */
.audio-trimmer,
div[role="dialog"].audio-trimmer {
background: var(--chrome-bg) !important;
border: 1px solid var(--chrome-border-strong) !important;
border-radius: var(--chrome-radius-pill) !important;
}
/* Labels — chrome mono */
.label-row {
font-family: var(--chrome-font-mono);
font-weight: 500;
color: var(--chrome-fg-muted);
}
.label-icon { color: var(--chrome-fg-muted) !important; }
/* Override toggle (Production Overrides) — flat chrome */
.override-toggle {
font-family: var(--font-sans);
font-weight: 500;
border-radius: var(--chrome-radius-pill);
}
/* File drop zone — flat chrome → Tailwind utilities inline in
clone/AudioMethodPanel.jsx (P4). */
/* Section tab buttons (sidebar) */
/* Sidebar item — compact card with hover-reveal actions */
.history-item {
border-radius: var(--chrome-radius-pill) !important;
background: transparent !important;
border: 1px solid var(--chrome-border) !important;
padding: 6px 8px !important;
margin-bottom: 3px;
position: relative;
display: flex; flex-direction: column; gap: 2px;
transition: background var(--dur-fast), border-color var(--dur-fast);
}
.history-item::before {
content: ""; position: absolute; left: -1px; top: 10%; bottom: 10%; width: 2px;
background: var(--row-accent, var(--chrome-accent));
opacity: 0; transition: opacity var(--dur-fast);
}
.history-item:hover {
background: var(--chrome-hover-bg) !important;
border-color: var(--chrome-border-strong) !important;
}
.history-item:hover::before { opacity: 1; }
.history-item.project-active {
background: color-mix(in srgb, #b8bb26 10%, transparent) !important;
border-color: color-mix(in srgb, #b8bb26 40%, transparent) !important;
}
.history-item.project-active::before {
opacity: 1; background: #b8bb26;
}
/* Header row container migrated to utilities: flex items-center justify-between gap-2 min-w-0 */
.history-kind {
display: inline-flex; align-items: center; gap: 4px;
font-family: var(--chrome-font-mono);
font-size: var(--chrome-label-size); font-weight: 600;
padding: 1px 7px; border-radius: var(--chrome-radius-pill);
letter-spacing: var(--chrome-label-track); text-transform: uppercase;
background: transparent;
border: 1px solid var(--chrome-border);
color: var(--chrome-fg-muted);
flex-shrink: 0;
}
.history-meta {
font-family: var(--chrome-font-mono);
font-size: 0.6rem; color: var(--chrome-fg-dim); font-weight: 500;
white-space: nowrap;
}
/* Primary name/title line */
.history-title {
font-family: var(--font-sans);
font-size: 0.78rem; font-weight: 600;
color: var(--chrome-fg); line-height: 1.2;
letter-spacing: 0.01em;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.history-subtitle {
font-family: var(--font-sans);
font-size: 0.62rem; color: var(--chrome-fg-dim); font-weight: 500;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* Action row — visible on hover, always visible on focus-within */
.history-actions {
display: flex; gap: 4px; margin-top: 2px;
opacity: 0; max-height: 0;
overflow: hidden;
transition: opacity 0.2s, max-height 0.2s;
}
.history-item:hover .history-actions,
.history-item:focus-within .history-actions {
opacity: 1; max-height: 40px;
}
.history-action-btn {
flex: 1;
display: inline-flex; align-items: center; justify-content: center; gap: 4px;
padding: 4px 8px;
background: transparent;
border: 1px solid var(--chrome-border);
border-radius: var(--chrome-radius-pill);
color: var(--chrome-fg-muted);
font-family: var(--font-sans);
font-size: 0.62rem; font-weight: 500;
letter-spacing: 0.02em;
cursor: pointer;
transition: background var(--dur-fast), color var(--dur-fast), border-color var(--dur-fast);
white-space: nowrap;
}
.history-action-btn:hover {
background: var(--chrome-hover-bg);
color: var(--chrome-fg);
border-color: var(--chrome-border-strong);
}
.history-action-btn.danger:hover {
background: color-mix(in srgb, var(--chrome-severity-err) 12%, transparent);
color: var(--chrome-severity-err);
border-color: color-mix(in srgb, var(--chrome-severity-err) 45%, transparent);
}
.history-action-btn.accent:hover {
background: color-mix(in srgb, #b8bb26 12%, transparent);
color: #b8bb26;
border-color: color-mix(in srgb, #b8bb26 45%, transparent);
}
.history-action-icon {
width: 26px; flex: 0 0 auto; padding: 4px 6px;
}
/* Wobbly glass panels — applied broadly except sidebar/history panel */
.glass-panel {
border-radius: 14px 18px 12px 20px / 16px 12px 18px 14px;
}
.glass-panel.history-panel,
.glass-panel[data-square="1"] {
border-radius: 0;
}
/* Segment table — keep rectangular for virtualized rows */
.segment-table,
.segment-row,
.segment-header {
border-radius: 0 !important;
}
/* ═══ Settings — page chrome (`.settings-page` layout / padding / scroll / the
≥760px [rail | content] grid + `.settings-content`) is now Tailwind on the
token bridge in src/pages/Settings.jsx; the page-specific tab-rail look and
the Models / Engines rules that can't migrate stay in src/pages/Settings.css.
Badge, tabs, subtabs migrated to ui/Badge + ui/Tabs + ui/Segmented.
The legacy section/row frames are gone too (P4): .settings-section + h2 and
.settings-row(.label/.value) were superseded by the st-section primitive
(components/settings/primitives/SettingsSection.jsx), and .settings-log moved
to Tailwind utilities inline in components/settings/LogsTab.jsx. ═══ */
/* Studio panels — flat chrome surface. Radial warmth + paper grain removed
so Clone / Design / Dub panes sit on the same neutral frame as the rest
of the chrome-tokenized app. */
.studio-panel {
background: var(--chrome-bg);
border: 1px solid var(--chrome-border);
border-radius: 0;
padding: 10px 12px;
position: relative;
overflow: auto;
display: flex; flex-direction: column;
min-height: 0;
}
.studio-panel::before { display: none; }
.studio-panel > * { position: relative; z-index: 1; }
/* `.studio-column` removed (fast shadcn): its only consumers were Clone's two
columns, now built from Tailwind utilities in CloneDesignTab.jsx +
ScriptPanel.jsx. The dub area uses `.studio-panel` directly, not a column
wrapper. */
/* Clone's pinned Synthesize action bar (ActionBar.jsx). Relocated here from the
deleted CloneDesignTab.css: WorkspaceHistory.css adds a `position: sticky`
override to `.studio-action-bar` in the narrow shell (the #476 CTA-clipping
fix, guarded by test/workspaceHistoryReflow.test.js), so this stays a shell
class rather than inline utilities. Its __row/__lang/__steps/__overrides
children migrated to Tailwind utilities at the usage site. */
.studio-action-bar {
flex: 0 0 auto;
/* Expanded settings leave room for the form even in a short window. */
max-height: 60%;
border-top: 1px solid var(--chrome-border);
background: var(--chrome-bg);
padding: 8px 12px 10px;
display: flex;
flex-direction: column;
gap: 8px;
}
.studio-action-bar .override-content {
min-height: 0;
flex-shrink: 1;
max-height: 40vh;
overflow-y: auto;
}
/* ═══════════════════════════════════════════════════════════
RESPONSIVE — make every component adapt to viewport width
════════════════════════════════════════════════════════════ */
/* DubTab split grid → single column is handled inline in DubTab.jsx /
IdleSkeleton.jsx via Tailwind max-[1000px]:grid-cols-1 utilities.
Clone's split is a single-column flex stack at every width (built from
Tailwind utilities in CloneDesignTab.jsx) — no narrow override needed. */
/* .grid-2 / .grid-4 responsive collapse → Tailwind max-[700px]/max-[500px]
variants inline at the usage sites (P4). .grid-3 was already dead (no base
rule, no usages) and is dropped with them. */
/* ── Launchpad ── */
/* The feature cards now reflow via `.lp-cards` (auto-fit minmax, driven by the
grid's OWN width) with the card floor tuned by useShellNarrow — so there are
no viewport @media overrides here (they fire at the wrong width whenever
--ui-scale ≠ 1). See the ".lp-cards / .lp-action-card" rules above. */
/* ── Studio panels — tighter on small screens ── */
@media (max-width: 800px) {
.studio-panel { padding: 10px 10px; }
.studio-panel .label-row { font-size: 0.68rem; }
}
@media (max-width: 600px) {
.studio-panel { padding: 8px 6px; }
}
/* DubTab settings-bar wrap is handled inline in DubLeftColumn.jsx via Tailwind
max-[900px]:gap utilities (the bar is already a flex-column at every width). */
/* ── Segment table — tighter columns ── */
@media (max-width: 800px) {
.segment-table {
/* Tighter grid template on narrow screens. */
--seg-grid-cols: 16px 56px 60px minmax(0, 1fr) 38px 52px 36px 38px;
}
.segment-time { font-size: 0.58rem !important; }
.segment-row { padding: 2px 4px; }
.segment-header { padding: 2px 4px; font-size: 0.58rem; }
}
/* Legacy WaveSurfer region sizing; the current editor uses SegmentTrack. */
@media (max-width: 800px) {
.waveform-container [data-id^="wavesurfer-region"] {
top: 36px !important;
height: 18px !important;
}
}
/* ── Tabs — scroll instead of squish on narrow ── */
@media (max-width: 600px) {
.tabs { overflow-x: auto; -webkit-overflow-scrolling: touch; }
.tab { flex: 0 0 auto; padding: 5px 10px; font-size: 0.65rem; }
}
/* ── Preset grid — 2 columns on narrow ── */
@media (max-width: 700px) {
.preset-grid { grid-template-columns: 1fr 1fr !important; }
}
@media (max-width: 500px) {
.preset-grid { grid-template-columns: 1fr !important; }
}
/* ── Compare modal — stack on narrow ── */
@media (max-width: 800px) {
.ui-compare__grid {
grid-template-columns: 1fr !important;
}
}
/* ── General: prevent horizontal overflow everywhere ── */
@media (max-width: 600px) {
body { font-size: 11px; }
.btn-primary { font-size: 0.7rem; padding: 5px 8px; }
.input-base { font-size: 0.7rem; }
select.input-base { font-size: 0.7rem; }
}
/* ═══ CAPTURE / DICTATION PILL HOST ═══ */
/* Positions the CaptureWidget pill at top-center of the main window.
Non-blocking — no dark backdrop, no modal trap. */
.capture-pill-host {
position: fixed;
top: 48px;
left: 50%;
transform: translateX(-50%);
z-index: 9999;
pointer-events: none;
}
.capture-pill-host > * {
pointer-events: auto;
}
/* ── Icon refinement — thinner, crisper strokes app-wide ──────────────────
Lucide ships stroke-width 2 on a 24px grid; rendered at our 11–16px
sizes that weight reads heavy and clunky. stroke-width is an SVG
presentation attribute (lowest cascade priority), so this single rule
re-weights every lucide icon app-wide without touching call sites.
geometricPrecision favors smooth anti-aliased curves over the default
renderer's stroke snapping, which blurs at small raster sizes.
Hand-rolled SVGs (logo mark, batch spinner) don't carry the .lucide
class and keep their bespoke weights. */
svg.lucide {
stroke-width: 1.5;
shape-rendering: geometricPrecision;
}
/* ── Native file inputs — themed "Choose file" chip ───────────────────────
Every in-app file input is currently hidden behind a themed label, but
any visible <input type="file"> (future panels, the LAN/share web view,
dev surfaces) must not render the OS-default grey button. Mirrors the
shadcn `subtle` button variant (components/ui/button.tsx). */
input[type="file"] {
color: var(--chrome-fg-muted);
font-size: var(--text-xs);
}
input[type="file"]::file-selector-button {
margin-right: 8px;
padding: 3px 10px;
font: inherit;
font-size: var(--text-xs);
color: var(--chrome-fg-muted);
background: transparent;
border: 1px solid var(--chrome-border);
border-radius: var(--chrome-radius-pill);
cursor: pointer;
transition: background 120ms ease, color 120ms ease, border-color 120ms ease;
}
input[type="file"]::file-selector-button:hover {
background: var(--chrome-hover-bg);
color: var(--chrome-fg);
border-color: var(--chrome-border-strong);
}
/* ═══════════════════════════════════════════════════════════════════════
CONSOLIDATED COMPONENT STYLES (final CSS consolidation)
─────────────────────────────────────────────────────────────────────
Every former per-component .css file is folded in here, VERBATIM, in
one place so the project ships essentially a single stylesheet. These
rules are ALL intentionally UNLAYERED — exactly as they were as separate
files — so they keep beating Tailwind's @layer utilities regardless of
specificity. They are appended AFTER all of index.css's own rules to
preserve the previous cross-file load order (index.css → residual.css →
component css). @keyframes are referenced by name, so relocation needs no
usage changes; all keyframe names are globally unique. Do NOT wrap any of
this in @layer. Provenance is recorded in the "from <path>" header above
each block.
═══════════════════════════════════════════════════════════════════════ */
/* ═══ from src/styles/residual.css ═══ */
/* ─────────────────────────────────────────────────────────────────────────
residual.css — consolidated post-Tailwind-migration stub rules.
After the Tailwind v4 migration most component styling moved to utility
classes on the JSX. What remained were a handful of tiny per-component
stylesheets holding only the few rules that *can't* be expressed as layered
utilities — @keyframes-driven animations, focus-visible rings, glass
surfaces, a <select> caret, ::before/::after pseudos, and attribute-selector
overrides that must out-rank Tailwind's @layer utilities.
These were ~15 separate files, each imported by its own component. They are
ALL plain global class selectors, so the file boundary bought nothing — this
file collapses them into one, loaded once at app root (main-app.jsx) and once
in the visual harness (harness.jsx), preserving the previous load order
(always after index.css).
CRITICAL: every rule here is intentionally UNLAYERED. The migration relies on
the fact that unlayered CSS beats anything inside Tailwind's `@layer
utilities`, regardless of specificity. Do NOT wrap any of this in `@layer` —
that would drop these rules below the utilities they are meant to override
(focus rings, hover backgrounds, element resets, etc.) and silently change
the rendering. Each block is moved verbatim from its origin file; the
"from <Component>" header above each block records provenance.
───────────────────────────────────────────────────────────────────────── */
/* ── ui primitives ─────────────────────────────────────────────────────── */
/* from Badge (ui/Badge.css) */
/* All static chrome (base, sizes, tones, dot) now lives as Tailwind utility
classes in Badge.jsx. Only the pulse modifier survives here: it is driven by
the externally-applied `.ui-badge--pulse` parent class (Header status badge)
targeting the dot via a descendant selector + the global `pulse` keyframes —
a relationship Tailwind utilities on the component itself can't express. */
.ui-badge--pulse .ui-badge__dot {
animation: pulse 1.5s ease-in-out infinite;
}
/* from Input (ui/Input.css) — native <select> caret only */
/* The shared input/textarea/select shell, sizes, states, and the Field
wrapper are now Tailwind utilities in Input.jsx. Only the <select> caret
remains here: an SVG data-URI background (with its appearance reset, caret
placement, padding-right for the arrow, and hover border) that isn't
practical to express as a utility. */
.ui-select {
appearance: none;
padding-right: 20px;
/* Caret color tracks the theme via --select-caret (see :root / [data-theme]). */
background-image: var(--select-caret);
background-repeat: no-repeat;
background-position: right 7px center;
background-size: 0.5rem auto;
cursor: pointer;
}
.ui-select:hover {
border-color: var(--color-border-strong);
}
/* Native option popup: theme-token colors (see select.input-base option note). */
.ui-select option,
.ui-select optgroup {
background-color: var(--color-bg-elev-1);
color: var(--chrome-fg);
}
/* from Panel (ui/Panel.css) — glass surface only */
/* Layout, border, radius, padding, header/title/actions, and the solid/flat
variants are now Tailwind utilities in Panel.jsx. Only the glass variant's
backdrop-filter + layered gradient surface and the ::before top-edge
highlight remain here — none of these are expressible as utilities (the app
ships Tailwind v4 without the filter/backdrop `--tw-*` plumbing, and ::before
is a pseudo-element). */
/* glass — warm backdrop-filter card (replaces legacy .glass-panel) */
.ui-panel--glass {
background:
radial-gradient(500px 240px at 15% 0%, rgba(251, 188, 148, 0.05) 0%, transparent 70%),
radial-gradient(500px 280px at 100% 100%, rgba(211, 134, 155, 0.045) 0%, transparent 70%),
linear-gradient(160deg, rgba(47, 41, 39, 0.82) 0%, rgba(38, 33, 31, 0.82) 100%);
backdrop-filter: var(--glass-blur-md);
-webkit-backdrop-filter: var(--glass-blur-md);
}
.ui-panel--glass::before {
content: "";
position: absolute;
top: 0; left: 0; right: 0;
height: 1px;
background: linear-gradient(
90deg,
rgba(255, 255, 255, 0),
rgba(255, 255, 255, 0.08) 30%,
rgba(255, 255, 255, 0.12) 50%,
rgba(255, 255, 255, 0.08) 70%,
rgba(255, 255, 255, 0)
);
pointer-events: none;
}
/* from Progress (ui/Progress.css) */
/* Track, sizes, fill base, and the per-tone gradient fills are Tailwind
utilities in Progress.jsx now. What remains here can't be expressed by
utilities: the shimmer overlay (a `::after` pseudo-element + keyframes) and
the indeterminate sliding bar (a descendant rule + keyframes). Both hook off
the retained `ui-progress` / `ui-progress__fill` / `has-shimmer` /
`is-indeterminate` class names. */
.ui-progress__fill.has-shimmer::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.25), transparent);
background-size: 200% 100%;
animation: ui-shimmer 1.5s linear infinite;
}
@keyframes ui-shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
/* indeterminate: sliding bar ~30% wide */
.ui-progress.is-indeterminate .ui-progress__fill {
width: 30%;
animation: ui-progress-indet 1.4s cubic-bezier(0.65, 0, 0.35, 1) infinite;
}
@keyframes ui-progress-indet {
0% { transform: translateX(-100%); }
100% { transform: translateX(433%); } /* (100% - 30%) / 30% * 100 + 100 */
}
/* from Segmented (ui/Segmented.css) */
/* Segmented now renders through the shadcn/ui ToggleGroup (the `seg` variant in
src/components/ui/toggle.tsx); its options no longer carry the `.ui-seg__opt`
class. The former `.ui-seg__opt:focus-visible` override is obsolete — keyboard
focus falls through to the global unlayered `:focus-visible` ring in
index.css, which already paints the same brand ring, so no per-option rule is
needed. */
/* ── components ────────────────────────────────────────────────────────── */
/* from AudioTrimmer (components/AudioTrimmer.css) */
/* AudioTrimmer — canvas-specific styles, uses Dialog/Button from ui/. */
.audio-trimmer {
display: flex;
flex-direction: column;
gap: var(--space-5);
outline: none;
}
/* from DemoPresetGrid (components/DemoPresetGrid.css) */
/* Demo preset grid — empty state of the Voice Design tab.
Layout/typography moved to Tailwind utilities on DemoPresetGrid.jsx.
Kept here: the active-preview state, an attribute selector
([aria-pressed="true"]) that must stay unlayered so it wins over the
button's hover utilities. */
.demo-preset-card__preview[aria-pressed="true"] {
background: rgba(243, 165, 182, 0.18);
border-color: rgba(243, 165, 182, 0.4);
color: #fff9ef;
}
/* from MultiLangPicker (components/MultiLangPicker.css) */
.multi-lang-options {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 210px), 1fr));
align-content: start;
gap: 4px;
}
.multi-lang-options > div { grid-column: 1 / -1; }
@keyframes engine-title-enter {
from { opacity: 0; transform: translateY(4px); }
to { opacity: 1; transform: translateY(0); }
}
.engine-title-label { animation: engine-title-enter 180ms ease-out; }
@media (prefers-reduced-motion: reduce) {
.engine-title-label { animation: none; }
}
/* Dropdown */
.multi-lang__drop {
position: fixed;
z-index: var(--z-overlay);
background: var(--chrome-bg);
border: 0;
border-radius: 8px;
box-shadow: 0 8px 24px rgba(0,0,0,0.35);
max-height: 260px;
display: flex;
flex-direction: column;
overflow: hidden;
animation: mlp-in 0.15s ease-out;
}
.multi-lang__drop--portal {
position: fixed;
top: auto;
right: auto;
margin-top: 0;
}
@keyframes mlp-in {
from { opacity: 0; transform: translateY(-4px); }
to { opacity: 1; transform: translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
.multi-lang__drop { animation: none; }
}
/* from ReadinessChecklist (components/ReadinessChecklist.css) */
/* System readiness panel showing pass/warn/fail gates
for ASR model, TTS engine, LLM, ffmpeg, GPU, etc. */
.readiness-checklist {
display: flex;
flex-direction: column;
gap: var(--space-3);
padding: var(--space-5);
background: var(--color-bg-elev-1);
backdrop-filter: var(--glass-blur-sm);
-webkit-backdrop-filter: var(--glass-blur-sm);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
font-family: var(--font-ui);
font-size: var(--text-sm);
}
/* ── Status icons ───────────────────────────────────────────────────── */
.readiness-checklist__status--pass { color: var(--color-success); }
.readiness-checklist__status--warn { color: var(--color-accent); }
.readiness-checklist__status--fail { color: var(--color-danger); }
.readiness-checklist__status--loading {
color: var(--color-fg-subtle);
animation: rc-spin 1s linear infinite;
}
@keyframes rc-spin {
to { transform: rotate(360deg); }
}
/* from TranscriptionPicker (components/TranscriptionPicker.css) */
/* __search / __list / __row base / __text / __meta base / __empty layout →
Tailwind utilities in TranscriptionPicker.jsx. The `__search > *` and
`__meta span` combinators + the row :hover/:focus-visible states stay here
(focus-visible must stay unlayered to beat the global focus reset). */
.txn-picker__search > * { flex: 1 1 auto; min-width: 0; }
.txn-picker__row:hover, .txn-picker__row:focus-visible {
border-color: var(--chrome-accent, #fe8019);
background: color-mix(in srgb, var(--chrome-accent, #fe8019) 8%, transparent);
}
.txn-picker__meta span { display: inline-flex; align-items: center; gap: 3px; }
/* from UpdatesPanel (components/UpdatesPanel.css) */
.updates-panel {
display: flex;
flex-direction: column;
gap: var(--space-4);
color: var(--chrome-fg);
font-size: var(--text-sm);
}
.updates-panel__summary {
display: grid;
grid-template-columns: minmax(0, 1.1fr) minmax(260px, 0.9fr);
gap: var(--space-3);
}
.updates-panel__live,
.updates-panel__preferences,
.updates-panel__notes,
.updates-panel__whatsnew,
.updates-panel__releases {
border-radius: var(--chrome-radius-pill);
background: var(--chrome-hover-bg);
}
.updates-panel__live {
display: flex;
align-items: center;
gap: var(--space-4);
min-height: 92px;
padding: var(--space-5);
}
.updates-panel__status-icon {
display: inline-flex;
width: 40px;
height: 40px;
flex: none;
align-items: center;
justify-content: center;
border-radius: var(--chrome-radius-pill);
color: var(--color-success);
background: color-mix(in srgb, var(--color-success) 12%, var(--chrome-bg));
}
.updates-panel__live--available .updates-panel__status-icon,
.updates-panel__live--downloading .updates-panel__status-icon,
.updates-panel__live--ready .updates-panel__status-icon,
.updates-panel__live--checking .updates-panel__status-icon {
color: var(--chrome-accent);
background: color-mix(in srgb, var(--chrome-accent) 12%, var(--chrome-bg));
}
.updates-panel__live--error .updates-panel__status-icon {
color: var(--color-danger);
background: color-mix(in srgb, var(--color-danger) 12%, var(--chrome-bg));
}
.updates-panel__live-copy {
display: flex;
min-width: 0;
flex: 1;
flex-direction: column;
align-items: flex-start;
gap: var(--space-2);
}
.updates-panel__version-label {
color: var(--chrome-fg-dim);
font-family: var(--chrome-font-mono);
font-size: var(--text-xs);
}
.updates-panel__cta {
display: inline-flex;
align-items: center;
gap: var(--space-2);
border: 1px solid transparent;
border-radius: var(--chrome-radius-pill);
background: var(--chrome-accent);
color: var(--chrome-bg);
padding: var(--space-2) var(--space-4);
font: inherit;
font-weight: 600;
cursor: pointer;
}
.updates-panel__cta:hover:not(:disabled) { filter: brightness(1.08); }
.updates-panel__cta:disabled { cursor: not-allowed; opacity: 0.55; }
.updates-panel__ok,
.updates-panel__err,
.updates-panel__progress {
display: inline-flex;
flex-wrap: wrap;
align-items: center;
gap: var(--space-2);
color: var(--chrome-fg);
line-height: 1.45;
}
.updates-panel__err { color: var(--color-danger); }
.updates-panel__link {
display: inline-flex;
align-items: center;
gap: 3px;
border: none;
background: none;
color: var(--chrome-accent);
padding: 2px;
font: inherit;
font-weight: 600;
cursor: pointer;
}
.updates-panel__link:hover { text-decoration: underline; }
.updates-panel__cta:focus-visible,
.updates-panel__link:focus-visible,
.updates-panel__icon:focus-visible,
.updates-panel__segbtn:focus-visible,
.changelog-viewer__head:focus-visible {
outline: none;
box-shadow: var(--focus-ring);
}
.updates-panel__icon {
display: inline-flex;
border: none;
background: none;
color: var(--color-danger);
padding: 2px;
cursor: pointer;
}
.updates-panel__bar {
display: inline-block;
width: min(150px, 34vw);
height: 5px;
overflow: hidden;
border-radius: var(--chrome-radius-pill);
background: color-mix(in srgb, var(--chrome-fg) 10%, transparent);
}
.updates-panel__bar span {
display: block;
height: 100%;
border-radius: inherit;
background: var(--chrome-accent);
transition: width 180ms ease;
}
.updates-panel__preferences {
display: flex;
flex-direction: column;
justify-content: center;
gap: var(--space-4);
padding: var(--space-5);
}
.updates-panel__channel {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: var(--space-3);
color: var(--chrome-fg-muted);
font-weight: 500;
}
.updates-panel__seg {
display: inline-flex;
padding: 2px;
border-radius: var(--chrome-radius-pill);
background: var(--chrome-bg);
}
.updates-panel__segbtn {
border: none;
border-radius: var(--chrome-radius-pill);
background: none;
color: var(--chrome-fg-muted);
padding: var(--space-2) var(--space-3);
font: inherit;
cursor: pointer;
}
.updates-panel__segbtn.is-active {
background: var(--chrome-hover-bg);
color: var(--chrome-fg);
box-shadow: 0 1px 4px color-mix(in srgb, black 20%, transparent);
}
.updates-panel__rel-head {
display: flex;
align-items: center;
gap: var(--space-2);
margin: 0 0 var(--space-3);
color: var(--chrome-fg);
font-size: var(--text-md);
font-weight: 600;
}
.updates-panel__rel-empty {
display: inline-flex;
align-items: center;
gap: var(--space-3);
padding: var(--space-2) 0;
color: var(--chrome-fg-muted);
}
.updates-panel__rel {
padding: var(--space-3) var(--space-4);
border-radius: var(--chrome-radius-pill);
}
.updates-panel__rel + .updates-panel__rel { margin-top: var(--space-2); }
.updates-panel__rel.is-current {
background: color-mix(in srgb, var(--color-success) 8%, var(--chrome-bg));
}
.updates-panel__rel-row { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2); }
.updates-panel__rel-ver { font-family: var(--chrome-font-mono); font-weight: 600; }
.updates-panel__rel-tag,
.updates-panel__rel-pre {
border-radius: var(--chrome-radius-pill);
padding: 1px var(--space-2);
font-size: var(--text-xs);
font-weight: 600;
}
.updates-panel__rel-tag {
color: var(--color-success);
background: color-mix(in srgb, var(--color-success) 12%, transparent);
}
.updates-panel__rel-pre {
color: var(--chrome-accent);
background: color-mix(in srgb, var(--chrome-accent) 12%, transparent);
}
.updates-panel__rel-date { margin-left: auto; color: var(--chrome-fg-dim); font-size: var(--text-xs); }
.updates-panel__rel-notes {
overflow-wrap: break-word;
margin: var(--space-2) 0 0;
color: var(--chrome-fg-muted);
font-size: var(--text-xs);
line-height: 1.55;
}
/* Safe markdown-lite renderer (release notes / changelog bullets) */
.md-lite__heading { font-weight: 600; color: var(--chrome-fg); margin: 6px 0 2px; }
.md-lite__para { margin: 4px 0; }
.md-lite__list { margin: 4px 0; padding-left: 16px; list-style: disc; }
.md-lite__list li { margin: 2px 0; }
.md-lite code { font-size: 0.9em; padding: 0 3px; border-radius: 3px; background: color-mix(in srgb, var(--chrome-fg) 8%, transparent); }
/* Updates panel: available-update notes, data-safety line, "What's new" */
.updates-panel__notes,
.updates-panel__whatsnew,
.updates-panel__releases { padding: var(--space-5); }
.updates-panel__notes {
color: var(--chrome-fg-muted);
font-size: var(--text-sm);
line-height: 1.55;
}
.updates-panel__notes-head { margin-bottom: var(--space-2); color: var(--chrome-fg); font-weight: 600; }
.updates-panel__backup {
display: flex;
align-items: flex-start;
gap: var(--space-2);
padding-top: var(--space-3);
border-top: 1px solid color-mix(in srgb, var(--chrome-fg) 7%, transparent);
color: var(--chrome-fg-dim);
font-size: var(--text-xs);
line-height: 1.5;
}
.updates-panel__backup svg { flex: none; color: var(--color-success); margin-top: 2px; }
/* Changelog reader (accordion over the shipped CHANGELOG.md) */
.changelog-viewer { overflow: hidden; border-radius: var(--chrome-radius-pill); background: var(--chrome-bg); }
.changelog-viewer__rel + .changelog-viewer__rel { border-top: 1px solid color-mix(in srgb, var(--chrome-fg) 7%, transparent); }
.changelog-viewer__head {
display: flex;
align-items: center;
gap: var(--space-2);
width: 100%;
padding: var(--space-3) var(--space-4);
border: none;
background: none;
color: var(--chrome-fg);
font: inherit;
cursor: pointer;
text-align: left;
}
.changelog-viewer__head:hover,
.changelog-viewer__rel.is-open .changelog-viewer__head { background: var(--chrome-hover-bg); }
.changelog-viewer__ver { font-family: var(--chrome-font-mono); font-weight: 600; }
.changelog-viewer__date { margin-left: auto; color: var(--chrome-fg-dim); font-size: var(--text-xs); }
.changelog-viewer__body { padding: var(--space-2) var(--space-5) var(--space-4); color: var(--chrome-fg-muted); font-size: var(--text-xs); line-height: 1.55; }
.changelog-viewer__intro { margin: var(--space-2) 0 var(--space-3); }
.changelog-viewer__sec-title { margin: var(--space-3) 0 var(--space-2); color: var(--chrome-fg); font-weight: 600; }
.changelog-viewer__bullets { margin: var(--space-2) 0; padding-left: var(--space-5); list-style: disc; }
.changelog-viewer__bullets li { margin: var(--space-2) 0; }
@media (max-width: 920px) {
.updates-panel__summary { grid-template-columns: minmax(0, 1fr); }
}
@container settings (max-width: 700px) {
.updates-panel__summary { grid-template-columns: minmax(0, 1fr); }
}
/* from VoiceSelector (components/VoiceSelector.css) */
/* Shared voice picker (#22): the SearchableSelect grows to fill, adornment
buttons sit to its right.
.voice-selector / __adornments / __btn base layout → Tailwind utilities in
VoiceSelector.jsx. The `> .ss-wrap` child combinator, the __btn :hover/
:disabled states, and the spin animation stay here. */
.voice-selector > .ss-wrap {
flex: 1 1 auto;
min-width: 0;
}
.voice-selector__btn:hover:not(:disabled) {
color: var(--chrome-fg, #eee);
border-color: var(--chrome-border-strong, transparent);
}
.voice-selector__btn:disabled {
opacity: 0.5;
cursor: default;
}
.voice-selector__spin {
animation: voice-selector-spin 0.8s linear infinite;
}
@keyframes voice-selector-spin {
to { transform: rotate(360deg); }
}
/* from ApiKeysPanel (components/settings/ApiKeysPanel.css) */
/* Settings → API Keys panel — Wave 2 AUTH-03 UI. Most of this panel's chrome
now lives in Tailwind utilities on the JSX. The rules kept here are the
source/row card + the "Active" badge: ApiKeysPanel.test.jsx selects them by
class name (`.apikeys-row`, `.apikeys-row--active`, `.apikeys-badge--active`),
so they stay as a stable contract. All surfaces still route through
var(--chrome-*) tokens so they theme across all 6 themes. */
.apikeys-row {
border: 1px solid var(--chrome-border);
border-radius: var(--chrome-radius-pill);
padding: var(--space-4) var(--space-4);
background: var(--chrome-bg);
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.apikeys-row--active {
border-left: 2px solid var(--chrome-accent);
background: color-mix(in srgb, var(--chrome-accent) 5%, var(--chrome-bg));
}
.apikeys-badge {
font-size: 0.66rem;
text-transform: uppercase;
letter-spacing: 0.05em;
padding: 2px 7px;
border-radius: 999px;
background: var(--chrome-hover-bg);
color: var(--chrome-fg-muted);
}
.apikeys-badge--active {
background: color-mix(in srgb, var(--chrome-accent) 25%, transparent);
color: var(--chrome-accent);
}
/* ── pages ─────────────────────────────────────────────────────────────── */
/* from ToolsPage (pages/ToolsPage.css) */
/* Layout/spacing/box rules now live as Tailwind utilities in ToolsPage.jsx.
Only the rules below remain: the `<h1>` font-family + the descendant
`code`/`pre` typography — both fight the unlayered global element rules in
index.css (h1 → font-sans, code/pre → font-variant-numeric), which would win
over @layer utilities, so they stay as plain CSS. */
.tools-page__bar h1 {
margin: 0;
font-family: var(--font-display);
font-size: var(--text-xl);
font-weight: var(--weight-bold);
display: inline-flex;
align-items: center;
gap: var(--space-3);
}
.tools-card__out code { font-family: var(--font-mono); color: var(--color-fg); }
.tools-card__out pre { margin: var(--space-2) 0 0; font-family: var(--font-mono); overflow-x: auto; white-space: pre-wrap; }
/* ═══ from src/components/Misc.css ═══ */
/* ═══ CheckpointBanner extracted styles ═══ */
.ckpt-banner {
display: flex; align-items: center; gap: 10px;
padding: 8px 12px; margin-bottom: 6px;
border-radius: var(--chrome-radius-pill);
background: var(--chrome-bg);
border: 1px solid var(--chrome-border);
}
.ckpt-icon { flex-shrink: 0; }
.ckpt-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 1px; }
.ckpt-head { display: flex; align-items: baseline; gap: 6px; }
.ckpt-title {
font-family: var(--chrome-font-mono);
font-size: var(--chrome-label-size);
letter-spacing: var(--chrome-label-track);
text-transform: uppercase;
font-weight: 600;
color: var(--chrome-fg);
}
.ckpt-count {
font-family: var(--chrome-font-mono);
font-size: var(--chrome-label-size);
color: var(--chrome-fg-muted);
font-variant-numeric: tabular-nums;
}
.ckpt-hint { font-size: 0.64rem; color: var(--chrome-fg-muted); line-height: 1.35; }
/* ═══ DirectionDialog extracted styles ═══ */
.dir-preview-actions { display: flex; gap: 8px; align-items: center; margin-top: 8px; }
.dir-clear-btn { margin-right: auto; }
.dir-rate-up { color: var(--color-brand); }
.dir-rate-down { color: var(--color-info); }
.dir-error { color: var(--color-warn); font-size: 0.7rem; }
/* ═══ App.jsx startup / wizard extracted styles ═══ */
.app-startup {
display: flex; align-items: center; justify-content: center;
min-height: 100vh; flex-direction: column; gap: 12px;
color: #a89984; font-size: 13px;
}
.app-startup__title { font-size: 18px; color: #ebdbb2; }
.app-wizard-wrap {
/* Fills the WHOLE viewport: the first-run wizard deliberately renders no
LogsFooter (studio chrome belongs to the studio), so there is nothing to
reserve space for. While it did reserve 28px, the wizard's own root was
`fixed inset-0` and escaped this box anyway — its pinned Continue / HF
token row rendered underneath the status bar and was unreachable.
UI scale: SAME contract as .app-container (#504) — the box is shrunk to
`100vw/100vh ÷ scale`, then `zoom` magnifies it back to exactly the
viewport. The previous shape (`inset: 0` + inline `zoom` on the mount)
zoomed a FULL-viewport box, so at any scale > 1 the bottom
`(1 − 1/scale)` of the wizard — the pinned Continue button and the HF
token card — rendered past the window edge with no scrollbar to reach
them (overflow: hidden). At scale 1 nothing showed, which is how it
shipped. Guarded by src/test/SetupWizardChrome.test.jsx. */
position: fixed;
top: 0;
left: 0;
width: calc(100vw / var(--ui-scale, 1));
height: calc(100vh / var(--ui-scale, 1));
zoom: var(--ui-scale, 1);
overflow: hidden;
background: var(--color-bg, #1d2021);
display: flex;
flex-direction: column;
}
/* Older WebKitGTK (Linux) where `zoom` is a layout no-op (same probe +
rationale as the .app-container override above): the shrunk box can't be
magnified back, so render at 1.0 filling the window instead. */
html[data-zoom-layout='off'] .app-wizard-wrap {
width: 100vw;
height: 100vh;
zoom: 1;
}
html[data-ui-scale-engine='native'] .app-wizard-wrap {
width: 100vw;
height: 100vh;
zoom: 1;
}
.app-bootstrap-scale {
position: relative;
width: calc(100vw / var(--ui-scale, 1));
height: calc(100vh / var(--ui-scale, 1));
zoom: var(--ui-scale, 1);
overflow: hidden;
background: var(--color-bg, #1d2021);
}
html[data-ui-scale-engine='native'] .app-bootstrap-scale {
width: 100vw;
height: 100vh;
zoom: 1;
}
.app-wizard-dragstrip {
position: fixed; top: 0; left: 0; right: 0;
height: 28px; z-index: 10;
}
.app-lazy-fallback { padding: 12px; color: #6b6657; font-size: 0.7rem; }
/* ═══ First-start UI scale check ═══ */
.ui-scale-setup {
width: 100%;
min-height: 100%;
overflow: auto;
display: flex;
align-items: center;
justify-content: center;
padding: 28px 20px;
color: var(--color-fg);
}
.ui-scale-setup__card {
width: min(720px, 100%);
padding: 28px;
border-radius: 14px;
background: var(--color-bg-elev-1);
box-shadow: 0 20px 70px rgba(0, 0, 0, 0.28);
}
.ui-scale-setup__eyebrow {
display: flex;
align-items: center;
gap: 7px;
color: var(--color-fg-muted);
font-family: var(--font-mono);
font-size: var(--text-xs);
letter-spacing: 0.04em;
text-transform: uppercase;
}
.ui-scale-setup__title {
margin: 14px 0 7px;
color: var(--color-fg);
font-size: clamp(1.4rem, 3vw, 2rem);
line-height: 1.12;
text-wrap: balance;
}
.ui-scale-setup__description {
max-width: 560px;
margin: 0;
color: var(--color-fg-muted);
font-size: var(--text-sm);
line-height: 1.5;
text-wrap: pretty;
}
.ui-scale-setup__options {
display: grid;
grid-template-columns: repeat(6, minmax(0, 1fr));
gap: 8px;
margin-top: 24px;
}
.ui-scale-option {
position: relative;
min-height: 66px;
padding: 10px 6px;
border: 1px solid var(--color-border);
border-radius: 9px;
background: var(--color-bg-elev-2);
color: var(--color-fg-muted);
cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4px;
transition:
border-color 120ms ease,
background 120ms ease,
color 120ms ease;
}
.ui-scale-option:hover,
.ui-scale-option:focus-visible {
border-color: var(--color-primary);
color: var(--color-fg);
}
.ui-scale-option.is-selected {
border-color: var(--color-primary);
background: color-mix(in srgb, var(--color-primary) 14%, var(--color-bg-elev-2));
color: var(--color-fg);
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-primary) 38%, transparent);
}
.ui-scale-option__value {
font-family: var(--font-mono);
font-size: var(--text-sm);
font-weight: 700;
}
.ui-scale-option__hint {
overflow: hidden;
max-width: 100%;
color: var(--color-primary);
font-size: 0.56rem;
line-height: 1;
text-overflow: ellipsis;
white-space: nowrap;
}
.ui-scale-preview {
margin-top: 24px;
padding: 14px;
border-radius: 10px;
background: var(--color-bg-elev-2);
}
.ui-scale-preview__heading {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
color: var(--color-fg-muted);
font-size: var(--text-xs);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.ui-scale-preview__percent {
color: var(--color-primary);
font-family: var(--font-mono);
}
.ui-scale-preview__window {
overflow: hidden;
border-radius: 7px;
background: var(--color-bg);
}
.ui-scale-preview__toolbar {
display: flex;
gap: 5px;
padding: 7px 9px;
background: var(--color-bg-elev-1);
}
.ui-scale-preview__dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--color-fg-muted);
opacity: 0.55;
}
.ui-scale-preview__body { display: flex; min-height: 112px; }
.ui-scale-preview__sidebar {
width: 22%;
background: color-mix(in srgb, var(--color-bg-elev-1) 68%, transparent);
}
.ui-scale-preview__content { flex: 1; padding: 18px; }
.ui-scale-preview__line {
width: 46%;
height: 8px;
margin-top: 10px;
border-radius: 4px;
background: var(--color-border);
}
.ui-scale-preview__line--wide {
width: 72%;
margin-top: 0;
background: color-mix(in srgb, var(--color-primary) 50%, var(--color-border));
}
.ui-scale-preview__controls { display: flex; gap: 8px; margin-top: 24px; }
.ui-scale-preview__controls span {
width: 54px;
height: 20px;
border-radius: 4px;
background: var(--color-bg-elev-1);
}
.ui-scale-setup__footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-top: 22px;
}
.ui-scale-setup__note { color: var(--color-fg-muted); font-size: var(--text-xs); }
.ui-scale-control {
display: inline-flex;
align-items: center;
gap: 2px;
white-space: nowrap;
}
.ui-scale-control__label {
margin-right: 3px;
color: var(--color-fg-muted);
font-family: var(--font-mono);
font-size: 0.58rem;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.ui-scale-control__value {
min-width: 3.2em;
color: var(--color-fg);
font-family: var(--font-mono);
font-size: 0.65rem;
text-align: center;
}
@media (max-width: 620px) {
.ui-scale-setup__card { padding: 20px; }
.ui-scale-setup__options { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.ui-scale-setup__footer { align-items: stretch; flex-direction: column; }
}
@media (prefers-reduced-motion: reduce) {
.ui-scale-option { transition: none; }
}
/* ═══ AudioTrimmer play button ═══ */
.audio-trimmer__play-btn {
color: var(--color-success);
border-color: rgba(142,192,124,0.3);
background: rgba(142,192,124,0.08);
}
/* ═══ from src/components/firstrun.css ═══ */
/* First-run journey — the only irreducible motion kept from the old bespoke
* "studio console" design. Everything else (layout, surfaces, spacing, colors)
* is now standard shadcn primitives + Tailwind utilities themed by the palette
* tokens. These three keyframes give the setup → install → models flow its one
* signature: a breathing voice waveform, a staggered rise-in, and a pulsing
* "active step" LED — none of which Tailwind utilities can express alone.
*/
@keyframes fr-breathe {
from { transform: scaleY(calc(var(--h, 0.4) * 0.55)); opacity: 0.22; }
to { transform: scaleY(var(--h, 0.4)); opacity: 0.6; }
}
@keyframes fr-rise {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes fr-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
/* Breathing waveform masthead — bar height + stagger come from inline vars. */
.fr-wave {
display: flex;
align-items: center;
gap: 4px;
height: 22px;
overflow: hidden;
-webkit-mask-image: linear-gradient(90deg, transparent, #000 5%, #000 95%, transparent);
mask-image: linear-gradient(90deg, transparent, #000 5%, #000 95%, transparent);
}
.fr-wave__bar {
flex: 1 0 auto;
width: 2px;
height: 100%;
border-radius: 1px;
background: color-mix(in srgb, var(--color-brand) 70%, transparent);
transform: scaleY(var(--h, 0.4));
transform-origin: center;
opacity: 0.45;
animation: fr-breathe 2.8s ease-in-out infinite alternate;
animation-delay: var(--d, 0ms);
}
.fr-rise {
animation: fr-rise 560ms cubic-bezier(0.22, 1, 0.36, 1) both;
animation-delay: calc(var(--rise, 0) * 70ms);
}
.fr-pulse {
animation: fr-pulse 1.6s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
.fr-wave__bar,
.fr-rise,
.fr-pulse {
animation: none;
}
}
/* ═══ from src/components/Sidebar.css ═══ */
/* Sidebar — page-specific layout + accents, base comes from ui/ primitives. */
/* .sidebar base (flex column) + .sidebar__tabs layout (incl. its collapsed
variant) → Tailwind utilities in Sidebar.jsx. The .sidebar__tab collapsed
override stays here: the .sidebar__tab base is still unlayered CSS below, so
the collapsed combinator must also be unlayered to win the cascade over it.
(Collapsed icon size is handled by the JSX `size` prop, not CSS.) */
.sidebar.is-collapsed .sidebar__tab {
max-width: 100%;
padding: 0;
width: 36px;
height: 36px;
flex: none;
}
.sidebar__tab {
position: relative;
flex: 1;
height: var(--chrome-pill-h);
cursor: pointer;
border: 1px solid transparent;
background: transparent;
color: var(--chrome-fg-muted);
border-radius: var(--chrome-radius-pill);
transition: border-color var(--dur-base), background var(--dur-base), color var(--dur-base);
display: flex;
justify-content: center;
align-items: center;
gap: 5px;
--sidebar-tab-accent: var(--color-brand);
white-space: nowrap;
padding: 0 10px;
}
.sidebar__tab:hover {
background: var(--chrome-hover-bg);
color: var(--chrome-fg);
}
.sidebar__tab.is-active {
border-color: color-mix(in srgb, var(--sidebar-tab-accent) 35%, transparent);
background: color-mix(in srgb, var(--sidebar-tab-accent) 12%, transparent);
color: var(--sidebar-tab-accent);
}
.sidebar__tab:focus-visible {
outline: none;
box-shadow: var(--focus-ring);
}
/* .sidebar__tab-badge → utilities in Sidebar.jsx (positioned mono count pill). */
/* ── Search ─────────────────────────────────────────────────── */
/* .sidebar__search + .sidebar__search-icon → utilities in Sidebar.jsx. */
.sidebar__search-input {
width: 100%;
font-size: var(--text-sm);
padding: 4px 26px 4px 22px;
/* Chrome-radius instead of 999 px pill so the input rhymes with the
footer / header pills. Colors come from the base Input primitive. */
border-radius: var(--chrome-radius-pill);
}
.sidebar__search-clear {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
background: transparent !important;
border: none !important;
width: 16px !important;
height: 16px !important;
}
/* ── Save-project button tint ───────────────────────────────── */
.sidebar__save-btn {
margin-bottom: var(--space-2);
color: var(--chrome-fg);
border: 1px solid var(--chrome-border-strong);
background: transparent;
border-radius: var(--chrome-radius-pill);
font-family: var(--font-sans);
letter-spacing: 0.02em;
}
.sidebar__save-btn:hover { background: var(--chrome-hover-bg); }
.sidebar__save-btn.is-active-project {
color: #b8bb26;
border-color: color-mix(in srgb, #b8bb26 45%, transparent);
background: color-mix(in srgb, #b8bb26 10%, transparent);
}
.sidebar__save-btn--full { justify-content: center; }
/* ── Clear-history row ──────────────────────────────────────── */
.sidebar__clear { margin-top: var(--space-5); color: var(--color-fg-subtle); opacity: 0.7; }
.sidebar__clear:hover { opacity: 1; }
/* ── Empty-state placeholders ───────────────────────────────── */
/* .sidebar__empty + __empty-icon/__empty-title/__empty-sub → utilities in
Sidebar.jsx (EmptyState). Collapsed-hide combinator below stays here. */
/* Hide the empty-state card when collapsed to icon-only width. EmptyState is a
shared component with no collapsed-state prop, so the hide stays a parent-
state combinator here. (The subtitle + search blocks are already gated out of
the JSX when collapsed, so they need no CSS hide.) */
.sidebar.is-collapsed .sidebar__empty { display: none; }
/* Section label — matches the status-bar's "LOGS" uppercase treatment.
Labels are displays (not buttons) but this one IS clickable (collapses
the section), so the chevron on the right signals interactivity while
the text keeps the chrome label typography. */
/* .sidebar__section-title base → utilities in Sidebar.jsx; :hover stays here
(unlayered, so it wins over the layered base color utility). */
.sidebar__section-title:hover { color: var(--chrome-fg); }
/* ── Collapsed-mode icon tiles — chrome square buttons ────────── */
/* .sidebar__icon-tile base (layout + transition) → utilities in Sidebar.jsx
(IconTile). Interactive :hover/.is-active states stay here. */
.sidebar__icon-tile:hover {
background: var(--chrome-hover-bg);
color: var(--chrome-fg);
}
.sidebar__icon-tile.is-active {
background: var(--chrome-accent-bg);
border-color: var(--chrome-accent-border);
color: var(--chrome-accent);
}
/* .sidebar__icon-tile__lock → utilities in Sidebar.jsx. */
/* .sidebar__subtitle base → utilities in Sidebar.jsx; collapsed-hide
combinator above stays here. */
/* ── Scrollable body + collapsed-mode tweaks ─────────────────── */
/* .sidebar__scroll base + its collapsed-mode tweaks → utilities in Sidebar.jsx. */
/* Preset rows that don't need a computed accent */
.history-item--dub { --row-accent: #83a598; }
/* ── History-item kind badge variants ────────────────────────── */
.history-kind--audio { color: #83a598; background: rgba(131, 165, 152, 0.13); }
/* Locked project label + italic subtitle */
.history-meta--locked { color: #b8bb26; font-style: italic; }
.history-subtitle--italic { font-style: italic; }
/* Generated-history title clamp — real line-clamp with an ellipsis, not a
max-height guillotine that sliced the third line mid-glyph. Click toggles
the full text (see WorkspaceHistory). */
.history-title--clamp {
width: 100%;
padding: 0;
border: 0;
background: transparent;
text-align: left;
font: inherit;
color: inherit;
white-space: normal;
font-weight: 500;
font-size: 0.74rem;
line-height: 1.3;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
cursor: pointer;
}
.history-title--clamp:focus-visible {
border-radius: var(--chrome-radius-pill);
outline: none;
box-shadow: var(--focus-ring);
}
.history-title--expanded {
-webkit-line-clamp: unset;
max-height: none;
}
.history-subtitle--seed {
font-family: var(--font-mono);
font-size: 0.58rem;
color: var(--chrome-fg-dim);
}
/* Inline waveform player in generate history (shared WaveformPlayer) */
.history-audio {
margin-top: 4px;
width: 100%;
}
/* Restore / re-open icon tiles on history + export rows.
.sidebar-tile base + per-kind color variants → utilities in Sidebar.jsx
(SIDEBAR_TILE const + inline text color). */
/* ═══ from src/components/LogsFooter.css ═══ */
/* VSCode-style bottom log panel. Fixed to the bottom of the viewport so the
main app can freely scroll underneath. Everything that can be expressed as
element-local styling (buttons, pills, badges, version, discord/donate,
log lines, notification items) now lives as Tailwind utilities in
LogsFooter.jsx. What stays here CANNOT be element-local Tailwind:
(a) the fixed footer shell + the ancestor-combinator inset rules
(`.app-container .logs-footer`, `.rail-right`, the ≤600px media query) —
they depend on the `logs-footer` class being matched by ancestor
selectors, which Tailwind utilities can't express;
(b) the collapsed/open height;
(c) the `::-webkit-scrollbar` pseudo-elements on the scrollable body;
(d) the @keyframes referenced via Tailwind arbitrary `[animation:…]` utilities
(version-dot-pulse on the update dot, heart-glow on the donate button). */
/* (a) Fixed footer shell + rail insets ------------------------------------ */
.logs-footer {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 40;
display: flex;
flex-direction: column;
/* Chrome tokens defined at :root in index.css; shared with the Header topbar. */
background: var(--chrome-bg);
border-top: 1px solid var(--chrome-border);
font-family: var(--font-sans);
font-size: 12px;
color: var(--chrome-fg);
user-select: none;
}
/* Inside the shell the footer is a GRID ITEM (row 3), not a fixed overlay —
content in row 2 physically cannot render underneath it (the fix for
"buttons hidden under the footer on small windows"; class previously
band-aided per-page in #476/#504). The base `position: fixed` above still
applies to the splash/wizard footers, which render outside .app-container
and stay full-width. It sits *beside* the nav rail (rail spans rows 2/-1). */
.app-container .logs-footer {
position: static;
grid-row: 4;
grid-column: 2 / -1;
}
.app-container.rail-right .logs-footer {
grid-column: 1 / -2;
}
/* shell-mini: the nav rail is hidden — footer reclaims the full width.
(Class-driven like the other shell-* rules, so it tracks the EFFECTIVE
scaled width rather than a raw @media viewport width.) */
.app-container.shell-mini .logs-footer,
.app-container.shell-mini.rail-right .logs-footer {
grid-column: 1 / -1;
}
/* Titlebar tabs: no rail, so nothing owns column 1 and the footer starts
there. Leaving it at `2 / -1` doesn't just misalign it — the shell's
templates are one column narrower in this mode, so column 2 is IMPLICIT:
the grid invents an auto-sized column for the footer and takes that width
off the content column. That reads as "the app renders in 65% of the
window with a black band", not as a misplaced footer. */
.app-container.nav-tabs .logs-footer {
grid-column: 1 / -1;
}
/* Global audio mini-player dock — grid row 3, directly above the footer,
same column recipe as .logs-footer (beside the nav rail; full width when
the rail hides). It renders nothing while idle, so the `auto` row
collapses to 0 and the layout is byte-identical to pre-player builds. */
.app-container .global-audio-dock {
grid-row: 3;
grid-column: 2 / -1;
}
.app-container.rail-right .global-audio-dock {
grid-column: 1 / -2;
}
.app-container.shell-mini .global-audio-dock,
.app-container.shell-mini.rail-right .global-audio-dock {
grid-column: 1 / -1;
}
/* Same phantom-column trap as the footer above. */
.app-container.nav-tabs .global-audio-dock {
grid-column: 1 / -1;
}
/* (b) Collapsed/open height ----------------------------------------------- */
.logs-footer--collapsed {
height: var(--chrome-bar-h);
}
.logs-footer--open {
/* height is set inline via React state */
min-height: 180px;
}
/* (c) Scrollbar pseudo-elements for the log body (Tailwind can't target these) */
.logs-footer__body::-webkit-scrollbar { width: 8px; height: 8px; }
.logs-footer__body::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.08); border-radius: 4px; }
.logs-footer__body::-webkit-scrollbar-thumb:hover { background: rgba(255, 255, 255, 0.15); }
/* (d) Keyframes referenced from LogsFooter.jsx via `[animation:…]` utilities.
The update dot keeps pulsing under reduced motion (matches prior behavior);
the donate button opts out via `motion-reduce:[animation:none]` in JSX. */
@keyframes version-dot-pulse {
0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--chrome-accent, #b8bb26) 55%, transparent); }
50% { box-shadow: 0 0 0 4px color-mix(in srgb, var(--chrome-accent, #b8bb26) 0%, transparent); }
}
@keyframes heart-glow {
0%, 100% { opacity: 0.7; transform: scale(1); }
50% { opacity: 1; transform: scale(1.08); }
}
/* Donation-moment popover (DonateMomentPopover.jsx): Clippy-esque bounce-in,
collapsing to a plain fade under prefers-reduced-motion (the JSX swaps the
animation via motion-reduce:). donate-heart-pulse is the gentle attention
pulse the heart runs only while the popover is open. */
@keyframes donate-pop-in {
0% { opacity: 0; transform: translateY(10px) scale(0.9); }
60% { opacity: 1; transform: translateY(-3px) scale(1.03); }
100% { opacity: 1; transform: translateY(0) scale(1); }
}
@keyframes donate-fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes donate-heart-pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 1; transform: scale(1.22); }
}
/* ═══ from src/components/CaptureWidget.css ═══ */
/* ── Capture Pill ────────────────────────────────────────────────────────
Floating dictation indicator.
Slim horizontal pill: dot | label | timer | dismiss
──────────────────────────────────────────────────────────────────────── */
.capture-pill {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 16px;
min-width: 180px;
/* Wide enough for the waveform + status labels ("Downloading voice
model… 42%") and the Accessibility action button. */
max-width: 340px;
height: 48px;
background: rgba(18, 18, 22, 0.88);
backdrop-filter: blur(24px) saturate(180%);
-webkit-backdrop-filter: blur(24px) saturate(180%);
border: 1px solid transparent;
border-radius: 100px;
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.4),
0 2px 8px rgba(0, 0, 0, 0.2),
inset 0 1px 0 rgba(255, 255, 255, 0.05);
font-family: "Inter Variable", "Inter", -apple-system, sans-serif;
font-size: 13px;
color: rgba(255, 255, 255, 0.9);
animation: pill-slide-in 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) both;
user-select: none;
cursor: default;
overflow: hidden;
white-space: nowrap;
}
/* ── State-specific borders ─────────────────────────────────────────── */
.capture-pill--recording {
border-color: rgba(239, 68, 68, 0.3);
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.4),
0 0 20px rgba(239, 68, 68, 0.1),
inset 0 1px 0 rgba(255, 255, 255, 0.05);
}
.capture-pill--transcribing {
border-color: rgba(99, 102, 241, 0.3);
box-shadow:
0 8px 32px rgba(0, 0, 0, 0.4),
0 0 20px rgba(99, 102, 241, 0.1),
inset 0 1px 0 rgba(255, 255, 255, 0.05);
}
.capture-pill--done {
border-color: rgba(34, 197, 94, 0.3);
}
.capture-pill--error {
border-color: rgba(239, 68, 68, 0.3);
}
.capture-pill--setup {
border-color: rgba(245, 158, 11, 0.35);
}
/* ── Widget-window shadow (standalone Tauri window) ───────────────────
The `widget` window (tauri.conf.json, label `widget`) is exactly
300x64, `transparent: true`, `decorations: false`. Its body gets a
flush `padding: 8px` on every edge (see `body:has(.capture-pill)`
below) around the 48px-tall pill, plus `overflow: hidden` — so there
is only an 8px gutter on every side for anything painted outside the
pill's box before the clip kicks in.
The shadows above need real clearance (`0 8px 32px` alone wants ~40px)
that only the main window (`.capture-pill-host`, ~line 2236) has. Here
they were hard-clipped into a straight edge at the window boundary —
a rounded capsule sitting inside a hard-edged dark rectangle instead of
floating free. The shadow is decorative, so shrink it to actually fit
rather than let it clip: every non-inset layer must keep
`|y-offset| + blur + spread <= 8` (the gutter), which bounds every
side since padding is symmetric and every offset-x here is 0. Applies
to every state that sets its own box-shadow, not just the base pill,
otherwise the clip reappears the moment recording/transcribing starts.
Also caps `max-width` to the 300px window's 284px content box
(300 - 8px padding * 2) — the shared 340px value is wider than the
window itself. */
html[data-window='widget'] .capture-pill {
max-width: 284px;
box-shadow:
0 1px 3px rgba(0, 0, 0, 0.35), /* 1 + 3 + 0 = 4 <= 8 */
0 1px 2px rgba(0, 0, 0, 0.25), /* 1 + 2 + 0 = 3 <= 8 */
inset 0 1px 0 rgba(255, 255, 255, 0.05);
}
html[data-window='widget'] .capture-pill--recording {
box-shadow:
0 1px 3px rgba(0, 0, 0, 0.35), /* 1 + 3 + 0 = 4 <= 8 */
0 0 6px rgba(239, 68, 68, 0.1), /* 0 + 6 + 0 = 6 <= 8 */
inset 0 1px 0 rgba(255, 255, 255, 0.05);
}
html[data-window='widget'] .capture-pill--transcribing {
box-shadow:
0 1px 3px rgba(0, 0, 0, 0.35), /* 1 + 3 + 0 = 4 <= 8 */
0 0 6px rgba(99, 102, 241, 0.1), /* 0 + 6 + 0 = 6 <= 8 */
inset 0 1px 0 rgba(255, 255, 255, 0.05);
}
/* ── Animations ─────────────────────────────────────────────────────── */
@keyframes pill-slide-in {
from {
opacity: 0;
transform: translateY(8px) scale(0.92);
filter: blur(4px);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
filter: blur(0);
}
}
/* ── Status dot ─────────────────────────────────────────────────────── */
.capture-pill__dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
background: rgba(255, 255, 255, 0.3);
transition: background 0.2s ease;
}
.capture-pill--recording .capture-pill__dot {
background: #ef4444;
animation: dot-pulse 1.2s ease-in-out infinite;
}
.capture-pill--transcribing .capture-pill__dot {
background: #6366f1;
animation: dot-pulse 1.5s ease-in-out infinite;
}
.capture-pill--done .capture-pill__dot {
background: #22c55e;
animation: none;
}
.capture-pill--error .capture-pill__dot {
background: #ef4444;
animation: none;
}
.capture-pill--setup .capture-pill__dot {
background: #f59e0b;
animation: none;
}
@keyframes dot-pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.4; transform: scale(0.6); }
}
/* ── Live waveform (recording) ──────────────────────────────────────────
Data-driven bars — heights come from the captureWaveform ring buffer
(client-side RMS of the mic worklet frames), not a CSS animation, so the
movement is the actual signal. */
.capture-pill__wave {
display: flex;
align-items: center;
gap: 2px;
width: 44px;
height: 20px;
flex-shrink: 0;
}
.capture-pill__wave-bar {
flex: 1 1 0;
min-height: 2px;
border-radius: 1px;
background: #ef4444;
transition: height 60ms linear;
}
/* ── Content / timer / dismiss ──────────────────────────────────────────
Layout + (theme-less, hardcoded-white) colors for the pill's content,
label, timer, transcribing spinner, and dismiss button now live as
Tailwind utilities on the JSX (the spinner uses `motion-safe:animate-spin`
so it self-disables under reduced motion). Only the irreducible glass
shell, state borders, and keyframe-driven dot/enter animations remain. */
/* ── Reduced motion ─────────────────────────────────────────────────── */
@media (prefers-reduced-motion: reduce) {
.capture-pill { animation: none; opacity: 1; }
.capture-pill__dot { animation: none !important; }
.capture-pill__wave-bar { transition: none; }
}
/* ── Widget-mode body (standalone Tauri window) ─────────────────────── */
html[data-window='widget'] body:has(.capture-pill) {
background: transparent !important;
margin: 0;
padding: 8px;
overflow: hidden;
}
/* ═══ from src/components/CompareModal.css ═══ */
/* CompareModal — non-modal bottom drawer. Leaves the app interactive;
no backdrop wash. ESC or click-outside dismisses. Mirrors
ExportModal's drawer chrome so the two read as siblings.
Mechanical layout/typography rules now live as Tailwind utilities on the
JSX (including the two-column compare grid base). What stays here is the
irreducible non-modal drawer presentation: fixed overlay positioning + the
sheet chrome + slide-up keyframe. The `.ui-compare__grid` responsive
collapse to one column on narrow widths is owned by index.css. */
.compare-drawer {
position: fixed;
left: 0;
right: 0;
/* Above the footer AND the global audio dock (0px while the player is idle
— GlobalAudioPlayer publishes --audio-dock-height like the footer does). */
bottom: calc(var(--logs-footer-height, 28px) + var(--audio-dock-height, 0px));
z-index: 90;
display: flex;
justify-content: center;
pointer-events: none;
}
.compare-drawer__sheet {
pointer-events: auto;
width: min(880px, calc(100vw - 24px));
max-height: min(80vh, 640px);
display: flex;
flex-direction: column;
background: var(--chrome-bg);
border: 1px solid var(--chrome-border-strong);
border-bottom: none;
border-radius: 8px 8px 0 0;
box-shadow:
0 -8px 24px rgba(0, 0, 0, 0.45),
0 -1px 0 var(--chrome-border) inset;
animation: compare-drawer-slide-up var(--dur-base, 180ms) var(--ease-out, cubic-bezier(.2,.8,.2,1));
overflow: hidden;
}
@keyframes compare-drawer-slide-up {
from { transform: translateY(100%); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
/* ═══ from src/components/FloatingPill.css ═══ */
/* ── Floating Status Pill ───────────────────────────────────────────────
Always-on-top pill that tracks long-running operations (ASR model load,
dubbing progress, export).
─────────────────────────────────────────────────────────────────────── */
.floating-pill {
position: fixed;
/* Bottom-center (snackbar) so it never collides with the right-side
workspace panels (Voice / Dub), the top title bar, or the nav rail.
Rides above the LogsFooter at any expanded height (the footer writes
--logs-footer-height on :root — see LogsFooter.jsx) and above the global
audio dock (GlobalAudioPlayer writes --audio-dock-height the same way). */
bottom: calc(var(--logs-footer-height, 28px) + var(--audio-dock-height, 0px) + 16px);
left: 50%;
transform: translateX(-50%);
z-index: var(--z-toast);
display: flex;
align-items: center;
gap: var(--space-4);
padding: var(--space-3) var(--space-5) var(--space-3) var(--space-4);
min-width: 220px;
max-width: 360px;
background: var(--color-bg-elev-1);
backdrop-filter: var(--glass-blur-md);
-webkit-backdrop-filter: var(--glass-blur-md);
border: 1px solid var(--color-border-strong);
border-radius: var(--radius-pill);
box-shadow: var(--shadow-lg), 0 0 0 1px rgba(0,0,0,0.15);
font-family: var(--font-ui);
font-size: var(--text-sm);
color: var(--color-fg);
/* Slide-in animation */
animation: pill-enter 0.35s var(--ease-spring) both;
pointer-events: auto;
user-select: none;
cursor: default;
}
.floating-pill--exiting {
animation: pill-exit 0.25s var(--ease-out) both;
}
@keyframes pill-enter {
from {
opacity: 0;
transform: translate(-50%, 14px) scale(0.92);
filter: blur(4px);
}
to {
opacity: 1;
transform: translate(-50%, 0) scale(1);
filter: blur(0);
}
}
@keyframes pill-exit {
to {
opacity: 0;
transform: translate(-50%, 10px) scale(0.95);
filter: blur(2px);
}
}
@media (prefers-reduced-motion: reduce) {
.floating-pill { animation: none; opacity: 1; }
.floating-pill--exiting { animation: none; opacity: 0; }
/* 10x P4 (spec §3): pulse/sweep loops die too — the dot and the
indeterminate bar hold a static frame. */
.floating-pill__dot { animation: none; }
.floating-pill__progress-fill--indeterminate { animation: none; transform: none; }
}
/* ── Stage indicator dot ────────────────────────────────────────────── */
.floating-pill__dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
animation: pill-dot-pulse 1.5s ease-in-out infinite;
}
.floating-pill__dot--loading-model { background: var(--color-accent); }
.floating-pill__dot--transcribing { background: var(--color-info); }
.floating-pill__dot--translating { background: var(--color-brand); }
.floating-pill__dot--generating { background: var(--color-warn); }
.floating-pill__dot--exporting { background: var(--color-success); }
.floating-pill__dot--refining { background: var(--color-info); }
.floating-pill__dot--recording { background: var(--color-danger); }
.floating-pill__dot--done { background: var(--color-success); animation: none; }
.floating-pill__dot--error { background: var(--color-danger); animation: none; }
@keyframes pill-dot-pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.4; transform: scale(0.7); }
}
/* ── Content area ───────────────────────────────────────────────────── */
/* .floating-pill__content / __label / __meta / __timer / __error layout
moved to Tailwind utilities on the JSX. The base __label rule lives there
too; the unlayered `.floating-pill--done .floating-pill__label` override
below still wins (utilities are low-priority @layer). */
/* ── Mini progress bar ──────────────────────────────────────────────── */
/* .floating-pill__progress (track) moved to Tailwind utilities on the JSX.
The fill (base + indeterminate) stays here — it has !important + animation. */
.floating-pill__progress-fill {
height: 100%;
border-radius: 2px;
background: var(--color-brand);
transition: width 0.3s var(--ease-out);
}
.floating-pill__progress-fill--indeterminate {
width: 40% !important;
animation: pill-progress-sweep 1.4s ease-in-out infinite;
}
@keyframes pill-progress-sweep {
0% { transform: translateX(-100%); }
100% { transform: translateX(300%); }
}
/* ── Dismiss button ─────────────────────────────────────────────────── */
/* .floating-pill__dismiss + :hover moved to Tailwind utilities on the JSX. */
/* ── Done stage — green tint ────────────────────────────────────────── */
.floating-pill--done {
border-color: rgba(142, 192, 124, 0.25);
}
.floating-pill--done .floating-pill__label {
color: var(--color-success);
}
/* ── Error stage — red tint ─────────────────────────────────────────── */
.floating-pill--error {
border-color: rgba(251, 73, 52, 0.25);
}
/* ═══ from src/components/DubSegmentRow.css ═══ */
/* ═══ DubSegmentRow extracted layout styles ═══
Per-cell badge/label/time-span layout now lives in Tailwind utilities on
DubSegmentRow.jsx. Kept here: the row + its generation/selection/playhead
state classes (shared with DubSegmentTable.css + index.css, !important,
compound selectors), the text inputs (font:inherit + focus rings), and the
select/range/actions cells whose `input-base` / `input[type=range]` siblings
are unlayered and would otherwise beat utilities in the cascade. */
/* Row transition — smooth bg/border changes during generation */
.segment-row {
transition: background 0.2s ease, border-left 0.15s ease, opacity 0.3s ease;
border-left: 2px solid transparent;
}
.segment-active {
/* Background comes from index.css's .segment-row.segment-active block
(var(--chrome-accent-bg)) — only the border edge is owned here. */
border-left: 2px solid var(--chrome-accent) !important;
}
.segment-done {
opacity: 0.6;
border-left: 2px solid rgba(142, 192, 124, 0.35) !important;
}
.segment-selected {
background: color-mix(in srgb, var(--chrome-accent) 5%, transparent) !important;
}
/* Row containing the current media playhead — distinct from segment-active
(which fires during dub *generation*) so users can tell at a glance
where the video is playing while editing. */
.segment-row.segment-playing {
background: rgba(131, 165, 152, 0.10) !important;
box-shadow: inset 2px 0 0 #83a598;
}
.segment-row.segment-playing.segment-active {
/* Generation wins visually when both are true. */
box-shadow: inset 2px 0 0 var(--chrome-accent);
}
/* Time + speaker inputs: `font: inherit` and the focus ring don't map to
utilities cleanly, so they stay here. */
.seg-time-input {
background: transparent;
border: none;
padding: 0;
font: inherit;
font-size: 0.66rem;
color: var(--chrome-fg);
width: 36px;
text-align: right;
min-width: 0;
}
.seg-time-input:focus {
outline: 1px solid var(--chrome-accent);
outline-offset: 1px;
background: var(--chrome-hover-bg);
}
.seg-time-stepper {
display: grid;
grid-template-columns: 18px 42px 18px;
align-items: center;
gap: 2px;
}
.seg-time-stepper button {
width: 18px;
height: 18px;
padding: 0;
border: 1px solid var(--chrome-border);
border-radius: 3px;
background: var(--chrome-surface-raised);
color: var(--chrome-fg-muted);
display: inline-flex;
align-items: center;
justify-content: center;
}
.seg-time-stepper button:hover:not(:disabled) {
color: var(--chrome-fg);
border-color: var(--chrome-accent);
}
.seg-overlap-warning {
display: flex;
align-items: center;
gap: 2px;
color: var(--color-danger, #fb4934);
font-size: 0.5rem;
line-height: 1.1;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.seg-speaker-input {
width: 100%;
min-width: 0;
background: transparent;
border: none;
padding: 0;
font-size: 0.66rem;
color: var(--chrome-fg-muted);
text-align: left;
overflow: hidden;
text-overflow: ellipsis;
}
.seg-speaker-input:focus {
outline: 1px solid var(--chrome-accent);
outline-offset: 1px;
background: var(--chrome-hover-bg);
color: var(--chrome-fg);
}
/* Descendant override of the shared (unlayered) .segment-input — must stay
unlayered to win over index.css. */
.seg-text-col {
display: flex; flex-direction: column; gap: 1px;
min-width: 0; overflow: hidden;
}
.seg-text-col .segment-input {
width: 100%; min-width: 0;
}
/* Selects carry `input-base` (unlayered) — these size/padding overrides must
stay unlayered too, else input-base would win over a utilities-layer class. */
.seg-lang-select {
width: 100%; min-width: 0; font-size: 0.6rem; padding: 1px 2px;
}
.seg-profile-select {
width: 100%; min-width: 0; font-size: 0.6rem; padding: 1px 4px;
overflow: hidden; text-overflow: ellipsis;
}
/* Voice column now hosts the shared <VoiceSelector> (#1220). The grid cell must
let the picker shrink to the (narrow) track instead of forcing overflow. */
.seg-voice-col { display: flex; min-width: 0; width: 100%; }
.seg-voice-col .voice-selector { width: 100%; min-width: 0; }
/* Range slider — overrides the unlayered input[type=range] base. */
.seg-gain-slider {
width: 100%; min-width: 0; padding: 0; margin: 0;
height: 3px;
}
.seg-actions {
display: flex; gap: 2px; justify-content: flex-end;
min-width: 0;
}
.seg-actions > button {
width: 22px; height: 22px;
display: inline-flex; align-items: center; justify-content: center;
}
/* Live dub preview (opt-in, /ws/tts as-you-edit streaming) */
.dub-live-toggle {
display: inline-flex; align-items: center; gap: 4px;
font-size: 0.62rem; color: var(--chrome-fg-muted);
cursor: pointer; white-space: nowrap; user-select: none;
}
.dub-live-toggle:hover { color: var(--chrome-fg); }
.seg-live-btn {
width: 18px; height: 18px; flex-shrink: 0;
display: inline-flex; align-items: center; justify-content: center;
background: transparent; border: 1px solid var(--chrome-border);
color: var(--chrome-fg-muted); border-radius: var(--chrome-radius-pill);
cursor: pointer; padding: 0;
transition: color var(--dur-fast), border-color var(--dur-fast);
}
.seg-live-btn:hover { color: var(--chrome-fg); border-color: var(--chrome-border-strong); }
.seg-live-btn--on {
color: var(--color-brand);
border-color: var(--color-brand);
animation: seg-live-pulse 1.1s ease-in-out infinite;
}
@keyframes seg-live-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.35; }
}
/* Row selected from the waveform timeline (#280, item 3) — distinct from
multi-select (checkbox) and from the playhead row. */
.segment-row.segment-timeline-selected {
background: rgba(250, 189, 47, 0.08) !important;
box-shadow: inset 2px 0 0 #fabd2f;
}
/* ═══ from src/components/DubSegmentTable.css ═══ */
/* Table body specific styles — chrome comes from ui/Table. */
/* A row owns its layout at every panel width. react-window measures the
resulting height, including wrapped controls and optional status badges. */
.segment-table .segment-row {
display: grid;
grid-template-columns: 20px minmax(0, 1fr);
align-items: start;
gap: 8px;
padding: 12px 8px;
border-bottom: 1px solid var(--chrome-border);
}
.segment-table .segment-done { opacity: 1; }
.segment-table .seg-select { grid-column: 1; margin-top: 10px; }
.segment-table .seg-text-col { grid-column: 2; gap: 4px; }
.segment-table .seg-text-col textarea.segment-input {
resize: none;
height: 48px;
font-size: 13px !important;
line-height: 1.6;
color: var(--chrome-fg);
background: var(--chrome-hover-bg);
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.segment-table .seg-original { font-size: 11px; color: var(--chrome-fg-muted); }
.segment-table .seg-original > span:first-child { font-size: 10px; color: inherit; }
.segment-table .seg-original > button { flex-shrink: 0; width: 24px; height: 24px; font-size: 14px; }
.segment-table .segment-time {
grid-column: 2;
display: flex;
flex-flow: row wrap;
align-items: center;
gap: 6px 12px;
overflow: visible;
}
.segment-table .segment-time > span { font-size: 11px; }
.segment-table .seg-status {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 3px 6px;
border-radius: 5px;
background: color-mix(in srgb, currentColor 8%, transparent);
}
.segment-table .seg-status > svg { width: 12px; height: 12px; flex-shrink: 0; }
.segment-table .seg-plan-apply {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 3px 6px;
border: 0;
border-radius: 4px;
background: var(--chrome-hover-bg);
color: var(--chrome-accent);
font-size: 11px;
cursor: pointer;
}
.segment-table .seg-time-stepper {
flex: 0 0 auto;
grid-template-columns: 24px 60px 24px;
gap: 2px;
}
.segment-table .seg-time-stepper button { width: 24px; height: 26px; }
.segment-table .seg-time-input { width: 100%; font-size: 12px; text-align: center; }
.segment-table .seg-overlap-warning { white-space: normal; }
.segment-table .seg-controls {
grid-column: 2;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
min-width: 0;
}
.segment-table .seg-speaker-input { flex: 1 1 90px; width: 90px; }
.segment-table .seg-lang-select { flex: 0 0 64px; width: 64px; }
.segment-table .seg-voice-col { flex: 2 1 140px; width: 140px; }
.segment-table .seg-gain-slider { flex: 0 0 60px; width: 60px; }
.segment-table .seg-controls .seg-speaker-input,
.segment-table .seg-controls .seg-lang-select,
.segment-table .seg-controls .seg-profile-select {
height: 30px;
padding: 4px 6px;
border-radius: 4px;
font-size: 12px;
background: var(--chrome-hover-bg);
}
.segment-table .seg-actions { flex: 0 0 auto; gap: 4px; margin-left: auto; }
.segment-table .seg-actions > button { width: 30px; height: 30px; flex-shrink: 0; }
.segment-table .dub-segment-table__header {
display: grid;
grid-template-columns: 20px minmax(0, 1fr);
align-items: center;
gap: 8px;
padding: 4px 8px;
}
.dub-segment-table__header .ui-table-header__cell { min-width: 0; }
.dub-segment-table__body { flex: 1; min-height: 0; }
.dub-segment-table__select-all {
display: inline-flex;
align-items: center;
justify-content: center;
}
.dub-segment-table__select-all input[type="checkbox"] {
accent-color: var(--color-brand);
cursor: pointer;
}
.dub-segment-table__spk-filter { min-width: 80px; }
.dub-segment-table__sel-count { color: var(--color-brand); }
/* ═══ from src/components/SegmentTrack.css ═══ */
/* SegmentTrack — custom dub-timeline segment lane (#280, item 3).
Container/onset/lane/label/handle-base/actions/action-btn/playhead layout
now lives in Tailwind utilities on SegmentTrack.jsx. Kept here: the box and
its many JS-toggled state variants, the resize-handle edges (with their
hover/selected compounds), the self-scroll viewport modifier, the disabled
compound, and the visually-hidden announce region — all selectors that are
compound / stateful / pseudo and must stay unlayered to win the cascade. */
/* WebKit fallback: the lane scrolls itself (no WaveSurfer wrapper to sync). */
.seg-track__viewport--scroll {
overflow-x: auto;
overflow-y: hidden;
}
.seg-track__box {
position: absolute;
top: 3px;
height: 34px;
border: 1px solid rgba(168, 153, 132, 0.35);
border-radius: 4px;
box-sizing: border-box;
display: flex;
align-items: center;
overflow: hidden;
cursor: grab;
touch-action: none;
outline: none;
}
.seg-track__box.is-dragging { cursor: grabbing; }
.seg-track__box:focus-visible {
border-color: #d3869b;
box-shadow: 0 0 0 1px #d3869b;
}
.seg-track__box.is-selected {
border-color: #d3869b;
box-shadow: 0 0 0 1px rgba(211, 134, 155, 0.7);
}
.seg-track__box.is-editing {
border-style: dashed;
border-color: #fabd2f;
}
/* Cache state from the incremental plan. */
.seg-track__box.is-stale { border-bottom: 2px solid #fabd2f; }
.seg-track__box.is-fresh { border-bottom: 2px solid rgba(142, 192, 124, 0.8); }
/* Hatched warning when Alt-drag created an overlap (≤200 ms allowed). */
.seg-track__box.is-overlap {
background-image: repeating-linear-gradient(
45deg,
rgba(251, 73, 52, 0.28) 0,
rgba(251, 73, 52, 0.28) 4px,
transparent 4px,
transparent 8px
) !important;
border-color: #fb4934;
}
.seg-track__handle--l { left: 0; border-left: 2px solid rgba(235, 219, 178, 0.45); }
.seg-track__handle--r { right: 0; border-right: 2px solid rgba(235, 219, 178, 0.45); }
.seg-track__box:hover .seg-track__handle--l,
.seg-track__box.is-selected .seg-track__handle--l { border-left-color: #d3869b; }
.seg-track__box:hover .seg-track__handle--r,
.seg-track__box.is-selected .seg-track__handle--r { border-right-color: #d3869b; }
.seg-track.is-disabled .seg-track__box {
cursor: default;
opacity: 0.6;
}
/* Visually hidden, still announced by screen readers. */
.seg-track__sr-announce {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
border: 0;
clip: rect(0 0 0 0);
clip-path: inset(50%);
overflow: hidden;
white-space: nowrap;
}
/* ═══ from src/components/dub/dub.css ═══ */
/* Dub Studio — irreducible CSS that can't live as Tailwind utilities:
keyframe-driven motion (stepper spin, idle-drop pulse, skeleton shimmer),
::before connectors, and a few rules that must override other *global*
design-system classes (.studio-panel / .label-row / .override-toggle /
.segment-del) where a utility would lose on cascade. Everything else in the
dub feature is plain Tailwind utilities on the components. */
/* ── Title strip — coexists with the global .label-row it sits on ── */
.dub-head__title {
display: flex;
align-items: center;
gap: var(--space-3);
margin-bottom: 0;
min-width: 0;
flex: 1;
}
/* One compact hierarchy for project identity, pipeline position, and the
primary batch action. */
.dub-command-bar {
display: grid;
grid-template-columns: minmax(190px, 1fr) auto minmax(max-content, 1fr);
align-items: center;
gap: 10px;
min-width: 0;
min-height: 44px;
flex: 0 0 auto;
margin-bottom: 2px;
padding: 5px 7px;
border-radius: 8px;
background: transparent;
}
.dub-command-bar__identity {
display: flex;
align-items: center;
gap: 7px;
min-width: 0;
}
.dub-command-bar__file {
display: inline-flex;
align-items: center;
justify-content: center;
width: 26px;
height: 26px;
flex: 0 0 auto;
border-radius: 7px;
color: var(--chrome-accent);
background: transparent;
}
.dub-command-bar__title {
overflow: hidden;
color: var(--chrome-fg);
font-size: 0.73rem;
font-weight: 600;
line-height: 1.15;
text-overflow: ellipsis;
white-space: nowrap;
}
.dub-command-bar__meta {
display: flex;
align-items: center;
gap: 4px;
min-width: 0;
margin-top: 2px;
color: var(--chrome-fg-dim);
font-family: var(--chrome-font-mono);
font-size: 0.58rem;
font-variant-numeric: tabular-nums;
line-height: 1;
}
.dub-command-bar__project {
overflow: hidden;
color: var(--chrome-severity-ok);
text-overflow: ellipsis;
white-space: nowrap;
}
.dub-command-bar__actions,
.dub-command-bar__utilities {
display: flex;
align-items: center;
gap: 4px;
}
.dub-command-bar__actions { justify-content: flex-end; min-width: 0; }
.dub-command-bar__actions button {
background: transparent;
box-shadow: none;
}
.dub-command-bar__actions button:focus-visible {
outline: 2px solid var(--chrome-accent);
outline-offset: 2px;
}
.dub-command-bar__utilities {
margin-right: 2px;
padding-right: 6px;
border-right: 1px solid var(--chrome-border);
}
.dub-command-bar__primary {
flex: 0 0 auto;
min-height: 28px;
padding-inline: 11px !important;
font-size: 0.66rem !important;
font-weight: 600;
}
/* ── Column flex that fills height — overrides .studio-panel overflow ── */
.dub-panel-col {
margin-bottom: 0;
overflow: hidden;
display: flex;
flex-direction: column;
}
/* ── Pipeline stepper (top spine) ─────────────────────────────────────── */
.dub-stepper {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
gap: 2px 0;
padding: 8px 12px;
flex: 0 0 auto;
border-bottom: 1px solid var(--chrome-border);
}
.dub-stepper__step {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 0.7rem;
font-weight: 500;
white-space: nowrap;
color: var(--chrome-fg-dim, #665c54);
}
.dub-stepper__action {
display: inline-flex;
align-items: center;
gap: inherit;
padding: 2px;
border: 0;
border-radius: 6px;
color: inherit;
background: transparent;
font: inherit;
}
button.dub-stepper__action {
cursor: pointer;
touch-action: manipulation;
}
button.dub-stepper__action:hover {
color: var(--chrome-fg);
background: var(--chrome-hover-bg, rgba(255, 255, 255, 0.06));
}
button.dub-stepper__action:focus-visible {
outline: 2px solid var(--chrome-accent, #d3869b);
outline-offset: 2px;
}
.dub-stepper__step:not(:first-child)::before {
content: '';
width: 24px;
height: 2px;
margin: 0 8px;
border-radius: 1px;
background: var(--chrome-border-strong, rgba(255, 255, 255, 0.12));
}
.dub-stepper__step.is-reached:not(:first-child)::before {
background: var(--chrome-accent, #d3869b);
}
.dub-stepper__icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
flex: 0 0 auto;
border-radius: 50%;
border: 1.5px solid var(--chrome-border-strong, transparent);
color: var(--chrome-fg-dim, #665c54);
}
.dub-stepper__step.is-done .dub-stepper__icon {
border-color: var(--chrome-accent, #d3869b);
color: var(--chrome-accent, #d3869b);
background: color-mix(in srgb, var(--chrome-accent, #d3869b) 16%, transparent);
}
.dub-stepper__step.is-active .dub-stepper__icon {
border-color: var(--chrome-accent, #d3869b);
color: var(--chrome-accent, #d3869b);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--chrome-accent, #d3869b) 14%, transparent);
}
.dub-stepper__step.is-done .dub-stepper__label,
.dub-stepper__step.is-active .dub-stepper__label {
color: var(--chrome-fg);
}
.dub-stepper__spin { animation: dub-stepper-spin 1s linear infinite; }
@keyframes dub-stepper-spin { to { transform: rotate(360deg); } }
/* ── Inline variant — sits on the DubHeader row alongside title + actions ── */
.dub-stepper--inline {
padding: 0;
border-bottom: none;
justify-content: flex-start;
flex: 0 1 auto;
min-width: 0;
}
/* Tighter footprint when inlined on the header row so the stages, title, and
actions all fit before wrapping. */
.dub-stepper--inline .dub-stepper__step {
gap: 5px;
font-size: 0.66rem;
}
.dub-stepper--inline .dub-stepper__icon {
width: 19px;
height: 19px;
}
.dub-stepper--inline .dub-stepper__step:not(:first-child)::before {
width: 10px;
margin: 0 4px;
}
/* The active stage owns the emphasis; other stages remain quiet and dense. */
.dub-stepper--command {
gap: 1px;
padding: 3px;
border-radius: 7px;
background: transparent;
}
.dub-stepper--command .dub-stepper__step { gap: 0; font-size: 0.6rem; }
.dub-stepper--command .dub-stepper__step:not(:first-child)::before { display: none; }
.dub-stepper--command .dub-stepper__action {
gap: 4px;
min-height: 24px;
padding: 3px 6px;
border-radius: 5px;
}
.dub-stepper--command button.dub-stepper__action:hover { background: transparent; }
.dub-stepper--command .dub-stepper__icon {
width: 14px;
height: 14px;
border: 0;
border-radius: 0;
}
.dub-stepper--command .dub-stepper__icon svg { width: 11px; height: 11px; }
.dub-stepper--command .dub-stepper__step.is-done .dub-stepper__icon {
color: var(--chrome-severity-ok);
background: transparent;
}
.dub-stepper--command .dub-stepper__step.is-active .dub-stepper__action {
color: var(--chrome-accent);
background: transparent;
text-decoration: underline;
text-underline-offset: 4px;
}
.dub-stepper--command .dub-stepper__step.is-active .dub-stepper__icon {
border: 0;
color: var(--chrome-accent);
box-shadow: none;
}
.shell-narrow .dub-command-bar,
.shell-mini .dub-command-bar {
grid-template-columns: minmax(0, 1fr) max-content;
gap: 5px 8px;
}
.shell-narrow .dub-stepper--command,
.shell-mini .dub-stepper--command {
grid-column: 1 / -1;
grid-row: 2;
justify-content: stretch;
}
.shell-narrow .dub-stepper--command .dub-stepper__step,
.shell-mini .dub-stepper--command .dub-stepper__step { flex: 1 1 auto; justify-content: center; }
.shell-mini .dub-stepper--command .dub-stepper__step:not(.is-active) .dub-stepper__label {
display: none;
}
/* ── Idle / drop-zone ─────────────────────────────────────────────────── */
.dub-idle-drop {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
cursor: pointer;
border: 2px dashed transparent;
border-radius: 8px;
transition: background 0.3s, border-color 0.3s;
margin: 2px;
background: transparent;
}
@keyframes dub-pulse {
0%, 100% { border-color: rgba(211, 134, 155, 0.3); background: rgba(211, 134, 155, 0.04); }
50% { border-color: rgba(211, 134, 155, 0.6); background: rgba(211, 134, 155, 0.08); }
}
.dub-idle-drop.is-dragging {
border-color: #d3869b;
background: rgba(211, 134, 155, 0.05);
animation: dub-pulse 1.2s ease-in-out infinite;
}
.dub-idle-drop__puck {
width: 60px; height: 60px; border-radius: 50%;
display: flex; align-items: center; justify-content: center;
background: rgba(211, 134, 155, 0.06);
border: 1px solid rgba(211, 134, 155, 0.1);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.dub-idle-drop:hover .dub-idle-drop__puck {
transform: scale(1.08);
box-shadow: 0 0 24px rgba(211, 134, 155, 0.15);
}
.dub-ingest-row__cta {
padding: 3px 10px;
border-radius: 4px;
font-size: 0.7rem;
background: rgba(255, 255, 255, 0.03);
border: 1px solid transparent;
color: #665c54;
cursor: default;
}
.dub-ingest-row__cta.is-ready {
background: rgba(211, 134, 155, 0.15);
border-color: rgba(211, 134, 155, 0.3);
color: var(--color-brand);
cursor: pointer;
}
/* ── Landing options — descendants override global element styling ── */
.dub-landing-opts__lang select { max-width: 220px; }
.dub-landing-adv__field--grow { flex: 1; min-width: 200px; }
.dub-landing-adv__field--grow input { flex: 1; }
/* ── Transcript toggle — sits on the global .override-toggle ── */
.dub-transcript-toggle__inner {
margin-top: 0;
padding: 3px var(--space-3);
font-family: var(--chrome-font-mono);
font-size: var(--chrome-label-size);
color: var(--chrome-fg-muted);
letter-spacing: var(--chrome-label-track);
text-transform: uppercase;
background: var(--chrome-bg);
border: 1px solid var(--chrome-border);
border-radius: var(--chrome-radius-pill);
}
.dub-transcript-toggle__inner:hover { color: var(--chrome-fg); }
.dub-skel-transcript-toggle__inner { margin-top: 0; padding: 2px 6px; font-size: 0.65rem; opacity: 0.3; cursor: default; }
/* ── Prep / Transcribe overlays ───────────────────────────────────────── */
.dub-prep-overlay--large {
flex: 1;
justify-content: center;
gap: var(--space-6);
padding: var(--space-5);
margin: var(--space-2);
border: 2px dashed rgba(211, 134, 155, 0.2);
border-radius: var(--radius-xl);
background: rgba(211, 134, 155, 0.03);
}
.dub-prep-bar {
width: min(240px, 85vw);
height: 4px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.06);
overflow: hidden;
}
.dub-prep-overlay--large .dub-prep-bar { width: 320px; height: 5px; }
.dub-prep-bar__fill {
height: 100%;
background: linear-gradient(90deg, rgba(211, 134, 155, 0.75), rgba(211, 134, 155, 1));
border-radius: inherit;
transition: width 220ms ease-out;
}
.dub-prep-chip {
padding: 1px 6px;
border-radius: var(--radius-xs);
background: rgba(255, 255, 255, 0.03);
border: 1px solid transparent;
color: var(--color-fg-subtle);
}
.dub-prep-chip.is-active {
background: rgba(211, 134, 155, 0.20);
border-color: rgba(211, 134, 155, 0.30);
color: var(--color-brand);
}
.dub-prep-chip.is-active.is-cached {
background: rgba(142, 192, 124, 0.20);
border-color: rgba(142, 192, 124, 0.30);
color: var(--color-success);
}
.dub-prep-chips--lg .dub-prep-chip { padding: 2px 8px; border-radius: var(--radius-sm); font-weight: var(--weight-regular); }
.dub-prep-chips--lg .dub-prep-chip.is-active { font-weight: var(--weight-semibold); }
/* ── Cast strip — --muted overrides descend onto __label/__kicker ── */
.dub-cast__pair { display: flex; align-items: center; gap: 3px; padding: 1px 5px; border-radius: var(--radius-sm); transition: background 0.2s ease; }
.dub-cast__pair:hover { background: rgba(255, 255, 255, 0.03); }
.dub-cast__select { width: 95px; padding: 1px 3px; font-size: 0.60rem; }
.dub-cast--muted { background: transparent; border-color: var(--chrome-border); }
.dub-cast--muted .dub-cast__label,
.dub-cast--muted .dub-cast__kicker { color: var(--chrome-fg-dim); }
/* ── Casting board — drag voice chips onto speaker rows ──────────────────── */
.dub-cast__board-toggle {
display: inline-flex; align-items: center; gap: 3px; margin-left: auto;
padding: 1px 7px; font-size: 0.58rem; font-weight: var(--weight-semibold);
color: var(--chrome-fg-muted); background: transparent;
border: 1px solid var(--chrome-border); border-radius: var(--radius-pill);
cursor: pointer; transition: color 0.15s ease, border-color 0.15s ease;
}
.dub-cast__board-toggle:hover,
.dub-cast__board-toggle.is-open { color: var(--color-brand); border-color: var(--color-brand); }
.dub-cast__board { display: grid; gap: 12px; margin-top: 12px; padding: 0; min-width: 0; }
.dub-cast__palette { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 150px), 1fr)); gap: 6px; margin: 0; padding: 0; list-style: none; }
.dub-cast__chip { display: flex; align-items: center; gap: 10px; min-width: 0; min-height: 52px; padding: 8px 10px; font-size: 12px; color: var(--chrome-fg); background: var(--chrome-hover-bg); border: 1px solid transparent; border-radius: 8px; cursor: grab; user-select: none; }
.dub-cast__chip > svg { flex-shrink: 0; color: var(--chrome-accent); }
.dub-cast__chip-text { display: grid; gap: 3px; min-width: 0; }
.dub-cast__chip-text > span { overflow-wrap: anywhere; line-height: 1.4; }
.dub-cast__chip small { color: var(--chrome-fg-muted); font-size: 10px; }
.dub-cast__chip:hover { border-color: var(--chrome-accent); background: var(--chrome-accent-bg); }
.dub-cast__chip:active { cursor: grabbing; }
.dub-cast__hint { margin: 0; font-size: 12px; line-height: 1.5; color: var(--chrome-fg-muted); }
.dub-cast__rows { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 220px), 1fr)); gap: 8px; }
.dub-cast__row { display: grid; align-content: start; gap: 8px; min-width: 0; padding: 10px; border: 1px solid transparent; border-radius: 8px; background: var(--chrome-hover-bg); transition: border-color 0.15s ease, background 0.15s ease; }
.dub-cast__row.is-drop { border-color: var(--chrome-accent); background: var(--chrome-accent-bg); }
.dub-cast__row-speaker { display: flex; align-items: center; gap: 8px; font: 600 12px var(--font-sans); color: var(--chrome-fg); }
.dub-cast__row-select { min-height: 38px; padding: 8px 10px; border: 0; border-radius: 6px; background: var(--color-bg); color: var(--chrome-fg); font-size: 12px; }
.dub-cast__row-select:hover { background: var(--chrome-accent-bg); }
.dub-cast__row-select:focus-visible { outline: 2px solid var(--chrome-accent); outline-offset: 2px; }
.dub-cast__auto-chip { display: inline-flex; align-items: center; gap: 5px; font-size: 11px; color: var(--chrome-fg-muted); }
/* ── Multi-language preview switcher ──────────────────────────────────── */
.dub-lang-switch {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 6px;
padding: 6px 0 8px;
}
.dub-lang-pill {
padding: 5px 14px;
font-size: 0.72rem;
font-weight: 600;
color: var(--chrome-fg-muted);
background: var(--chrome-hover-bg, rgba(255, 255, 255, 0.05));
border: 1px solid var(--chrome-border);
border-radius: var(--chrome-radius-pill, 999px);
cursor: pointer;
transition: background var(--dur-fast), color var(--dur-fast), border-color var(--dur-fast);
}
.dub-lang-pill:hover {
color: var(--chrome-fg);
border-color: var(--chrome-border-strong);
}
.dub-lang-pill.is-active {
color: #1b1b1b;
background: var(--chrome-fg, #ebdbb2);
border-color: var(--chrome-fg, #ebdbb2);
}
.dub-lang-pill:focus-visible {
outline: 2px solid var(--chrome-accent);
outline-offset: 1px;
}
/* ── Skeleton segment rows (shimmer bars) ─────────────────────────────── */
.dub-skel-table { flex: 1; max-height: none; overflow-y: auto; min-height: 0; }
.dub-skel-row { gap: 6px; }
.dub-skel-bar {
height: 11px;
border-radius: 3px;
background: linear-gradient(90deg,
rgba(255, 255, 255, 0.04) 25%,
rgba(255, 255, 255, 0.09) 37%,
rgba(255, 255, 255, 0.04) 63%);
background-size: 400% 100%;
animation: dub-skel-shimmer 1.4s ease-in-out infinite;
}
.dub-skel-cell-acts__icon { opacity: 0.3; }
@keyframes dub-skel-shimmer {
0% { background-position: 100% 0; }
100% { background-position: 0 0; }
}
/* ── Reduced motion: shimmer/pulse die; stepper spinner holds a frame ── */
@media (prefers-reduced-motion: reduce) {
.dub-skel-bar,
.dub-idle-drop.is-dragging,
.dub-stepper__spin {
animation: none;
}
}
/* ═══ from src/components/VoicePreview.css ═══ */
/* VoicePreview — all panel chrome converted to Tailwind utilities on
VoicePreview.jsx. Two things stay here as irreducible CSS:
1. the entrance @keyframes (referenced via animate-[…] on the popover); and
2. the select/text rules that layer on top of the *unlayered* shared
.input-base — Tailwind utilities live in @layer utilities and would lose
the cascade to .input-base, so these overrides must stay unlayered. */
@keyframes voice-preview-in {
from { opacity: 0; transform: translateY(12px) scale(0.96); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
.voice-preview__select {
font-size: 0.78rem;
padding: 6px 8px;
}
.voice-preview__text {
font-size: 0.78rem;
padding: 8px;
resize: none;
line-height: 1.4;
min-height: 48px;
}
/* ═══ from src/components/WaveformErrorBoundary.css ═══ */
/* ═══ WaveformTimeline extracted layout styles ═══ */
.wfm-layout {
display: flex; flex-direction: column; flex: 1; min-height: 0;
}
/* Keep the editing rows together without stretching them into spare height. */
.wfm-stack { position: relative; display: flex; flex-direction: column; gap: 4px; flex: 0 0 auto; min-height: 0; }
.wfm-video-preview {
position: relative;
/* Width-driven: use the full available width and derive height from the
16:9 aspect ratio, capped at 60vh so a very tall parent can't push
the waveform off-screen. Previous `max-height: 45%` was clamping the
height first, which then back-pressured the width (aspect-ratio
preserves shape), leaving empty black space to the right of the
video. For portrait sources the inner <video> uses object-fit:contain
and letterboxes within the 16:9 box. */
flex: 0 0 auto;
width: 100%;
/* Keep the preview useful without letting it push the waveform/segment lane
below the browser viewport. The inner video already uses object-fit:contain,
so a bounded viewport is better than a width-driven 16:9 box here. */
height: clamp(180px, 36vh, 360px);
max-height: 360px;
background: #000; border-radius: 4px; overflow: hidden;
border: 1px solid transparent; display: flex;
}
.wfm-video-media { width: 100%; height: 100%; min-width: 0; }
.wfm-editor {
min-width: 0;
flex: 0 0 auto;
border-radius: 6px;
background: var(--chrome-hover-bg);
}
.wfm-wave-wrap {
position: relative;
overflow: hidden;
min-height: 56px;
z-index: 1;
}
.wfm-wave-inner {
min-height: 56px; width: 100%; overflow: hidden;
}
.seg-track {
flex: 0 0 50px !important;
min-height: 50px !important;
height: 50px;
z-index: 2;
overflow: visible;
background: var(--chrome-bg);
}
.wfm-editor > .seg-track { margin-top: 0; background: transparent; }
.wfm-loading {
position: absolute; inset: 0; display: flex; align-items: center;
justify-content: center; background: rgba(0,0,0,0.45);
border-radius: 4px; z-index: 3; gap: 6px;
}
.wfm-loading__text { font-size: 0.65rem; color: #a89984; }
.wfm-overlay {
position: absolute; inset: 0; border-radius: 4px; z-index: 4;
background: rgba(29,32,33,0.85); backdrop-filter: blur(3px);
display: flex; flex-direction: column; align-items: center;
justify-content: center; gap: 6px; padding: 8px;
}
.wfm-controls {
display: flex; align-items: center; justify-content: space-between;
flex-shrink: 0; flex-wrap: wrap; gap: 6px; margin-top: 3px;
}
.wfm-controls__group { display: flex; align-items: center; gap: 4px; }
.wfm-video-preview .wfm-controls--overlay {
position: absolute; inset: auto 8px 8px; z-index: 2;
margin: 0; padding: 0; background: transparent;
}
.wfm-controls--overlay .wfm-controls__group {
padding: 3px; border-radius: 8px;
background: transparent;
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
color: white;
}
.wfm-controls--overlay button,
.wfm-controls--overlay span {
background: transparent; border-color: transparent; color: inherit;
text-shadow: 0 1px 3px #000, 0 0 6px #000;
}
.wfm-controls--overlay button {
min-width: 32px; min-height: 32px;
}
.wfm-controls--overlay button svg,
.wfm-controls--overlay .wfm-kbd-hint svg { filter: drop-shadow(0 1px 2px #000); }
.wfm-controls--overlay button:hover { background: transparent; color: var(--chrome-accent); }
.wfm-controls--overlay button:focus-visible,
.wfm-controls--overlay input:focus-visible { outline: 2px solid white; outline-offset: 2px; }
/* Keyboard shortcut hint icon */
.wfm-kbd-hint {
color: rgba(168,153,132,0.4);
display: flex; align-items: center;
cursor: help; margin-left: 4px;
}
.wfm-kbd-hint:hover { color: rgba(168,153,132,0.7); }
.wfm-error {
display: flex; align-items: center; justify-content: center;
padding: 8px; background: rgba(0,0,0,0.15); border-radius: 4px;
border: 1px solid transparent; color: #a89984; font-size: 0.7rem;
}
/* NOTE: the ErrorBoundary (`errbnd-*`) styles that used to live here were
migrated to Tailwind utilities on ErrorBoundary.jsx (shadcn fast-mode wave).
Only the WaveformTimeline `wfm-*` layout rules above remain — this file is
still imported by WaveformTimeline.jsx. */
/* ═══ from src/components/WorkspaceHistory.css ═══ */
/* WorkspaceHistory — right-side history panel. Row internals reuse the shared
.history-* classes (defined in Sidebar.css, bundled app-wide); this file only
styles the panel chrome: header, filter chips, scroll, empty state. */
/* Layout wrapper: the workspace (definition column) + the history aside sit
side by side and each scroll independently. Drops to stacked on narrow. */
.studio-with-history {
display: flex;
flex: 1;
min-height: 0;
min-width: 0;
}
.studio-with-history__main {
flex: 1 1 auto;
min-width: 0;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* Voice workspace library: active voice + saved voices occupy their own
full-height rail on the left, leaving generation history alone on the right. */
.studio-voices {
flex: 0 0 280px;
display: flex;
flex-direction: column;
min-height: 0;
border-right: 1px solid var(--chrome-border);
background: var(--chrome-bg);
}
/* Dub projects stay scannable in a compact rail; Dub History keeps the wider
media-preview rail on the opposite side of the ingest workspace. */
.studio-projects {
flex: 0 0 240px;
display: flex;
flex-direction: column;
min-height: 0;
border-right: 1px solid var(--chrome-border);
background: var(--chrome-bg);
}
/* On a narrow shell the workspace + history aside stack vertically. Driven by
the shell-width class set in App.jsx from `app-container.clientWidth`, NOT a
raw `@media (max-width)`: the shell scales via `zoom`, so a viewport media
query fires at the wrong threshold whenever --ui-scale ≠ 1 and dropped the
action bar (Synthesize CTA) below the fold (#476). `.shell-narrow`/`.shell-mini`
track the real scaled layout width on every engine (index.css:294). */
.shell-narrow .studio-with-history,
.shell-mini .studio-with-history {
flex-direction: column;
overflow-y: auto;
}
/* Reserve a viewport-height workspace before the stacked voice library.
The inner form scrolls independently; the action bar remains visible. */
.shell-narrow .studio-with-history__main,
.shell-mini .studio-with-history__main {
overflow: visible;
flex: 1 0 100%;
min-height: 0;
order: 1;
}
.shell-narrow .studio-with-history__main .studio-action-bar,
.shell-mini .studio-with-history__main .studio-action-bar {
position: sticky;
bottom: 0;
z-index: 3;
}
/* Right column: generated voices/history gets the full rail height. */
.studio-right {
flex: 0 0 340px;
display: flex;
flex-direction: column;
min-height: 0;
border-left: 1px solid var(--chrome-border);
background: var(--chrome-bg);
}
/* Dub projects stay compact while media history keeps the wider preview rail. */
.studio-projects {
flex: 0 0 240px;
display: flex;
flex-direction: column;
min-height: 0;
border-right: 1px solid var(--chrome-border);
background: var(--chrome-bg);
}
/* Panel chrome (.wh / .wh__*) moved to Tailwind utilities on WorkspaceHistory.jsx. */
/* On a narrow shell the right column drops below the definition column. Same
shell-class trigger as above (not a viewport media query) so it reflows in
lockstep with the stacked layout at the scaled width (#476). */
.shell-narrow .studio-right,
.shell-mini .studio-right {
flex-basis: auto;
order: 3;
border-left: none;
border-top: 1px solid var(--chrome-border);
max-height: 60vh;
}
.shell-narrow .studio-voices,
.shell-mini .studio-voices,
.shell-narrow .studio-projects,
.shell-mini .studio-projects {
flex: 0 0 auto;
order: 2;
border-right: none;
border-top: 1px solid var(--chrome-border);
max-height: 60vh;
}
/* ═══ from src/components/WorkspaceVoices.css ═══ */
/* WorkspaceVoices — left-side "Saved voices" panel. Row internals reuse the
shared .history-* classes (bundled app-wide); this file styles the panel
chrome only. Lives in the dedicated .studio-voices rail. */
.wv {
flex: 1 1 0;
display: flex;
flex-direction: column;
min-height: 0;
overflow: hidden;
}
/* When the list is empty, shrink to just the header + one-line empty state so
the panel below it (e.g. History) gets the room instead of a big void. */
.wv--collapsed {
flex: 0 0 auto;
}
/* Active-voice card + empty-state CTA (.wv__active*, .wv__empty-cta) converted
to Tailwind utilities on WorkspaceVoices.jsx — these classes were local to
this component, so their rules were removed here. */
.wv__head {
flex: 0 0 auto;
display: flex;
flex-direction: column;
gap: 8px;
padding: 10px 12px;
border-bottom: 1px solid var(--chrome-border);
}
.wv__title {
font-family: var(--chrome-font-mono, var(--font-mono));
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--chrome-fg-muted);
}
.wv__search { position: relative; }
.wv__search-icon {
position: absolute;
top: 50%;
left: 8px;
transform: translateY(-50%);
color: var(--color-fg-subtle);
pointer-events: none;
}
.wv__search-input {
width: 100%;
box-sizing: border-box;
height: 28px;
font-size: 0.78rem;
padding: 4px 8px 4px 24px;
border-radius: var(--chrome-radius-pill, 999px);
}
.wv__scroll {
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 8px;
padding: 8px;
}
.wv__empty {
color: var(--chrome-fg-dim);
font-size: 0.72rem;
line-height: 1.5;
text-align: center;
padding: 24px 16px;
}
/* Inline project rename (Sidebar projects rail) — fills the card row like the title. */
.wv__rename-input {
width: 100%;
font-size: 0.78rem;
font-weight: 600;
padding: 2px 6px;
margin: 1px 0;
}
/* ═══ from src/components/donate/DonateGoal.css ═══ */
/* ════════════════════════════════════════════════════════════════════════
"Support VoiceStudio development" goal bar + Pip mascot (spec 007).
Reuses real design tokens from index.css / tokens.css:
--chrome-*, --space-*, --ease-spring, --font-serif, --chrome-font-mono.
════════════════════════════════════════════════════════════════════════ */
/* .goal root (layout + the --goal-accent default), __head, __title, __pct, and
__track base → Tailwind utilities in GoalBar.jsx. The --goal-accent value is
declared as an arbitrary property on the root there; .goal--met / .goal--mini
overrides + the animated fill/pip/shimmer stay here. */
/* ── Track + fill ─────────────────────────────────────────────────────── */
.goal__fill {
position: absolute;
inset: 0;
width: calc(var(--goal-pct, 0) * 100%);
min-width: 6px;
border-radius: 999px;
background: linear-gradient(
90deg,
color-mix(in srgb, var(--goal-accent) 70%, #d3869b),
var(--goal-accent)
);
box-shadow: 0 0 12px -2px color-mix(in srgb, var(--goal-accent) 60%, transparent);
overflow: hidden;
/* grow-in once on mount */
animation: goalFill 1s var(--ease-spring) both;
}
@keyframes goalFill {
0% { width: 0; }
100% { width: calc(var(--goal-pct, 0) * 100%); }
}
/* ONE shimmer pass over the fill (token @keyframes shimmer in index.css). */
.goal__shimmer {
position: absolute;
inset: 0;
background: linear-gradient(
90deg,
transparent 0%,
rgba(255, 255, 255, 0.35) 50%,
transparent 100%
);
background-size: 200% 100%;
/* iteration-count: 1 → a single sweep, not a loop. */
animation: shimmer 1.4s ease-out 0.6s 1 both;
}
/* ── Pip perched on the fill ──────────────────────────────────────────── */
.goal__pip {
position: absolute;
top: 50%;
left: calc(var(--goal-pct, 0) * 100%);
transform: translate(-50%, -50%);
color: var(--goal-accent);
pointer-events: none;
z-index: 1;
/* slide Pip to its perch in sync with the fill grow-in */
animation: goalPip 1s var(--ease-spring) both;
}
@keyframes goalPip {
0% { left: 0; }
100% { left: calc(var(--goal-pct, 0) * 100%); }
}
/* ── Caption ──────────────────────────────────────────────────────────── */
/* .goal__caption / __remaining / __caption-met → utilities in GoalBar.jsx.
The `.goal__amounts strong` descendant rule stays (element combinator). */
.goal__amounts strong {
color: var(--chrome-fg);
font-weight: 600;
}
/* ── Goal-met flourish ────────────────────────────────────────────────── */
.goal--met .goal__fill {
background: linear-gradient(90deg, #b8bb26, #8ec07c);
box-shadow: 0 0 14px -1px color-mix(in srgb, #b8bb26 60%, transparent);
}
.goal--met .goal__pip { color: #b8bb26; }
.goal--met .goal__pct { color: #b8bb26; }
/* ── Mini variant (inside the postcard toast) ─────────────────────────── */
.goal--mini { gap: var(--space-2, 4px); }
.goal--mini .goal__track { height: 8px; }
.goal--mini .goal__caption { font-size: 0.66rem; }
/* ════════════════════════════════════════════════════════════════════════
Pip mascot idle animation
════════════════════════════════════════════════════════════════════════ */
.pip {
display: inline-flex;
align-items: center;
justify-content: center;
line-height: 0;
filter: drop-shadow(0 1px 4px color-mix(in srgb, currentColor 35%, transparent));
}
.pip svg { display: block; overflow: visible; }
.pip__body { animation: pipBob 2.6s ease-in-out infinite; transform-origin: 50% 100%; }
.pip--waving .pip__arm {
transform-origin: 24px 11px;
animation: pipWave 1.8s ease-in-out infinite;
}
@keyframes pipBob {
0%, 100% { transform: translateY(0) scaleY(1); }
50% { transform: translateY(-1px) scaleY(1.03); }
}
@keyframes pipWave {
0%, 60%, 100% { transform: rotate(0deg); }
20% { transform: rotate(-22deg); }
40% { transform: rotate(-6deg); }
}
/* ════════════════════════════════════════════════════════════════════════
Reduced-motion: kill every animation in this stylesheet.
════════════════════════════════════════════════════════════════════════ */
@media (prefers-reduced-motion: reduce) {
.goal__fill,
.goal__pip,
.goal__shimmer,
.pip__body,
.pip--waving .pip__arm {
animation: none !important;
}
/* still honour the final fill width without the grow-in */
.goal__fill { width: calc(var(--goal-pct, 0) * 100%); }
.goal__pip { left: calc(var(--goal-pct, 0) * 100%); }
.goal__shimmer { display: none; }
}
/* ═══ from src/components/donate/Postcard.css ═══ */
/* ════════════════════════════════════════════════════════════════════════
"Support VoiceStudio development" kawaii postcard toast (spec 007, Phase 2).
Non-blocking — lives in the toast layer, no backdrop, never covers results.
Uses real tokens: --chrome-*, --space-*, --ease-spring, --z-toast.
════════════════════════════════════════════════════════════════════════ */
.postcard {
position: relative;
width: 300px;
max-width: calc(100vw - 32px);
padding: 14px 16px 13px;
border-radius: 10px;
background:
linear-gradient(180deg,
color-mix(in srgb, var(--chrome-accent) 7%, #16171a),
#131417);
border: 1px solid var(--chrome-border-strong);
box-shadow:
0 18px 48px -16px rgba(0, 0, 0, 0.6),
0 0 0 1px color-mix(in srgb, var(--chrome-accent) 12%, transparent);
color: var(--chrome-fg);
overflow: hidden;
isolation: isolate;
z-index: var(--z-toast, 1200);
animation: postcardIn 0.42s var(--ease-spring) both;
}
/* dashed "perforation" down the left third — the postcard tear-off look */
.postcard::before {
content: "";
position: absolute;
top: 8px;
bottom: 8px;
left: 64px;
width: 1px;
background-image: repeating-linear-gradient(
to bottom,
color-mix(in srgb, var(--chrome-fg) 28%, transparent) 0 4px,
transparent 4px 9px
);
pointer-events: none;
z-index: 0;
}
/* dot-grain paper texture */
.postcard__grain {
position: absolute;
inset: 0;
pointer-events: none;
opacity: 0.5;
z-index: 0;
background-image: radial-gradient(
circle,
color-mix(in srgb, var(--chrome-fg) 8%, transparent) 0.5px,
transparent 0.5px
);
background-size: 7px 7px;
}
/* ── Stamp (top-left, the "postage" with Pip) ─────────────────────────── */
.postcard__stamp {
position: absolute;
top: 12px;
left: 14px;
width: 40px;
height: 44px;
display: flex;
align-items: center;
justify-content: center;
color: var(--chrome-accent);
border: 1.5px dashed color-mix(in srgb, var(--chrome-accent) 50%, transparent);
border-radius: 4px;
background: color-mix(in srgb, var(--chrome-accent) 10%, transparent);
z-index: 1;
transform-origin: 50% 50%;
animation: stampThunk 0.5s var(--ease-spring) 0.1s both;
}
@keyframes stampThunk {
0% { transform: scale(1.7) rotate(-14deg); opacity: 0; }
55% { transform: scale(0.92) rotate(3deg); opacity: 1; }
100% { transform: scale(1) rotate(-5deg); opacity: 1; }
}
/* ── Close (×) ────────────────────────────────────────────────────────── */
/* Layout/reset moved to utilities in Postcard.jsx; :hover stays here. */
.postcard__close:hover { color: var(--chrome-fg); background: var(--chrome-hover-bg); }
/* ── Body (right of the perforation) ──────────────────────────────────── */
/* .postcard__body / __title / __lead → utilities in Postcard.jsx. */
/* mini GoalBar wrapper — a quiet link to the full Support page.
Layout/reset → utilities in Postcard.jsx; :hover stays here. */
.postcard__goal-link:hover { opacity: 0.92; }
/* ── Actions ──────────────────────────────────────────────────────────── */
/* .postcard__actions / __cta / __later base → utilities in Postcard.jsx.
Interactive :hover states stay here. */
.postcard__cta:hover {
background: color-mix(in srgb, var(--chrome-accent) 24%, transparent);
border-color: var(--chrome-accent);
transform: translateY(-1px);
}
.postcard__later:hover { color: var(--chrome-fg); border-color: var(--chrome-border-strong); }
/* ── Minor row (star + opt-out) ───────────────────────────────────────── */
/* .postcard__minor + the shared __star/__optout base → utilities in
Postcard.jsx. Per-button color + :hover stay here. */
.postcard__star { color: var(--chrome-fg-muted); }
.postcard__star:hover { color: #fabd2f; }
.postcard__optout { color: var(--chrome-fg-dim); }
.postcard__optout:hover { color: var(--chrome-fg-muted); text-decoration: underline; }
/* ── Enter / exit ─────────────────────────────────────────────────────── */
@keyframes postcardIn {
0% { opacity: 0; transform: translateY(14px) scale(0.96); }
100% { opacity: 1; transform: translateY(0) scale(1); }
}
.postcard.is-leaving {
animation: postcardOut 0.28s ease-in forwards;
}
@keyframes postcardOut {
0% { opacity: 1; transform: translateY(0) scale(1); }
100% { opacity: 0; transform: translateY(10px) scale(0.97); }
}
/* ── Reduced motion ───────────────────────────────────────────────────── */
@media (prefers-reduced-motion: reduce) {
.postcard,
.postcard.is-leaving,
.postcard__stamp {
animation: none !important;
}
.postcard__cta:hover { transform: none; }
}
/* ═══ from src/components/settings/AppearancePanel.css ═══ */
/* Settings → Appearance panel.
Built on the shared Settings primitives (SettingsSection / SettingRow).
Mechanical layout (scale slider, scale readout, theme/font containers) now
lives in Tailwind utilities on the JSX; only the rules below stay here:
they reach into a child primitive ([data-slot=setting-row-control]), or carry stateful
transitions / multi-layer box-shadow rings that are not 1:1 utility-safe.
All still route through --chrome-* tokens so they theme across every theme. */
/* The font picker spans the row's full width (SettingRow `stack` prop) so its
tiles lay out as a horizontal grid instead of a squeezed right-aligned column.
Reaches into the SettingRow primitive's control wrapper — kept as CSS. */
.appearance-panel__row--fonts [data-slot='setting-row-control'] { display: block; }
/* Color-theme swatches — stateful ring (hover scale + is-active multi-layer
box-shadow) with transitions; kept as CSS. */
.appearance-panel__theme-dot {
width: 18px;
height: 18px;
border-radius: 50%;
border: none;
background: var(--dot-color, var(--chrome-fg-dim));
cursor: pointer;
padding: 0;
/* Crisp ring drawn outside the dot — calm by default, accent-free until active. */
box-shadow: 0 0 0 1px var(--chrome-border), 0 0 0 3px transparent;
transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.appearance-panel__theme-dot:hover {
transform: scale(1.12);
box-shadow: 0 0 0 1px var(--chrome-border-strong);
}
.appearance-panel__theme-dot.is-active {
box-shadow:
0 0 0 2px var(--chrome-bg),
0 0 0 3px var(--chrome-fg);
}
/* Font picker tiles — live previews with stateful border/background/box-shadow
transitions and an is-active inset ring; kept as CSS. */
.appearance-panel__font-tile {
display: flex;
align-items: center;
gap: var(--space-4);
min-height: 44px;
padding: var(--space-3) var(--space-4);
border: 1px solid var(--chrome-border);
border-radius: var(--chrome-radius-pill);
background: var(--chrome-bg);
color: var(--chrome-fg);
cursor: pointer;
text-align: left;
transition: border-color 0.15s ease, background 0.15s ease, box-shadow 0.15s ease;
}
.appearance-panel__font-tile:hover {
border-color: var(--chrome-border-strong);
background: var(--chrome-hover-bg);
}
.appearance-panel__font-tile.is-active {
border-color: var(--chrome-accent);
background: color-mix(in srgb, var(--chrome-accent) 6%, var(--chrome-bg));
box-shadow: 0 0 0 1px var(--chrome-accent) inset;
}
.appearance-panel__font-sample {
font-size: 21px;
line-height: 1;
flex: 0 0 auto;
color: var(--chrome-fg);
}
.appearance-panel__font-tile.is-active .appearance-panel__font-name {
color: var(--chrome-fg);
}
.appearance-panel__font-name {
font-size: var(--text-sm);
color: var(--chrome-fg-muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
/* name renders in the tile's font too — keep it the same family */
}
/* Navigation-style picker: each tile carries a working miniature of the skin
it selects (rail down the edge vs tabs across the top), so the choice is
read as a shape rather than parsed from a label. */
.appearance-panel__nav-tile {
display: flex;
flex-direction: column;
gap: var(--space-2);
width: 132px;
padding: var(--space-3);
border: 1px solid var(--chrome-border);
border-radius: var(--chrome-radius-pill);
background: var(--chrome-bg);
cursor: pointer;
text-align: left;
transition: border-color 0.15s ease, background 0.15s ease, box-shadow 0.15s ease;
}
.appearance-panel__nav-tile:hover {
border-color: var(--chrome-border-strong);
background: var(--chrome-hover-bg);
}
.appearance-panel__nav-tile.is-active {
border-color: var(--chrome-accent);
background: color-mix(in srgb, var(--chrome-accent) 6%, var(--chrome-bg));
box-shadow: 0 0 0 1px var(--chrome-accent) inset;
}
.appearance-panel__nav-name {
font-size: var(--text-sm);
color: var(--chrome-fg-muted);
white-space: nowrap;
}
.appearance-panel__nav-tile.is-active .appearance-panel__nav-name { color: var(--chrome-fg); }
.appearance-panel__nav-preview {
display: flex;
flex-direction: column;
height: 52px;
border: 1px solid color-mix(in srgb, var(--chrome-fg-muted) 25%, transparent);
border-radius: 3px;
overflow: hidden;
/* The miniature paints the real arrangement: page plane, lighter chrome
strip, one accent-marked destination. Both planes are lifted off the
settings surface so the miniature reads as a little window, not as a
hole in the panel. */
background: color-mix(in srgb, var(--chrome-bg) 94%, #fff);
}
.appearance-panel__nav-preview--rail { flex-direction: row; }
.appearance-panel__nav-preview-plane {
flex: 1 1 auto;
background: color-mix(in srgb, var(--chrome-bg) 94%, #fff);
}
.appearance-panel__nav-preview-bar {
display: flex;
align-items: flex-end;
gap: 2px;
padding: 0 4px;
height: 15px;
background: color-mix(in srgb, var(--chrome-bg) 80%, #fff);
}
.appearance-panel__nav-preview-tab {
width: 24px;
height: 9px;
border-radius: 2px 2px 0 0;
background: color-mix(in srgb, var(--chrome-fg-muted) 35%, transparent);
}
.appearance-panel__nav-preview-tab.is-active {
height: 12px;
background: color-mix(in srgb, var(--chrome-bg) 94%, #fff);
box-shadow: inset 0 2px 0 var(--chrome-accent);
}
.appearance-panel__nav-preview-rail {
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
width: 15px;
padding-top: 6px;
background: color-mix(in srgb, var(--chrome-bg) 80%, #fff);
}
.appearance-panel__nav-preview-dot {
width: 7px;
height: 7px;
border-radius: 2px;
background: color-mix(in srgb, var(--chrome-fg-muted) 35%, transparent);
}
.appearance-panel__nav-preview-dot.is-active { background: var(--chrome-accent); }
/* ═══ from src/components/settings/PerformancePanel.css ═══ */
/* Settings → Performance panel (Wave 2 INST-12). Matches the API Keys
panel palette for visual consistency in the Credentials tab. */
.perfpanel {
display: flex;
flex-direction: column;
gap: 10px;
padding: 12px 0;
}
.perfpanel__title {
display: inline-flex;
align-items: center;
gap: 6px;
margin: 0;
font-family: var(--font-sans);
font-size: 0.92rem;
font-weight: 600;
color: var(--chrome-fg);
}
.perfpanel__error {
padding: 8px 10px;
background: color-mix(in srgb, var(--chrome-severity-err) 12%, transparent);
border: 1px solid color-mix(in srgb, var(--chrome-severity-err) 35%, transparent);
border-radius: var(--chrome-radius-pill);
font-size: 0.78rem;
color: var(--chrome-severity-err);
}
.perfpanel__row {
display: inline-flex;
/* Shared button/badge row across 6+ panels (RemoteBackend, HFMirror,
LLMEndpoint, Pronunciation, MCPBindings, …). Without these it ran off the
right edge on narrow content — wrap instead of overflow, and never exceed
the container; min-width:0 lets text children ellipsis rather than push out. */
flex-wrap: wrap;
max-width: 100%;
min-width: 0;
align-items: center;
gap: 8px;
cursor: pointer;
font-size: 0.84rem;
color: var(--chrome-fg);
}
.perfpanel__row:has(input:disabled) {
cursor: not-allowed;
opacity: 0.7;
}
.perfpanel__checkbox {
width: 16px;
height: 16px;
}
.perfpanel__badge {
font-size: 0.66rem;
text-transform: uppercase;
letter-spacing: 0.05em;
padding: 2px 7px;
border-radius: 999px;
background: var(--chrome-hover-bg);
color: var(--chrome-fg-muted);
margin-left: 4px;
}
.perfpanel__help {
margin: 0;
font-size: 0.78rem;
line-height: 1.55;
color: var(--chrome-fg-muted);
}
.perfpanel__help code {
font-family: var(--font-mono);
background: var(--chrome-hover-bg);
padding: 0 4px;
border-radius: 3px;
font-size: 0.85em;
}
.perfpanel__help a {
color: var(--chrome-accent, #83a598);
text-decoration: none;
}
.perfpanel__help a:hover {
text-decoration: underline;
}
/* ═══ from src/components/settings/VoicePanel.css ═══ */
/* Settings → Capture → Voice panel. The engine-warn banner and most of the
speech-model dropdown chrome now live in Tailwind utilities on the JSX. The
rules kept here can't be expressed 1:1 as utilities: a descendant rule that
reaches into the SettingRow primitive, the chevron's transform transition
(Tailwind's `rotate-*` targets the `rotate` property, not `transform`, so it
wouldn't animate via this transition), a child-combinator on the progress
row, and the spinner keyframe animation. All tokenised. */
/* The model row's control is taller than a single line — top-align it. Reaches
into the SettingRow primitive's control wrapper. */
.voicepanel__row--model [data-slot='setting-row-control'] { align-self: flex-start; }
.voicepanel__dd-chev {
flex: none;
transition: transform 0.15s ease;
color: var(--chrome-fg-muted);
}
.voicepanel__dd-chev.is-open {
transform: rotate(180deg);
}
.voicepanel__dd-progress {
display: flex;
align-items: center;
gap: 8px;
margin-top: 3px;
}
.voicepanel__dd-progress > :first-child {
flex: 1 1 auto;
}
.voicepanel__spin {
animation: voicepanel-spin 0.9s linear infinite;
color: var(--chrome-fg-muted);
}
@keyframes voicepanel-spin {
to { transform: rotate(360deg); }
}
/* ═══ from src/pages/AudiobookTab.css ═══ */
/* Manuscript surface. These selectors intentionally beat the global unlayered
textarea.input-base rules; layout and the responsive page grid stay in JSX. */
.audiobook-tab__manuscript {
background:
linear-gradient(180deg, color-mix(in srgb, var(--color-brand) 4%, transparent), transparent 96px),
var(--color-bg-elev-2);
box-shadow: inset 0 0 0 1px var(--chrome-border), 0 12px 30px rgba(0, 0, 0, 0.08);
transition: box-shadow var(--dur-fast), background var(--dur-fast);
}
.audiobook-tab__manuscript:focus-within {
background:
linear-gradient(180deg, color-mix(in srgb, var(--color-brand) 6%, transparent), transparent 110px),
var(--color-bg-elev-1);
box-shadow: inset 0 0 0 1px var(--chrome-border-strong), 0 14px 34px rgba(0, 0, 0, 0.12);
}
.audiobook-tab__manuscript textarea {
flex: 1 1 auto;
min-height: 0;
width: 100%;
resize: none;
font-family: var(--font-mono, monospace);
font-size: 0.82rem;
line-height: 1.72;
padding: 18px 20px 24px;
border: 0;
border-radius: 0;
background: transparent;
box-shadow: none;
}
.audiobook-tab__manuscript textarea:focus {
border: 0;
background: transparent;
box-shadow: none;
}
@media (max-width: 900px) {
.audiobook-tab__manuscript textarea { min-height: 240px; }
}
/* Synced-lyrics player (audiobook result): chapter text under the native
controls, current word highlighted from playback time. Words are real
<button>s (click-to-seek, keyboard reachable) restyled to read as prose. */
.synced-lyrics__pane {
margin-top: 8px;
max-height: 240px;
overflow-y: auto;
padding: 12px 14px;
border-radius: 10px;
background: var(--color-bg-elev-2);
box-shadow: inset 0 0 0 1px var(--chrome-border);
}
.synced-lyrics__chapter + .synced-lyrics__chapter {
margin-top: 10px;
}
.synced-lyrics__title {
appearance: none;
display: block;
width: 100%;
border: 0;
background: transparent;
padding: 3px 0;
text-align: left;
cursor: pointer;
font-family: var(--chrome-font-mono);
font-size: var(--chrome-label-size);
font-weight: 600;
letter-spacing: var(--chrome-label-track);
text-transform: uppercase;
color: var(--chrome-fg-muted);
margin-bottom: 4px;
}
.synced-lyrics__title:hover,
.synced-lyrics__title:focus-visible {
color: var(--color-fg);
}
.synced-lyrics__text {
margin: 0;
line-height: 1.9;
}
.synced-lyrics__word {
appearance: none;
border: 0;
background: transparent;
font: inherit;
color: var(--chrome-fg-muted);
padding: 1px 3px;
margin: 0 1px 0 0;
border-radius: 5px;
cursor: pointer;
transition: color var(--dur-fast), background var(--dur-fast);
}
.synced-lyrics__word:hover {
color: var(--color-fg);
background: color-mix(in srgb, var(--color-brand) 10%, transparent);
}
.synced-lyrics__word.is-past {
color: var(--color-fg);
}
.synced-lyrics__word.is-active {
color: var(--color-fg);
background: color-mix(in srgb, var(--color-brand) 22%, transparent);
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-brand) 35%, transparent);
}
/* ═══ from src/pages/BatchQueue.css ═══ */
/* Only the genuinely-irreducible bit remains: the running-job progress fill's
gradient + its ::after shimmer (pseudo-element + keyframes) — not expressible
as Tailwind utilities. Everything else (layout, the bar heading, the per-status
card border accents) now lives as utilities in BatchQueue.jsx. */
.batch-queue__progress-fill {
height: 100%;
border-radius: 3px;
background: linear-gradient(90deg, #d3869b 0%, #f3a5b6 50%, #b8bb26 100%);
transition: width 0.4s ease;
position: relative;
}
.batch-queue__progress-fill::after {
content: '';
position: absolute;
top: 0; right: 0;
width: 30px; height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.3));
animation: batch-shimmer 1.5s ease-in-out infinite;
}
@keyframes batch-shimmer {
0% { opacity: 0; }
50% { opacity: 1; }
100% { opacity: 0; }
}
/* ═══ from src/pages/Settings.css ═══ */
/* Settings page — page-specific styles layered over the design system.
Chrome tokens only: every surface here should rhyme with the status bar. */
/* The page chrome itself (`.settings-page` layout + centering + the ≥760px
[rail | content] grid, and `.settings-content`) now lives as Tailwind on the
token bridge directly in Settings.jsx. What remains here is the page-specific
styling the shared primitives can't carry: the tab-rail look (which must stay
unlayered to win over the shadcn Tabs primitive), the `.settings-prose strong`
emphasis (a descendant override still used across tabs), and the Models /
Engines / recommendation rules consumed by their sub-components. */
.settings-prose strong { color: var(--chrome-fg); font-weight: 600; }
/* The category list scrolls independently from its pinned search field and
content pane when short windows or UI scaling make the rail taller than the
available Settings viewport. */
.settings-sidebar-scroll,
.settings-list-scroll {
scrollbar-gutter: stable;
scrollbar-width: thin;
scrollbar-color: color-mix(in srgb, var(--chrome-fg) 18%, transparent) transparent;
}
.settings-sidebar-scroll::-webkit-scrollbar,
.settings-list-scroll::-webkit-scrollbar { width: 6px; }
.settings-sidebar-scroll::-webkit-scrollbar-track,
.settings-list-scroll::-webkit-scrollbar-track { background: transparent; }
.settings-sidebar-scroll::-webkit-scrollbar-thumb,
.settings-list-scroll::-webkit-scrollbar-thumb {
border-radius: var(--chrome-radius-pill);
background: color-mix(in srgb, var(--chrome-fg) 18%, transparent);
}
/* We're migrating to the ui/ Tabs primitive — keep a thin class so the old
.settings-tabs gradient-above space stays; the Tabs primitive itself
carries visual styling. */
/* The .settings-content bridge owns the gap below the bar (see below). */
.settings-tabs-ui { margin-bottom: 0; }
/* ── Tab nav: single-accent, no rainbow ───────────────────────────
The active tab uses the one global --chrome-accent (TAB_DEFS no longer
carries per-tab accents). At ≥760px the nav becomes a sticky vertical
rail; below that it's a single no-wrap horizontal scroll strip. */
.ui-tabs.settings-tabs-ui .ui-tabs__tab.is-active {
background: var(--chrome-hover-bg);
color: var(--chrome-fg);
border-color: transparent;
box-shadow: inset 2px 0 0 var(--chrome-accent);
}
.ui-tabs.settings-tabs-ui .ui-tabs__tab.is-active .ui-tabs__icon {
color: var(--chrome-accent);
}
/* ── Wide (≥760px): vertical rail beside the content column ─────────
The page grid that creates the [rail | content] tracks lives as Tailwind on
Settings.jsx; this block only turns the tab strip into that left-hand rail. */
@media (min-width: 760px) {
.ui-tabs.settings-tabs-ui {
flex-direction: column;
align-items: stretch;
gap: var(--space-1);
border: none;
background: transparent;
position: sticky;
top: var(--space-5);
align-self: start;
}
.ui-tabs.settings-tabs-ui .ui-tabs__tab { justify-content: flex-start; width: 100%; }
}
/* ── Narrow (<760px): horizontal no-wrap scroll strip ───────────── */
@media (max-width: 759.98px) {
.ui-tabs.settings-tabs-ui {
flex-wrap: nowrap;
overflow-x: auto;
scrollbar-width: none;
gap: var(--space-1);
margin-bottom: var(--space-4);
}
.ui-tabs.settings-tabs-ui::-webkit-scrollbar { display: none; }
}
/* The content panel (fills the 1fr track, caps the reading measure, and
establishes the `settings` container query that SettingRow's
`@max-[600px]/settings:` row-stacking variant depends on) is Tailwind on
Settings.jsx now — see the content `<div>` there. */
/* ── Models tab ──────────────────────────────────────────────── */
.models-row__tag {
padding: 0 5px;
font-family: var(--chrome-font-mono);
font-size: var(--chrome-label-size);
letter-spacing: var(--chrome-label-track);
text-transform: uppercase;
color: #d3869b;
border: 1px solid color-mix(in srgb, #d3869b 40%, transparent);
border-radius: var(--chrome-radius-pill);
background: color-mix(in srgb, #d3869b 8%, transparent);
}
/* Curated "recommended for this machine" pick — green sibling of the pink
required tag (same green the RecoBanner's installed state uses). */
.models-row__tag--rec {
color: #8ec07c;
border-color: color-mix(in srgb, #8ec07c 40%, transparent);
background: color-mix(in srgb, #8ec07c 8%, transparent);
cursor: help;
}
/* ── Models tab: table (virtualised) ─────────────────────────────
The toolbar / role tabs / search chrome and the recommendation
banner are Tailwind on their components now (ModelStoreTab.jsx,
EnginesTab.jsx, models/RecoBanner.jsx). What remains here is the
virtualised-table geometry the utilities can't carry. */
/* Table — dense rows, fixed-width action column. */
.models-table { border-radius: var(--chrome-radius-pill); }
.models-table .ui-table-header { padding: 4px var(--space-3); }
.models-table__body { max-height: min(760px, 95dvh); overflow-y: auto; position: relative; }
.models-table__header { align-items: center; }
.models-table__sort {
border: 0;
background: transparent;
color: inherit;
font: inherit;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 4px;
min-width: 0;
}
.models-table__sort:hover { color: var(--chrome-fg); }
.models-table__sort:focus-visible {
outline: none;
box-shadow: var(--focus-ring);
border-radius: var(--chrome-radius-pill);
}
.models-table__sort--off {
border: 0;
background: transparent;
color: inherit;
font: inherit;
cursor: default;
display: inline-flex;
align-items: center;
}
.models-table__sortmark {
color: var(--chrome-accent);
font-family: var(--chrome-font-mono);
font-size: var(--text-2xs);
}
.models-table__virtual {
position: relative;
width: 100%;
}
.models-table__empty {
padding: 18px var(--space-4);
color: var(--chrome-fg-dim);
font-size: var(--text-sm);
font-family: var(--font-sans);
}
/* Inside the wizard embed the table stretches to fill — no fixed cap. */
.setup-wizard__embed [data-slot='settings-section'] {
display: flex; flex-direction: column; height: 100%; min-height: 0;
}
.setup-wizard__embed .models-table {
flex: 1 1 0; display: flex; flex-direction: column; min-height: 0;
}
.setup-wizard__embed .models-table__body { max-height: none; flex: 1 1 0; min-height: 0; }
.models-row {
display: flex;
align-items: center;
gap: var(--space-2);
position: absolute;
left: 0;
right: 0;
top: 0;
width: 100%;
/* Idle rows stay on one compact scan line. Live progress and errors wrap
below it and are measured by the virtualizer as temporary expansions. */
padding: 3px var(--space-3);
min-height: 38px;
border-bottom: 1px solid var(--chrome-border);
font-family: var(--font-sans);
font-size: var(--text-sm);
color: var(--chrome-fg);
border-left: 2px solid transparent;
}
.models-row:last-child { border-bottom: none; }
.models-row.is-ok { border-left-color: var(--chrome-severity-ok); }
.models-row.is-off { border-left-color: var(--chrome-border-strong); opacity: 0.92; }
.models-row.is-unsupported { opacity: 0.4; pointer-events: none; }
.models-row.is-unsupported .models-row__actions { pointer-events: auto; }
.models-row:hover { background: var(--chrome-hover-bg); }
.models-row__cell { min-width: 0; }
.models-row__name { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; overflow: hidden; }
.models-row__title { display: inline-flex; min-width: 0; align-items: center; gap: 6px; color: var(--chrome-fg); font-weight: 500; line-height: 1.3; white-space: nowrap; }
.models-row__label { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.models-row__avatar {
flex-shrink: 0;
width: 18px;
height: 18px;
border-radius: 4px;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 0.48rem;
font-weight: 700;
color: rgba(255, 255, 255, 0.75);
letter-spacing: 0.02em;
}
.models-row__repo { flex: 1 1 120px; min-width: 0; color: var(--chrome-fg-dim); font-size: var(--text-2xs); font-family: var(--chrome-font-mono); line-height: 1.3; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.models-row__progressline,
.models-row__error { flex: 1 0 100%; }
.models-row__hint { display: flex; align-items: center; gap: 4px; color: var(--chrome-fg-dim); font-size: var(--text-2xs, 0.62rem); font-family: var(--chrome-font-mono); line-height: 1.4; margin-top: 1px; cursor: help; opacity: 0.7; transition: opacity 150ms ease; }
.models-row:hover .models-row__hint { opacity: 1; }
.models-row__role { color: var(--chrome-fg-muted); font-size: var(--text-xs); font-family: var(--chrome-font-mono); text-transform: uppercase; letter-spacing: 0.03em; }
.models-row__progress { color: var(--chrome-accent); font-size: var(--text-xs); font-family: var(--chrome-font-mono); margin-top: 2px; }
.models-row__progresstext {
color: var(--chrome-fg-muted); font-size: var(--text-xs);
font-family: var(--chrome-font-mono); font-variant-numeric: tabular-nums;
line-height: 1.4;
}
.models-row__size-live {
font-family: var(--chrome-font-mono);
font-variant-numeric: tabular-nums;
font-size: var(--text-2xs);
color: var(--chrome-accent, #fe8019);
}
.models-row__size-sep {
color: var(--chrome-fg-dim);
margin: 0 1px;
}
.models-row__error {
color: var(--chrome-severity-err); font-size: var(--text-xs);
margin-top: 4px;
}
.models-row__size { font-family: var(--chrome-font-mono); font-size: var(--text-xs); color: var(--chrome-fg-muted); font-variant-numeric: tabular-nums; }
.models-row__actions {
justify-content: flex-end;
display: inline-flex;
gap: 4px;
opacity: 0.55;
transition: opacity var(--dur-fast, 120ms) ease;
}
.models-row:hover .models-row__actions { opacity: 1; }
/* ── Responsive: narrow windows stack row cells ─────────────── */
@media (max-width: 580px) {
.models-row { flex-wrap: wrap; gap: var(--space-3); }
.models-row__name { flex: 1 1 100%; }
.models-row__size { width: auto !important; text-align: left !important; }
.models-row__actions { margin-left: auto; }
}
/* ═══ from src/pages/VoiceGallery.css ═══ */
/* VoiceGallery.css — trimmed to the irreducible bits after the shadcn/Tailwind
migration. Everything layout/chrome moved to Tailwind utilities in the gallery
JSX. What remains is only what isn't cleanly expressible as utilities OR is
rendered by an out-of-scope file:
• .spin — app-wide spinner class. NOTE: this is NOT in index.css (only
`@keyframes spin` + `.spinner` are), and it's used beyond the gallery
(e.g. src/pages/AudiobookTab.jsx), so it must live here. Do not delete.
• .now-playing equalizer (+ @keyframes eq) and the .arch-avatar / flag
classes are all rendered by src/utils/archetypeIcons.jsx (out of scope),
which ships no CSS of its own — keep them here. */
.spin {
animation: spin 1s linear infinite;
}
/* Color-coded avatar tile + flag badge (archetypeIcons.jsx → ArchetypeAvatar). */
.arch-avatar {
position: relative;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid transparent;
border-radius: 11px;
}
.arch-avatar-flag {
position: absolute;
right: -4px;
bottom: -4px;
display: flex;
line-height: 0;
border-radius: 3px;
overflow: hidden;
box-shadow: 0 0 0 2px var(--bg-tertiary, #1d2021);
}
.accent-flag {
display: block;
border-radius: 2px;
}
.flag-globe {
color: var(--text-secondary);
}
/* Animated "now playing" equalizer (archetypeIcons.jsx → NowPlaying). */
.now-playing {
display: inline-flex;
align-items: flex-end;
gap: 2px;
width: 15px;
height: 14px;
}
.now-playing i {
width: 2.5px;
background: currentcolor;
border-radius: 1px;
transform-origin: bottom;
animation: eq 0.9s ease-in-out infinite;
}
.now-playing i:nth-child(1) { height: 45%; animation-delay: 0s; }
.now-playing i:nth-child(2) { height: 80%; animation-delay: 0.15s; }
.now-playing i:nth-child(3) { height: 60%; animation-delay: 0.3s; }
.now-playing i:nth-child(4) { height: 95%; animation-delay: 0.45s; }
@keyframes eq {
0%, 100% { transform: scaleY(0.4); }
50% { transform: scaleY(1); }
}
@media (prefers-reduced-motion: reduce) {
.now-playing i { animation: none; }
}
/* ═══ from src/ui/Dialog.css ═══ */
/* ── Dialog primitive ───────────────────────────────────────────── */
/* Backed by shadcn/ui Dialog (src/components/ui/dialog.tsx). shadcn's
DialogContent owns the fixed centering, z-index, and open/close animation
(Radix data-[state] + tw-animate-css animate-in/out). What remains here is
the GLASS look, which Tailwind utilities can't express in this build (the app
ships Tailwind v4 without the backdrop-filter `--tw-*` plumbing):
• the content surface (warm layered gradient + backdrop blur), applied via
the `.ui-dialog` class the wrapper passes to DialogContent. It is unlayered
so it wins over shadcn's layered `bg-background`/`border`/`shadow` defaults.
• the overlay backdrop blur, keyed off shadcn's `data-slot="dialog-overlay"`
(shadcn already paints the `bg-black/50` scrim; we only add the blur). */
[data-slot='dialog-overlay'] {
backdrop-filter: var(--glass-blur-sm);
-webkit-backdrop-filter: var(--glass-blur-sm);
}
.ui-dialog {
background: linear-gradient(160deg, rgba(47, 41, 39, 0.95) 0%, rgba(32, 28, 27, 0.95) 100%);
backdrop-filter: var(--glass-blur-md);
-webkit-backdrop-filter: var(--glass-blur-md);
border: 1px solid var(--color-border-warm);
box-shadow: var(--shadow-xl);
max-height: calc(100vh - var(--space-7) * 2);
}
/* ═══ from src/ui/Menu.css ═══ */
/* ── Menu primitive ─────────────────────────────────────────────── */
/* Backed by shadcn/ui DropdownMenu (src/components/ui/dropdown-menu.tsx).
shadcn's DropdownMenuContent owns positioning (Radix anchored placement, set
via inline styles) and the enter/exit animation (data-[state]/data-[side] +
tw-animate-css). What remains here is the OmniVoice glass SURFACE + the item
rows — applied via the `.ui-menu*` classes the wrapper passes through.
`.ui-menu` is unlayered so it wins over shadcn's layered
`bg-popover`/`border`/`rounded-md`/`shadow-md` defaults. Position must NOT be
declared here — it would fight Radix's anchored placement. */
.ui-menu {
z-index: var(--z-overlay);
min-width: 160px;
max-width: 320px;
padding: 4px;
background: linear-gradient(160deg, rgba(47, 41, 39, 0.98) 0%, rgba(32, 28, 27, 0.98) 100%);
backdrop-filter: var(--glass-blur-md);
-webkit-backdrop-filter: var(--glass-blur-md);
border: 1px solid var(--color-border-strong);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-xl);
display: flex;
flex-direction: column;
gap: 1px;
}
.ui-menu__item {
display: flex;
align-items: center;
gap: var(--space-3);
padding: 6px 10px;
background: transparent;
border: 0;
border-radius: var(--radius-sm);
color: var(--color-fg);
font-family: var(--font-sans);
font-size: var(--text-md);
font-weight: var(--weight-medium);
cursor: pointer;
text-align: left;
white-space: nowrap;
transition: background var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out);
}
.ui-menu__item:focus {
outline: none;
background: rgba(211, 134, 155, 0.15);
color: var(--color-fg);
}
.ui-menu__item:hover:not(.is-disabled):not(.is-destructive) {
background: rgba(255, 255, 255, 0.06);
}
.ui-menu__item.is-destructive { color: var(--color-danger); }
.ui-menu__item.is-destructive:focus,
.ui-menu__item.is-destructive:hover { background: rgba(251, 73, 52, 0.15); }
.ui-menu__item.is-disabled {
opacity: 0.4;
cursor: not-allowed;
}
.ui-menu__icon { color: var(--color-fg-muted); flex-shrink: 0; }
.ui-menu__label { flex: 1; }
.ui-menu__shortcut {
margin-left: var(--space-5);
font-size: var(--text-xs);
color: var(--color-fg-subtle);
font-family: var(--font-mono);
letter-spacing: 0.04em;
}
.ui-menu__separator {
height: 1px;
margin: 4px 2px;
background: var(--color-border);
}
/* ═══ from src/ui/Table.css ═══ */
/* ── Table primitive (chrome only) ──────────────────────────────── */
.ui-table {
display: flex;
flex-direction: column;
min-height: 0;
flex: 1;
border: 1px solid var(--chrome-border);
border-radius: var(--chrome-radius-pill);
overflow: hidden;
}
/* ── Toolbar ────────────────────────────────────────────────────── */
.ui-table-toolbar {
display: flex;
align-items: center;
gap: var(--space-3);
padding: var(--space-2) var(--space-3);
background: var(--chrome-bg);
border-bottom: 1px solid var(--chrome-border);
flex-shrink: 0;
}
.ui-table-toolbar__search {
position: relative;
flex: 1;
min-width: 120px;
display: flex;
align-items: center;
}
.ui-table-toolbar__search-icon {
position: absolute;
left: 6px;
color: var(--chrome-fg-dim);
pointer-events: none;
}
.ui-table-toolbar__search-input {
width: 100%;
padding-left: 22px;
padding-right: 22px;
font-family: var(--font-sans);
font-size: var(--text-xs);
border-radius: var(--chrome-radius-pill);
}
.ui-table-toolbar__search-clear {
position: absolute !important;
right: 2px;
background: transparent !important;
border: none !important;
width: 16px !important;
height: 16px !important;
}
.ui-table-toolbar__meta {
font-family: var(--chrome-font-mono);
font-size: var(--text-xs);
color: var(--chrome-fg-dim);
white-space: nowrap;
font-variant-numeric: tabular-nums;
}
/* ═══ Stories Editor — calm production desk ════════════════════════════ */
.stories-editor {
overflow: hidden;
background:
radial-gradient(circle at 18% -10%, color-mix(in srgb, var(--color-brand) 4%, transparent), transparent 30%),
var(--chrome-bg);
}
.stories-title {
font-size: 1.05rem;
font-weight: var(--weight-semibold);
letter-spacing: -0.015em;
line-height: 1.2;
}
.stories-speed-value {
min-width: 28px;
font-size: 10px;
font-variant-numeric: tabular-nums;
}
.stories-panel {
width: min(calc(100% - 24px), 1100px);
flex: 0 0 auto;
margin: 6px auto 0;
padding: 8px 10px;
border-radius: 9px;
background: color-mix(in srgb, var(--color-bg-elev-1) 94%, var(--chrome-bg));
box-shadow:
0 8px 22px color-mix(in srgb, black 12%, transparent),
inset 0 0 0 1px color-mix(in srgb, var(--chrome-fg) 6%, transparent);
gap: 7px;
}
.stories-split-panel {
max-height: min(280px, 38vh);
overflow-y: auto;
overscroll-behavior: contain;
}
.stories-cast-member:focus-within {
box-shadow: var(--focus-ring);
}
.stories-track-list {
min-height: 0;
padding: 8px 12px 18px;
gap: 6px;
overscroll-behavior: contain;
scrollbar-gutter: stable;
}
.stories-line,
.stories-chapter {
width: min(100%, 1100px);
margin-inline: auto;
content-visibility: auto;
}
.stories-line {
grid-template-columns: 20px minmax(220px, 1fr) 118px 150px 132px;
grid-template-areas:
'drag text character voice actions'
'. drawer drawer drawer drawer';
gap: 6px;
padding: 7px 8px;
border-radius: 10px;
background: color-mix(in srgb, var(--color-bg-elev-1) 90%, var(--chrome-bg));
box-shadow:
0 4px 14px color-mix(in srgb, black 9%, transparent),
inset 0 0 0 1px color-mix(in srgb, var(--chrome-fg) 5%, transparent);
contain-intrinsic-size: 54px;
transition: box-shadow 150ms ease, background 150ms ease;
}
.stories-line:hover {
background: color-mix(in srgb, var(--color-bg-elev-1) 96%, var(--chrome-bg));
box-shadow:
0 6px 18px color-mix(in srgb, black 12%, transparent),
inset 0 0 0 1px color-mix(in srgb, var(--chrome-fg) 8%, transparent);
}
.stories-line:focus-within,
.stories-line--active {
background: color-mix(in srgb, var(--color-brand) 5%, var(--color-bg-elev-1));
box-shadow:
0 6px 20px color-mix(in srgb, black 14%, transparent),
inset 0 0 0 1px color-mix(in srgb, var(--color-brand) 24%, transparent);
}
.stories-line__drag { grid-area: drag; }
.stories-line__text { grid-area: text; }
.stories-line__character { grid-area: character; }
.stories-line__voice { grid-area: voice; }
.stories-line__actions { grid-area: actions; justify-content: flex-end; }
.stories-line__drawer { grid-area: drawer; }
.stories-line-number {
color: var(--chrome-fg-dim);
font-family: var(--chrome-font-mono);
font-size: 9px;
font-variant-numeric: tabular-nums;
letter-spacing: 0.04em;
}
.stories-line__text {
min-height: 38px;
padding: 4px 6px;
border-radius: 7px;
font-size: var(--text-sm);
line-height: 1.5;
transition: background 150ms ease, box-shadow 150ms ease;
}
.stories-line__text:hover {
background: color-mix(in srgb, var(--chrome-bg) 28%, transparent);
}
.stories-line__text:focus-visible {
background: color-mix(in srgb, var(--chrome-bg) 48%, transparent);
box-shadow: var(--focus-ring);
}
.stories-line__character,
.stories-line__voice {
padding: 2px 4px;
border-radius: 7px;
background: color-mix(in srgb, var(--chrome-bg) 42%, transparent);
}
.stories-line__drawer {
margin-top: 2px;
padding: 8px;
border-radius: 8px;
background: color-mix(in srgb, var(--chrome-bg) 46%, transparent);
box-shadow: inset 0 1px 0 color-mix(in srgb, var(--chrome-fg) 5%, transparent);
}
.stories-chapter {
min-height: 36px;
margin-block: 7px 0;
padding: 5px 9px;
border-radius: 9px;
background: color-mix(in srgb, var(--color-accent) 7%, var(--color-bg-elev-1));
box-shadow: inset 3px 0 0 color-mix(in srgb, var(--color-accent) 70%, transparent);
contain-intrinsic-size: 36px;
}
.stories-chapter__input {
border-radius: 7px;
font-size: var(--text-sm);
font-weight: 650;
letter-spacing: -0.01em;
}
.stories-chapter__input:focus-visible,
.stories-icon-button:focus-visible {
box-shadow: var(--focus-ring);
}
.stories-empty {
margin: 12px;
border-radius: 12px;
background: color-mix(in srgb, var(--color-bg-elev-1) 62%, transparent);
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--chrome-fg) 5%, transparent);
}
.stories-empty__icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 44px;
height: 44px;
border-radius: 12px;
color: var(--color-brand);
background: color-mix(in srgb, var(--color-brand) 10%, transparent);
}
.stories-statusbar {
flex: 0 0 auto;
min-height: 30px;
padding: 5px 12px;
background: color-mix(in srgb, var(--chrome-bg) 90%, transparent);
box-shadow: 0 -1px 0 color-mix(in srgb, var(--chrome-fg) 6%, transparent);
backdrop-filter: blur(14px);
font-variant-numeric: tabular-nums;
}
.shell-narrow .stories-panel,
.shell-mini .stories-panel {
width: calc(100% - 18px);
}
.shell-narrow .stories-track-list,
.shell-mini .stories-track-list {
padding: 7px 9px 16px;
}
.shell-mini .stories-line {
grid-template-columns: 24px minmax(0, 1fr) auto;
grid-template-areas:
'drag text text'
'drag character actions'
'. voice voice'
'. drawer drawer';
padding: 7px;
}
.shell-mini .stories-line__actions {
flex-wrap: wrap;
}
/* Stories v2 — full-height production workspace */
.stories-header {
flex: 0 0 auto;
min-height: 56px;
padding: 9px 14px;
background: color-mix(in srgb, var(--chrome-bg) 88%, var(--color-bg-elev-1));
box-shadow: 0 1px 0 color-mix(in srgb, var(--chrome-fg) 6%, transparent);
}
.stories-identity {
flex: 0 1 360px;
}
.stories-title__icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
flex: 0 0 auto;
border-radius: 8px;
color: var(--color-brand);
background: color-mix(in srgb, var(--color-brand) 10%, transparent);
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-brand) 12%, transparent);
}
.stories-kicker {
display: block;
margin-bottom: 1px;
color: var(--chrome-fg-dim);
font-family: var(--chrome-font-mono);
font-size: 8px;
font-weight: 600;
letter-spacing: 0.1em;
line-height: 1;
text-transform: uppercase;
}
.stories-header-output {
flex: 0 0 auto;
}
.stories-header-stat {
margin-right: 5px;
color: var(--chrome-fg-dim);
font-family: var(--chrome-font-mono);
font-size: 10px;
font-variant-numeric: tabular-nums;
white-space: nowrap;
}
.stories-workspace {
display: grid;
grid-template-columns: 248px minmax(0, 1fr);
overflow: hidden;
}
.stories-sidebar {
display: flex;
min-width: 0;
min-height: 0;
flex-direction: column;
gap: 8px;
padding: 10px;
overflow-y: auto;
overscroll-behavior: contain;
background: color-mix(in srgb, var(--color-bg-elev-1) 72%, var(--chrome-bg));
box-shadow: 1px 0 0 color-mix(in srgb, var(--chrome-fg) 6%, transparent);
scrollbar-gutter: stable;
}
.stories-rail-section {
display: flex;
min-width: 0;
flex-direction: column;
gap: 6px;
padding: 8px;
border-radius: 9px;
background: color-mix(in srgb, var(--chrome-bg) 58%, transparent);
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--chrome-fg) 5%, transparent);
}
.stories-rail-heading,
.stories-rail-label,
.stories-project-editor,
.stories-project-row {
display: flex;
min-width: 0;
align-items: center;
}
.stories-rail-heading {
justify-content: space-between;
gap: 4px;
}
.stories-section-toggle {
display: inline-flex;
min-width: 0;
flex: 1;
align-items: center;
gap: 6px;
padding: 3px 2px;
border: 0;
color: var(--chrome-fg-muted);
background: transparent;
font-family: var(--chrome-font-mono);
font-size: 9px;
font-weight: 600;
letter-spacing: 0.07em;
text-transform: uppercase;
cursor: pointer;
}
.stories-section-toggle:hover {
color: var(--chrome-fg);
}
.stories-section-toggle:focus-visible,
.stories-project-row > button:first-child:focus-visible {
border-radius: 5px;
box-shadow: var(--focus-ring);
}
.stories-section-count {
display: inline-flex;
min-width: 17px;
height: 17px;
align-items: center;
justify-content: center;
margin-left: auto;
border-radius: 5px;
color: var(--chrome-fg-dim);
background: color-mix(in srgb, var(--color-bg-elev-2) 72%, transparent);
font-size: 8px;
font-variant-numeric: tabular-nums;
}
.stories-project-editor {
gap: 4px;
}
.stories-project-list {
display: flex;
max-height: 156px;
flex-direction: column;
gap: 2px;
overflow-y: auto;
overscroll-behavior: contain;
}
.stories-project-row {
gap: 2px;
border-radius: 6px;
}
.stories-project-row--active {
background: color-mix(in srgb, var(--color-brand) 8%, transparent);
}
.stories-project-row > button:first-child {
display: flex;
min-width: 0;
flex: 1;
padding: 5px 6px;
border: 0;
color: var(--chrome-fg-muted);
background: transparent;
font-size: var(--text-xs);
text-align: left;
cursor: pointer;
}
.stories-project-row > button:first-child:hover,
.stories-project-row--active > button:first-child {
color: var(--chrome-fg);
}
.stories-cast-grid {
display: flex;
min-width: 0;
flex-direction: column;
gap: 4px;
}
.stories-cast-member {
display: grid;
grid-template-columns: 8px minmax(62px, 0.8fr) minmax(82px, 1.2fr) 26px;
align-items: center;
gap: 4px;
padding: 4px;
border-radius: 7px;
background: color-mix(in srgb, var(--color-bg-elev-1) 62%, transparent);
}
.stories-cast-member input {
width: 100%;
min-width: 0;
}
.stories-cast-swatch {
width: 7px;
height: 22px;
border-radius: 999px;
}
.stories-output-rail {
margin-top: auto;
}
.stories-rail-label {
gap: 6px;
color: var(--chrome-fg-muted);
font-size: var(--text-xs);
}
.stories-rail-label .stories-speed-value {
margin-left: auto;
color: var(--chrome-fg);
font-family: var(--chrome-font-mono);
}
.stories-manuscript {
display: flex;
flex-direction: column;
overflow: hidden;
background:
linear-gradient(90deg, color-mix(in srgb, var(--color-brand) 2%, transparent), transparent 22%),
var(--chrome-bg);
}
.stories-manuscript-toolbar {
display: flex;
min-height: 42px;
flex: 0 0 auto;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 5px 10px;
background: color-mix(in srgb, var(--chrome-bg) 92%, transparent);
box-shadow: 0 1px 0 color-mix(in srgb, var(--chrome-fg) 5%, transparent);
}
.stories-manuscript-heading {
display: inline-flex;
align-items: center;
gap: 6px;
color: var(--chrome-fg-muted);
font-family: var(--chrome-font-mono);
font-size: 9px;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.stories-manuscript .stories-split-panel {
width: calc(100% - 16px);
margin: 8px;
}
.stories-manuscript .stories-track-list {
width: 100%;
flex: 1 1 auto;
padding: 10px 12px 18px;
}
.stories-manuscript .stories-line,
.stories-manuscript .stories-chapter {
width: 100%;
max-width: 1120px;
}
.stories-manuscript .stories-statusbar {
width: 100%;
}
.shell-narrow .stories-header,
.shell-mini .stories-header {
flex-direction: row;
align-items: center;
padding: 7px 9px;
}
.shell-narrow .stories-workspace {
grid-template-columns: 220px minmax(0, 1fr);
}
.shell-narrow .stories-line {
grid-template-columns: 24px minmax(160px, 1fr) 118px 132px;
grid-template-areas:
'drag text text text'
'drag character voice actions'
'. drawer drawer drawer';
}
.shell-mini .stories-header-stat {
display: none;
}
.shell-mini .stories-workspace {
grid-template-columns: minmax(0, 1fr);
grid-template-rows: auto minmax(0, 1fr);
}
.shell-mini .stories-sidebar {
display: grid;
max-height: 220px;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
padding: 7px;
overflow-y: auto;
box-shadow: 0 1px 0 color-mix(in srgb, var(--chrome-fg) 6%, transparent);
}
.shell-mini .stories-output-rail {
grid-column: 1 / -1;
margin-top: 0;
}
.shell-mini .stories-manuscript-toolbar {
flex-wrap: wrap;
}
@media (prefers-reduced-motion: reduce) {
.stories-line,
.stories-line__text {
transition: none;
}
}
/* Stories shares the Clone workspace tab navigation, with one task per panel. */
.stories-tabbed .stories-tabbar {
flex-shrink: 0;
padding: 12px 16px;
border-bottom: 1px solid var(--color-border);
}
.stories-tabbed .stories-tab span {
min-width: 0;
overflow-wrap: anywhere;
}
.stories-tabbed .stories-tab-panel {
min-height: 0;
min-width: 0;
flex: 1;
overflow-y: auto;
margin-top: 0;
}
.stories-tabbed .stories-tab-panel[data-state="inactive"] {
display: none;
}
.stories-tabbed .stories-workspace {
display: flex;
flex-direction: column;
}
.stories-tabbed .stories-manuscript {
height: 100%;
}
.stories-tabbed .stories-rail-section {
width: min(100% - 32px, 1040px);
margin: 20px auto;
padding: 20px;
gap: 18px;
border: 1px solid var(--color-border);
border-radius: 14px;
background: var(--color-bg-elev-1);
box-shadow: none;
}
.stories-tabbed .stories-export-panel .stories-rail-section {
max-width: 640px;
}
.stories-tabbed .stories-rail-heading {
flex-wrap: wrap;
gap: 12px;
}
.stories-tabbed .stories-section-toggle {
cursor: default;
}
.stories-tabbed .stories-section-toggle,
.stories-tabbed .stories-manuscript-heading {
font-family: var(--font-sans);
font-size: 13px;
letter-spacing: normal;
text-transform: none;
}
.stories-tabbed .stories-section-count {
font-size: 12px;
min-width: 24px;
height: 24px;
}
.stories-tabbed .stories-cast-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 300px), 1fr));
gap: 12px;
}
.stories-tabbed .stories-cast-member {
grid-template-columns: 8px minmax(0, 1fr) 32px;
gap: 12px;
padding: 16px;
border: 1px solid var(--color-border);
border-radius: 10px;
}
.stories-tabbed .stories-cast-member > span.min-w-0 {
grid-column: 2;
grid-row: 2;
}
.stories-tabbed .stories-cast-member > button {
border: 0;
grid-column: 3;
grid-row: 1;
}
.stories-tabbed .stories-cast-member input {
min-height: 36px;
font-size: 13px;
}
.stories-tabbed .stories-project-list {
max-height: none;
gap: 8px;
}
.stories-tabbed .stories-project-row {
padding: 8px;
border: 1px solid var(--color-border);
}
.stories-tabbed .stories-project-editor {
gap: 12px;
}
.stories-tabbed .stories-project-editor input {
min-height: 38px;
}
.stories-tabbed .stories-manuscript-toolbar {
padding: 12px 16px;
flex-wrap: wrap;
}
.stories-tabbed .stories-manuscript-toolbar > div:last-child {
flex-wrap: wrap;
}
.stories-tabbed .stories-track-list {
gap: 12px;
padding: 16px;
}
.stories-tabbed .stories-line {
grid-template-columns: 28px minmax(100px, 1fr) minmax(120px, 1fr) auto;
grid-template-areas:
'drag text text text'
'. character voice actions'
'. drawer drawer drawer';
gap: 10px;
padding: 14px;
cursor: default;
box-shadow: none;
border: 1px solid var(--color-border);
contain-intrinsic-size: 112px;
}
.stories-tabbed .stories-line__text {
min-height: 56px;
font-size: 15px;
line-height: 1.65;
}
.stories-tabbed .stories-line-number {
font-size: 11px;
}
.stories-tabbed .stories-line__actions {
opacity: 1;
}
.stories-tabbed .stories-line__actions button {
border: 0;
color: var(--color-fg-muted);
min-width: 32px;
min-height: 32px;
}
.stories-tabbed .stories-line__actions button:hover:enabled {
color: var(--color-fg);
background: var(--color-bg-elev-2);
}
.stories-tabbed .stories-line__actions button:last-child:hover:enabled {
color: var(--color-danger);
}
.stories-tabbed .stories-kicker {
font-size: 10px;
line-height: 1.4;
}
.stories-tabbed .stories-header {
padding: 14px 16px;
}
@media (max-width: 600px) {
.stories-tabbed .stories-tab {
flex-direction: column;
gap: 4px;
padding: 8px 4px;
}
.stories-tabbed .stories-header {
flex-wrap: wrap;
}
.stories-tabbed .stories-tabbar {
padding: 8px;
}
.stories-tabbed .stories-line {
grid-template-columns: 22px minmax(0, 1fr) minmax(0, 1fr);
grid-template-areas: 'drag text text' '. character voice' '. actions actions' '. drawer drawer';
padding: 10px;
}
.stories-tabbed .stories-rail-section {
width: calc(100% - 16px);
padding: 14px;
margin: 8px;
}
}
.stories-tabbed .stories-rail-label { font-size: 13px; }
.stories-tabbed .stories-rail-label > span { flex-shrink: 0; }
.stories-tabbed .stories-export-panel select { min-height: 40px; max-width: 220px; }
.shell-mini .stories-tabbed .stories-line {
grid-template-columns: 22px minmax(0, 1fr) minmax(0, 1fr);
grid-template-areas:
'drag text text'
'. character voice'
'. actions actions'
'. drawer drawer';
padding: 10px;
}
/* Compact two-row Stories command header. */
.stories-tabbed .stories-header { padding: 8px 16px; }
.stories-tabbed .stories-tabbar {
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 6px 16px;
flex-wrap: wrap;
}
.stories-tabbed .stories-tab { min-height: 36px; padding: 6px 14px; }
.stories-tabbed .stories-script-tools { flex-wrap: wrap; }
@media (max-width: 600px) {
.stories-tabbed .stories-tabbar { padding: 6px 8px; gap: 6px; }
.stories-tabbed .stories-tabbar [role="tablist"] { width: 100%; }
.stories-tabbed .stories-tab { flex-direction: row; padding: 6px 4px; }
.stories-tabbed .stories-tab svg { display: none; }
}
/* ── Sticky header row ─────────────────────────────────────────── */
.ui-table-header {
display: flex;
align-items: center;
gap: var(--space-2);
padding: 3px var(--space-3);
background: var(--chrome-bg);
font-family: var(--chrome-font-mono);
font-size: var(--chrome-label-size);
font-weight: 600;
color: var(--chrome-fg-muted);
position: sticky;
top: 0;
z-index: 1;
border-bottom: 1px solid var(--chrome-border);
letter-spacing: var(--chrome-label-track);
text-transform: uppercase;
flex-shrink: 0;
}
.ui-table-header__cell { min-width: 0; }
.ui-table-header__cell--align-left { text-align: left; }
.ui-table-header__cell--align-center { text-align: center; }
.ui-table-header__cell--align-right { text-align: right; }
/* ═══ from src/ui/Tooltip.css ═══ */
/* ── Tooltip primitive ──────────────────────────────────────────── */
/* Backed by shadcn/ui Tooltip (src/components/ui/tooltip.tsx). shadcn's
TooltipContent owns positioning (Radix Popper) and the enter/exit animation
(data-[state]/data-[side] + tw-animate-css). What remains here is only the
OmniVoice tooltip SURFACE, applied via the `.ui-tooltip` class the wrapper
passes to TooltipContent — unlayered, so it wins over shadcn's layered
`bg-primary`/`text-primary-foreground`/`rounded-md`/`px-3 py-1.5` defaults. */
.ui-tooltip {
background: #0f0e0d;
color: var(--color-fg);
font-family: var(--font-sans);
font-size: var(--text-xs);
font-weight: var(--weight-medium);
padding: 4px 8px;
border-radius: var(--radius-sm);
border: 1px solid var(--color-border-strong);
box-shadow: var(--shadow-md);
white-space: nowrap;
pointer-events: none;
max-width: 240px;
}
/* Override header gradient for light modes */
[data-theme="light"] .header-area h1 {
background: linear-gradient(135deg, var(--chrome-fg) 0%, var(--chrome-fg-muted) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
@media (prefers-color-scheme: light) {
[data-theme="auto"] .header-area h1 {
background: linear-gradient(135deg, var(--chrome-fg) 0%, var(--chrome-fg-muted) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
}
/* ═══════════════════════════════════════════════════════════════════════
APP-WIDE BORDER / DIVIDER REMOVAL (owner-approved)
Zero every decorative border/hairline token. Kept LAST in source order so
it wins over the default `:root` and every `[data-theme]` override at equal
specificity. Focus indicators (--color-ring / --focus-ring) are deliberately
NOT zeroed — keyboard a11y focus rings stay.
═══════════════════════════════════════════════════════════════════════ */
:root, [data-theme] {
--color-border: transparent; --color-border-strong: transparent; --color-border-warm: transparent;
--chrome-border: transparent; --chrome-border-strong: transparent; --chrome-accent-border: transparent;
--glass-border: transparent;
}
/* ═══ RESPONSIVE DUB WORKSPACE — local Colab usability patch ═══
The original editor treated the two-column workspace as a fixed-height
desktop canvas and hid overflow. On laptop/remote-browser viewports that
compressed both panes until controls and transcript rows were effectively
clipped, forcing users to run the site at ~80% zoom. These rules respond to
the ACTUAL dub workspace width (container query), so UI scale/browser zoom
no longer makes the breakpoint fire at the wrong size. */
.dub-workspace {
min-width: 0;
min-height: 0;
}
.dub-editor,
.dub-editor-grid,
.dub-panel-left,
.dub-panel-right {
min-width: 0;
}
.dub-resizable-columns {
display: grid;
gap: 0;
}
.dub-resizable-columns > .dub-panel-col {
min-height: 0;
overflow-x: hidden;
overflow-y: auto;
scrollbar-gutter: stable;
padding-block: 4px 12px;
}
.dub-panel-left .wfm-video-preview {
aspect-ratio: 16 / 9;
height: auto;
max-height: min(52vh, 540px);
border-radius: 10px;
}
.dub-panel-right > .segment-table {
flex: 1 0 320px;
}
.dub-column-splitter {
position: relative;
min-width: 12px;
cursor: col-resize;
touch-action: none;
}
.dub-column-splitter::before {
position: absolute;
top: calc(50% - 20px);
height: 40px;
inset-inline: 5px;
border-radius: 999px;
background: color-mix(in srgb, var(--chrome-fg) 18%, transparent);
content: '';
transition: background 120ms ease, inset 120ms ease;
}
.dub-column-splitter:hover::before,
.dub-column-splitter:focus-visible::before {
inset-inline: 4px;
background: color-mix(in srgb, var(--color-brand) 72%, transparent);
}
.dub-column-splitter:focus-visible {
border-radius: 6px;
outline: none;
box-shadow: var(--focus-ring);
}
@container dub-shell (max-width: 960px) {
.dub-editor {
flex: 1 1 auto;
overflow-y: auto;
overflow-x: hidden;
overscroll-behavior: contain;
scrollbar-gutter: stable;
padding-bottom: 8px;
}
.dub-editor-grid {
display: flex;
flex: 0 0 auto;
flex-direction: column;
min-height: 0;
overflow: visible;
}
.dub-column-splitter {
display: none;
}
.dub-panel-col {
flex: 0 0 auto;
overflow: visible;
}
.dub-resizable-columns > .dub-panel-col {
overflow: visible;
scrollbar-gutter: auto;
}
/* Preview / timeline pane: keep enough vertical room to be useful rather
than shrinking the waveform to a strip. */
.dub-panel-left {
min-height: 460px;
}
/* Transcript / output pane needs more room because its segment table is the
primary editor. The page scrolls around the pane instead of squeezing it. */
.dub-panel-right {
min-height: 560px;
}
.dub-panel-right .dub-segment-table__body {
min-height: 280px;
}
.dub-command-bar {
align-items: flex-start;
flex-wrap: wrap;
row-gap: 6px;
}
.dub-command-bar__actions,
.dub-command-bar__utilities {
flex-wrap: wrap;
}
}
@container dub-shell (max-width: 720px) {
.dub-panel-left,
.dub-panel-right {
padding-inline: 8px;
}
.dub-panel-left { min-height: 420px; }
.dub-panel-right { min-height: 520px; }
/* Controls should wrap before their labels/inputs are truncated. */
.dub-panel-col .label-row {
white-space: normal;
overflow: visible;
text-overflow: clip;
}
.dub-lang-switch {
flex-wrap: wrap;
}
}
/* Short screens are common through Colab/Cloudflare because browser chrome
consumes a large part of the physical height. Keep the main workspace as the
scroll owner instead of allowing fixed-height children to be crushed. */
@media (max-height: 760px) {
.app-container > .main-content {
scrollbar-gutter: stable;
}
.dub-editor {
overscroll-behavior-y: contain;
}
}
/* Timeline visibility follow-up: keep the transcription lane and transport
controls on-screen at normal browser zoom, including Colab/Cloudflare tabs
where browser chrome reduces the usable vertical viewport. */
@container dub-shell (max-width: 960px) {
.dub-panel-left .wfm-video-preview {
height: clamp(180px, 32vh, 300px);
max-height: 300px;
}
.dub-panel-left .seg-track {
flex: 0 0 auto !important;
height: auto;
}
.dub-panel-left .wfm-controls {
flex-wrap: wrap;
row-gap: 5px;
}
}
@container dub-shell (max-width: 720px) {
.dub-panel-left .wfm-video-preview {
height: clamp(160px, 28vh, 240px);
max-height: 240px;
}
}
@media (max-height: 820px) {
.wfm-video-preview {
height: clamp(150px, 28vh, 260px);
max-height: 260px;
}
}
/* Dubbing start workspace: a focused import card with nearby language choices. */
.dub-start-screen .dub-panel-col {
overflow-y: auto;
padding: clamp(16px, 4vw, 48px);
background:
radial-gradient(ellipse at 50% 24%, color-mix(in srgb, var(--color-brand) 7%, transparent), transparent 62%),
var(--chrome-bg);
}
.dub-start-card {
width: min(100%, 740px);
min-width: 0;
flex-shrink: 0;
margin: auto;
padding: clamp(18px, 3vw, 32px);
border: 1px solid var(--color-border);
border-radius: 22px;
background: var(--color-bg-elev-1);
box-shadow: 0 20px 60px color-mix(in srgb, var(--chrome-bg) 35%, transparent);
}
.dub-start-card .dub-idle-drop {
flex: none;
margin: 0;
min-height: 220px;
padding: 26px 16px;
gap: 16px;
border: 1px dashed color-mix(in srgb, var(--color-brand) 45%, var(--color-border));
border-radius: 16px;
background: color-mix(in srgb, var(--color-brand) 4%, var(--chrome-bg));
}
.dub-start-card .dub-idle-drop:hover,
.dub-start-card .dub-idle-drop.is-dragging {
border-color: var(--color-brand);
background: color-mix(in srgb, var(--color-brand) 9%, var(--chrome-bg));
}
.dub-start-card .dub-idle-drop__puck {
width: 64px;
height: 64px;
border-radius: 20px;
color: var(--color-brand);
background: color-mix(in srgb, var(--color-brand) 12%, var(--color-bg-elev-1));
border-color: color-mix(in srgb, var(--color-brand) 24%, transparent);
box-shadow: 0 0 0 8px color-mix(in srgb, var(--color-brand) 3%, transparent);
}
.dub-start-card .dub-idle-drop__puck svg { color: inherit; }
.dub-start-card .dub-landing-opts__lang {
flex: 1;
flex-direction: column;
align-items: stretch;
gap: 8px;
}
.dub-start-card .dub-landing-opts__lang > svg { display: none; }
.dub-start-card .dub-landing-opts__lang select {
width: 100%;
max-width: none;
min-height: 42px;
font-size: 13px;
}
.dub-start-card input[type="text"],
.dub-start-card input[type="url"] { min-width: 0; font-size: 13px; }
.dub-start-card details { margin-top: 16px; }
.dub-start-card summary { cursor: pointer; padding: 10px 0; font-size: 13px; color: var(--color-fg-muted); }
.dub-start-card .dub-ingest-row__cta { min-height: 36px; font-size: 13px; }
@media (max-width: 600px) {
.dub-start-screen .dub-panel-col { padding: 12px; }
.dub-start-card { padding: 16px; border-radius: 16px; }
.dub-start-card .dub-idle-drop { min-height: 190px; padding: 20px 12px; }
}
.dub-start-title { margin: 0; color: var(--color-fg); font-size: 22px; font-weight: 600; line-height: 1.3; text-wrap: balance; }
.dub-start-formats { color: var(--color-fg-muted); font-size: 12px; line-height: 1.6; }
.dub-start-languages { display: flex; gap: 16px; align-items: stretch; margin-top: 24px; }
.dub-start-url { display: flex; align-items: center; gap: 10px; margin-top: 20px; padding: 8px 12px; border: 1px solid var(--color-border); border-radius: 10px; background: var(--chrome-bg); }
.dub-start-url input { flex: 1; min-height: 36px; }
.dub-start-url:focus-within { box-shadow: var(--focus-ring); }
@media (max-width: 600px) {
.dub-start-title { font-size: 19px; }
.dub-start-languages { flex-direction: column; gap: 12px; }
}
.dub-start-choose { display: inline-flex; align-items: center; justify-content: center; gap: 8px; min-height: 42px; padding: 10px 22px; border-radius: 10px; background: var(--color-brand); color: var(--color-bg); font-size: 14px; font-weight: 600; }
.dub-start-advanced, .dub-start-batch { display: flex; align-items: center; gap: 8px; border: 0; background: transparent; color: var(--color-fg-muted); cursor: pointer; font-size: 13px; min-height: 40px; }
.dub-start-advanced { width: 100%; justify-content: space-between; margin-top: 12px; padding: 8px 0; }
.dub-start-batch { justify-content: center; width: 100%; margin-top: 16px; padding: 16px 8px 0; border-top: 1px solid var(--color-border); text-wrap: pretty; }
.dub-start-advanced:hover, .dub-start-batch:hover { color: var(--color-fg); }
.dub-start-advanced:focus-visible, .dub-start-batch:focus-visible { outline: none; box-shadow: var(--focus-ring); border-radius: 6px; }
.dub-start-options { display: flex; flex-direction: column; gap: 18px; padding: 16px; border-radius: 10px; background: var(--chrome-bg); font-size: 13px; color: var(--color-fg-muted); }
.dub-start-caption-option, .dub-start-cookie-option { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
.dub-start-caption-option input { width: 16px; height: 16px; }
.dub-start-cookie-option input { min-width: 0; max-width: 100%; font-size: 12px; }
.dub-start-generation-options { display: flex; flex-wrap: wrap; gap: 16px; }
.dub-start-generation-options label { display: flex; flex-direction: column; gap: 8px; }
.dub-start-generation-options label > span { display: flex; align-items: center; gap: 6px; }
.dub-start-generation-options input { min-height: 36px; margin-left: 0; }
.dub-start-generation-options input[type="number"] { width: 88px; }
.dub-start-languages label > span { font-size: 13px; }
.dub-start-options[hidden] { display: none; }
/* Layout utilities must not reveal inactive Radix tab panels. */
[data-slot="tabs-content"][hidden] {
display: none !important;
}
/* Give the stacked library its own scrollable area below the import form. */
.shell-narrow .studio-right:has(.dub-project-library),
.shell-mini .studio-right:has(.dub-project-library) {
flex: 0 0 min(420px, 60vh);
min-height: 240px;
}
.dub-start-language-select { width: 100%; min-height: 42px; font-size: 13px; }
/* Keep project cards stationary as their actions become visible. */
.dub-project-library .history-actions {
min-height: 30px;
max-height: none;
transition: opacity 0.2s;
}
@media (hover: none) {
.dub-project-library .history-actions { opacity: 1; }
}
.capture-pill__control {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: 28px;
height: 28px;
padding: 0;
border: 0;
border-radius: 50%;
background: rgb(255 255 255 / 10%);
color: rgb(255 255 255 / 90%);
cursor: pointer;
}
.capture-pill__control:hover { background: rgb(255 255 255 / 20%); }
.capture-pill__control:focus-visible { outline: 2px solid white; outline-offset: 2px; }
/* Keep live text independent of the recording controls. */
.capture-pill {
width: 420px;
max-width: calc(100vw - 16px);
height: auto;
min-height: 48px;
flex-wrap: wrap;
border-radius: 20px;
}
.capture-pill__preview {
order: 2;
flex: 0 0 100%;
min-width: 0;
max-height: 84px;
overflow-y: auto;
white-space: pre-wrap;
overflow-wrap: anywhere;
color: white;
}
/* An error is a status line, not a transcript: two lines at most. A long
message used to wrap into a four-line block that spilled past the capsule's
edge. The label's `title` carries the full text, and anything the user must
act on also goes to the main window (utils/dictationNotice). */
.capture-pill--error .capture-pill__preview > span {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* ── In-app reduced motion (#1857) ─────────────────────────────────────
Settings → Appearance → Reduce motion sets `data-motion="reduce"` on the
root. This is the same blanket rule the `prefers-reduced-motion` blocks
above apply piecemeal, expressed once: it cannot miss an animation the way
a per-component list can, and the header status dot is exactly what that
list missed.
Deliberately NOT merged into the media query. The OS preference must keep
working on its own, so a user whose system asks for reduced motion still
gets it with this toggle off. The two are additive; either one is enough.
0.01ms rather than 0: a zero duration skips the `animationend` /
`transitionend` events some components wait on, which strands them. A
duration this short is imperceptible but still fires. */
[data-motion='reduce'],
[data-motion='reduce'] *,
[data-motion='reduce'] *::before,
[data-motion='reduce'] *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}