diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx
index 86a5f6e..b3b95c4 100644
--- a/apps/web/src/App.tsx
+++ b/apps/web/src/App.tsx
@@ -10,6 +10,7 @@ import { ThemeProvider } from '@/lib/theme';
import { IdentityProvider, useIdentityQuery } from '@/lib/identity';
import { LayoutProvider } from '@/lib/layout';
import { PiggyContextProvider } from '@/lib/piggy-context';
+import { PlatformAudioProvider } from '@/lib/audio';
import { Shell } from '@/components/Shell';
import { SignIn } from '@/pages/SignIn';
import { CreateProfile } from '@/pages/CreateProfile';
@@ -85,9 +86,19 @@ export function App() {
void patch('/api/me/preferences', prefs).catch(() => {});
}}
>
-
-
-
+ {/*
+ Above the router, so the music survives navigation AND covers the
+ anonymous Learn page — a share-code visitor gets the same platform
+ character as a member. It never plays unbidden: the browser refuses
+ audio until the page has had a real gesture, so it begins when
+ someone actually starts using the page, and the header control mutes
+ it for good on that device.
+ */}
+
+
+
+
+
{/* Inside ThemeProvider: the host reads the resolved light/dark value. */}
diff --git a/apps/web/src/components/Shell.tsx b/apps/web/src/components/Shell.tsx
index 60c60b2..e6a1a1b 100644
--- a/apps/web/src/components/Shell.tsx
+++ b/apps/web/src/components/Shell.tsx
@@ -29,7 +29,6 @@ import { NavLink } from 'react-router-dom';
import { useIdentity } from '@/lib/identity';
import { useLayout } from '@/lib/layout';
import { visibleNav, type NavItem } from '@/lib/nav';
-import { PlatformAudioProvider } from '@/lib/audio';
import { AppHeader } from './AppHeader';
import { AppSidebar } from './AppSidebar';
import { PiggyDock } from './PiggyDock';
@@ -45,14 +44,6 @@ export function Shell() {
const items = visibleNav(identity);
return (
- /*
- * Music is mounted HERE rather than in App.tsx, so it wraps only the
- * signed-in application. The anonymous Learn page renders outside Shell,
- * and a share-code visitor opening a link someone sent them should not get
- * unexpected audio — that is the one context where it reads as a fault
- * rather than as character.
- */
- item.primary)} />
-
);
}
diff --git a/apps/web/src/pages/Learn.tsx b/apps/web/src/pages/Learn.tsx
index 8f18f61..db803c5 100644
--- a/apps/web/src/pages/Learn.tsx
+++ b/apps/web/src/pages/Learn.tsx
@@ -55,6 +55,7 @@ import { usePageTitle } from '@/lib/title';
import { usePiggyContext } from '@/lib/piggy-context';
import { toPiggyPageRoute } from '@pig/core';
import { Badge, Button, Card, EmptyState, Skeleton } from '@/components/ui';
+import { AudioControl } from '@/components/AudioControl';
import { PiggyMark } from '@/components/PiggyMark';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { AddResourceDialog } from '@/components/learn/AddResourceDialog';
@@ -418,9 +419,15 @@ function AnonFrame({ children }: { children: ReactNode }) {
pig
-
+
+ {/* The anonymous page renders outside Shell, so it does not get the
+ app header — but it DOES get the music, and music with no way to
+ stop it is the worst version of this feature. */}
+
+
+
{children}