diff --git a/src/app/globals.css b/src/app/globals.css
index 3de2ac0..df890d3 100755
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -44,6 +44,11 @@
}
:root {
+ /* v6.4 accent plumbing — same values as .dark (the default theme); the
+ .light block overrides both for the white theme. */
+ --accent-rgb: 253 224 71;
+ --scrollbar-thumb: #56406f;
+
--radius: 0.625rem;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
@@ -78,58 +83,100 @@
--sidebar-ring: oklch(0.708 0 0);
}
+/*
+ * OVE dark theme (v6.4) — ROYAL PURPLE with light-yellow highlights.
+ *
+ * The app chrome is styled with the Tailwind zinc/emerald utilities directly,
+ * so — like the .light remap below — the dark palette is produced by remapping
+ * those ramps under `.dark` (unlayered class rules beat Tailwind's @layer
+ * theme, and color-mix keeps the var references so opacity variants follow):
+ * • zinc is re-hued into a royal-purple ramp — 950 is the deep purple page,
+ * 900 the panel purple, 800/700 borders, 500→50 lavender text;
+ * • emerald (the old green accent) becomes the LIGHT YELLOW highlight
+ * family — emerald-400 is #fde047 for CTAs/BPM/active states, hover
+ * lightens to #fef08a, and emerald-950 (text on the yellow buttons)
+ * becomes a deep royal purple;
+ * • the shadcn semantic tokens follow (background/card/border/primary…),
+ * so dialogs, toasts, sliders and switches pick up the same theme;
+ * • --accent-rgb carries the highlight color as an rgb triple for the
+ * .glow-* shadow utilities at the bottom of this file.
+ * The visualization canvas keeps its own black stage by design.
+ */
.dark {
color-scheme: dark;
- --background: oklch(0.145 0 0);
- --foreground: oklch(0.985 0 0);
- --card: oklch(0.205 0 0);
- --card-foreground: oklch(0.985 0 0);
- --popover: oklch(0.205 0 0);
- --popover-foreground: oklch(0.985 0 0);
- --primary: oklch(0.922 0 0);
- --primary-foreground: oklch(0.205 0 0);
- --secondary: oklch(0.269 0 0);
- --secondary-foreground: oklch(0.985 0 0);
- --muted: oklch(0.269 0 0);
- --muted-foreground: oklch(0.708 0 0);
- --accent: oklch(0.269 0 0);
- --accent-foreground: oklch(0.985 0 0);
+
+ /* semantic tokens — royal purple chrome, light-yellow accent */
+ --background: #1b0f2e;
+ --foreground: #f5f1fb;
+ --card: #2a1a42;
+ --card-foreground: #f5f1fb;
+ --popover: #2a1a42;
+ --popover-foreground: #f5f1fb;
+ --primary: #fde047;
+ --primary-foreground: #241238;
+ --secondary: #35244e;
+ --secondary-foreground: #ece4f8;
+ --muted: #2a1a42;
+ --muted-foreground: #ab97d1;
+ --accent: #35244e;
+ --accent-foreground: #ece4f8;
--destructive: oklch(0.704 0.191 22.216);
- --border: oklch(1 0 0 / 10%);
- --input: oklch(1 0 0 / 15%);
- --ring: oklch(0.556 0 0);
- --chart-1: oklch(0.488 0.243 264.376);
- --chart-2: oklch(0.696 0.17 162.48);
- --chart-3: oklch(0.769 0.188 70.08);
- --chart-4: oklch(0.627 0.265 303.9);
- --chart-5: oklch(0.645 0.246 16.439);
- --sidebar: oklch(0.205 0 0);
- --sidebar-foreground: oklch(0.985 0 0);
- --sidebar-primary: oklch(0.488 0.243 264.376);
- --sidebar-primary-foreground: oklch(0.985 0 0);
- --sidebar-accent: oklch(0.269 0 0);
- --sidebar-accent-foreground: oklch(0.985 0 0);
- --sidebar-border: oklch(1 0 0 / 10%);
- --sidebar-ring: oklch(0.556 0 0);
+ --border: #453063;
+ --input: #453063;
+ --ring: #fde047;
+ --chart-1: #fde047;
+ --chart-2: #937cbb;
+ --chart-3: #facc15;
+ --chart-4: #74599f;
+ --chart-5: #c9b8e6;
+ --sidebar: #2a1a42;
+ --sidebar-foreground: #f5f1fb;
+ --sidebar-primary: #fde047;
+ --sidebar-primary-foreground: #241238;
+ --sidebar-accent: #35244e;
+ --sidebar-accent-foreground: #ece4f8;
+ --sidebar-border: #453063;
+ --sidebar-ring: #fde047;
+
+ /* zinc ramp → royal purple */
+ --color-zinc-50: #f5f1fb;
+ --color-zinc-100: #ece4f8;
+ --color-zinc-200: #ded2f0;
+ --color-zinc-300: #c9b8e6;
+ --color-zinc-400: #ab97d1;
+ --color-zinc-500: #937cbb;
+ --color-zinc-600: #74599f;
+ --color-zinc-700: #56406f;
+ --color-zinc-800: #3d2957;
+ --color-zinc-900: #2a1a42;
+ --color-zinc-950: #1b0f2e;
+
+ /* emerald → light-yellow highlights */
+ --color-emerald-200: #fef9c3;
+ --color-emerald-300: #fef08a;
+ --color-emerald-400: #fde047;
+ --color-emerald-500: #facc15;
+ --color-emerald-950: #241238;
+
+ /* highlight rgb triple for the .glow-* utilities + scrollbar thumb */
+ --accent-rgb: 253 224 71;
+ --scrollbar-thumb: #56406f;
}
/*
- * OVE light theme (v6.3).
- *
- * The app is styled with the Tailwind zinc/emerald/rose/amber utilities
- * directly (no semantic tokens for the app chrome), so the light theme is
- * implemented by REMAPPING those palettes under `.light` (next-themes sets
- * the class on ):
- * • zinc is inverted — 950 (page) becomes white, 100 (text) becomes
- * near-black; borders/hover/panels land on their light equivalents;
- * • emerald/rose/amber accents are deepened one step so text and buttons
- * keep WCAG-ish contrast on white (emerald-400 → emerald-600 etc.),
- * hover states still lighten (emerald-300 → emerald-500);
- * • the visualization canvas keeps its own black stage by design.
+ * OVE light theme (v6.3) — the white theme on the flip side of the header
+ * tick switch. Same remap technique: zinc is inverted (950 page → white,
+ * 100 text → near-black) and the emerald/rose/amber accents are deepened one
+ * step so text and buttons keep WCAG-ish contrast on white. The dark theme
+ * above is completely independent — flipping the switch swaps both ramps.
*/
.light {
color-scheme: light;
+ /* accent plumbing for the white theme — deep emerald highlights */
+ --accent-rgb: 5 150 105;
+ --scrollbar-thumb: #d4d4d8;
+
/* inverted zinc ramp */
--color-zinc-50: #18181b;
--color-zinc-100: #18181b;
@@ -161,3 +208,31 @@
@apply bg-background text-foreground;
}
}
+
+/*
+ * Theme-aware accent glows (v6.4). The old hardcoded emerald rgba() shadows
+ * could not follow the palette swap, so every glow now reads --accent-rgb:
+ * light yellow on royal purple in the dark theme, deep emerald on white in
+ * the light theme.
+ */
+.glow-logo {
+ box-shadow: 0 0 16px rgb(var(--accent-rgb) / 0.35);
+}
+.glow-segment {
+ box-shadow: inset 0 0 0 1px rgb(var(--accent-rgb) / 0.35);
+}
+.glow-cta {
+ box-shadow: 0 0 20px rgb(var(--accent-rgb) / 0.25);
+}
+.glow-setbeat {
+ box-shadow: 0 0 24px rgb(var(--accent-rgb) / 0.28);
+}
+.glow-beat-flash {
+ box-shadow: 0 0 18px 4px rgb(var(--accent-rgb) / 0.65);
+}
+.glow-beat-idle {
+ box-shadow: 0 0 8px 1px rgb(var(--accent-rgb) / 0.3);
+}
+.glow-status {
+ box-shadow: 0 0 8px rgb(var(--accent-rgb) / 0.8);
+}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index ecd80c9..a3ac340 100755
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -40,6 +40,12 @@ function useBeatWatcher() {
useEffect(() => {
if (!stats) return
+ // No source playing/loaded → beat detection is suspended (v6.4). Also
+ // drop the phase reference so a resumed source can't false-flash.
+ if (stats.source === 'idle') {
+ prevPhaseRef.current = null
+ return
+ }
const b = stats.beat
const prev = prevPhaseRef.current
prevPhaseRef.current = b.phase
diff --git a/src/components/ava/beat-dot.tsx b/src/components/ava/beat-dot.tsx
index 29cf58f..49a7de8 100755
--- a/src/components/ava/beat-dot.tsx
+++ b/src/components/ava/beat-dot.tsx
@@ -22,6 +22,7 @@ export function BeatDot({ size = 44, className }: BeatDotProps) {
const phase = useAvaStore((s) => s.stats?.beat.phase ?? 0)
const beatFlash = useAvaStore((s) => s.beatFlash)
const bpm = useAvaStore((s) => s.stats?.beat.bpm ?? 0)
+ const idle = useAvaStore((s) => s.source) === 'idle'
// flash state is derived: a flash is "on" until its seq is cleared by the
// hold timeout (setState only ever happens inside the timeout callback)
@@ -47,7 +48,11 @@ export function BeatDot({ size = 44, className }: BeatDotProps) {
className={cn('relative shrink-0 select-none', className)}
style={{ width: size, height: size }}
role="img"
- aria-label={`Beat indicator — ${bpm > 0 ? `${bpm.toFixed(1)} BPM` : 'no tempo'}${flashSource === 'predicted' ? ', predicted' : ''}`}
+ aria-label={
+ idle
+ ? 'Beat indicator — paused, no audio playing'
+ : `Beat indicator — ${bpm > 0 ? `${bpm.toFixed(1)} BPM` : 'no tempo'}${flashSource === 'predicted' ? ', predicted' : ''}`
+ }
>
{/* phase progress ring */}
- {/* filled pulse dot (audio / tap beats) */}
+ {/* filled pulse dot (audio / tap beats) — dimmed while no source is
+ playing/loaded, since beat detection is suspended (v6.4) */}
s.beatFlash)
const beatConfig = useAvaStore((s) => s.beatConfig)
const setBeatValue = useAvaStore((s) => s.setBeatValue)
+ const source = useAvaStore((s) => s.source)
const beat = stats?.beat
const bpm = beat?.bpm ?? 0
@@ -37,11 +38,14 @@ export function BeatTab() {
const beatCount = stats?.beatCount ?? 0
const lastSource = beatFlash?.source ?? 'none'
- const tapFeedback = lastTap
- ? lastTap.locked
- ? `Locked: ${lastTap.bpm.toFixed(1)} BPM`
- : `Tap ${lastTap.tapCount}… keep tapping`
- : 'Tap along with the music — Space works too'
+ const tapFeedback =
+ source === 'idle'
+ ? 'No audio playing — start a source, then tap along'
+ : lastTap
+ ? lastTap.locked
+ ? `Locked: ${lastTap.bpm.toFixed(1)} BPM`
+ : `Tap ${lastTap.tapCount}… keep tapping`
+ : 'Tap along with the music — Space works too'
return (
@@ -74,7 +78,7 @@ export function BeatTab() {
type="button"
whileTap={{ scale: 0.965 }}
onClick={tapBeat}
- className="flex h-14 w-full flex-col items-center justify-center gap-0.5 rounded-xl bg-emerald-400 font-bold text-emerald-950 shadow-[0_0_24px_rgba(52,211,153,0.28)] transition-colors hover:bg-emerald-300"
+ className="flex h-14 w-full flex-col items-center justify-center gap-0.5 rounded-xl bg-emerald-400 font-bold text-emerald-950 glow-setbeat transition-colors hover:bg-emerald-300"
title="Tap to set the tempo (or press Space)"
>
@@ -162,7 +166,9 @@ export function BeatTab() {
How it works: beats are detected from the smoothed bass
energy crossing the sensitivity threshold. Consistent intervals lock a tempo (the confidence meter), and a
phase-locked predictor then keeps firing beats on the learned grid — shown as a hollow ring with a PRED badge —
- whenever the bass disappears. Set Beat teaches the tempo
+ whenever the bass disappears. Detection and prediction only run while a source is playing —
+ they pause (and the BPM resets) whenever no music is loaded.{' '}
+ Set Beat teaches the tempo
directly: tap along and two or more taps lock the period and phase to your rhythm.
diff --git a/src/components/ava/detect-tab.tsx b/src/components/ava/detect-tab.tsx
index 352d4f3..e6b22a0 100755
--- a/src/components/ava/detect-tab.tsx
+++ b/src/components/ava/detect-tab.tsx
@@ -56,8 +56,10 @@ const BANDS: BandSpec[] = [
gainKey: 'midGain',
lowRange: { min: 100, max: 5000, step: 25 },
highRange: { min: 300, max: 8000, step: 50 },
- barClass: 'bg-amber-300',
- textClass: 'text-amber-300',
+ // v6.4: orange (was amber — too close to the yellow bass/highlight in
+ // the royal-purple dark theme; keeps the three VU bands distinct)
+ barClass: 'bg-orange-400',
+ textClass: 'text-orange-400',
},
{
key: 'high',
diff --git a/src/components/ava/footer-bar.tsx b/src/components/ava/footer-bar.tsx
index ece12fb..3b1163b 100755
--- a/src/components/ava/footer-bar.tsx
+++ b/src/components/ava/footer-bar.tsx
@@ -58,7 +58,9 @@ export function FooterBar() {
diff --git a/src/components/ava/header.tsx b/src/components/ava/header.tsx
index fbc7178..be2d923 100755
--- a/src/components/ava/header.tsx
+++ b/src/components/ava/header.tsx
@@ -42,7 +42,7 @@ function SourceSegmentButton({
className={cn(
'flex h-11 items-center gap-1.5 rounded-md px-3 text-xs font-medium transition-colors',
active
- ? 'bg-emerald-400/15 text-emerald-300 shadow-[inset_0_0_0_1px_rgba(52,211,153,0.35)]'
+ ? 'glow-segment bg-emerald-400/15 text-emerald-300'
: tone === 'stop'
? 'text-rose-300/70 hover:bg-zinc-800 hover:text-rose-300'
: 'text-zinc-400 hover:bg-zinc-800 hover:text-zinc-100',
@@ -179,7 +179,7 @@ export function AppHeader() {