7702 lines
269 KiB
CSS
7702 lines
269 KiB
CSS
@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;
|
||
}
|