diff --git a/README.md b/README.md
index e76b92b..e30fa94 100644
--- a/README.md
+++ b/README.md
@@ -1492,6 +1492,17 @@ Things I chose while building, with the reasoning:
into view as you type. No strings, so no i18n; the additive change sits on top of the merged
scene-heat rework unchanged.
+107. **The Aero soundtrack button rides above the podcast player.** "The music thing button with
+ the Frutiger Aero doesn't work on the podcasts": the podcast page is a full-screen overlay at
+ `z-45` and the music control sat at `z-40` — visible through the transparent page, but every
+ tap landed on the overlay. The control now takes a `data-raised="player"` position on
+ `/podcast/[id]`: `z-index: 46`, docked to the header's corner (`top` under the header pill,
+ `right` mirroring the `max-w-3xl` geometry) where neither the transport bar nor the voice panel
+ lives, and the transcript gained `pt-32` so the episode card clears it. The study/review/exam
+ feeds keep the "steps aside" behaviour — their bottom corners are spoken for. aero-music-test
+ grew a podcast section that asserts the control is there, that `elementFromPoint` at its centre
+ hits the button (nothing covers it) and that a tap expands the credit bar (31 checks).
+
106. **When the browser has no speech engine at all.** "The podcasts don't even work on my Linux
setup on Firefox": Firefox on Linux speaks through speech-dispatcher, and without it
`speechSynthesis` is *present* (so `speechAvailable()` says yes) but empty and silent —
diff --git a/app/globals.css b/app/globals.css
index d8c4088..eaa8e5c 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -933,6 +933,15 @@ body:has(.overlay-open) :is(.app-header, .segmented, .tabbar, .cookie-notice) {
.aero-music[data-raised="wizard"] {
bottom: calc(5.5rem + env(safe-area-inset-bottom, 0px));
}
+/* The podcast player is a full-screen overlay (z-45): the music control has
+ to ride above it, and it moves to the header's corner — the transport and
+ the voice panel own the bottom of that screen. */
+.aero-music[data-raised="player"] {
+ top: calc(max(env(safe-area-inset-top), 0.75rem) + 3.9rem);
+ right: max(0.75rem, calc((100vw - 48rem) / 2 + 0.75rem));
+ bottom: auto;
+ z-index: 46;
+}
body:has(.overlay-open) .aero-music,
/* The one-time notice shares this corner; it wins while it is up. */
body:has(.cookie-notice) .aero-music {
diff --git a/components/AeroMusic.tsx b/components/AeroMusic.tsx
index 09549a2..0d3cf95 100644
--- a/components/AeroMusic.tsx
+++ b/components/AeroMusic.tsx
@@ -205,7 +205,14 @@ export function AeroMusic() {
/* The composer sits at the bottom of the chat screen and the wizard has its
own action bar; everywhere else the phone tab bar is the only neighbour. */
- const raised = pathname === "/chat" ? "chat" : pathname === "/create" || pathname === "/homework" ? "wizard" : undefined;
+ const raised =
+ pathname.startsWith("/podcast/")
+ ? "player"
+ : pathname === "/chat"
+ ? "chat"
+ : pathname === "/create" || pathname === "/homework"
+ ? "wizard"
+ : undefined;
return (
diff --git a/components/podcast/PodcastPlayer.tsx b/components/podcast/PodcastPlayer.tsx
index e2fe922..277cee9 100644
--- a/components/podcast/PodcastPlayer.tsx
+++ b/components/podcast/PodcastPlayer.tsx
@@ -585,7 +585,9 @@ export function PodcastPlayer({ episode }: { episode: Podcast }) {
const index = Number(target.dataset.word);
if (Number.isInteger(index)) jumpWord(index);
}}
- className="no-scrollbar min-h-0 flex-1 overflow-y-auto overscroll-contain px-4 pb-40 pt-24"
+ /* pt-32 clears the Aero music control, which rides above this overlay
+ in the header's corner while the theme is on. */
+ className="no-scrollbar min-h-0 flex-1 overflow-y-auto overscroll-contain px-4 pb-40 pt-32"
>
{naturalError ? (
diff --git a/package.json b/package.json
index bb2bde2..31e1cd2 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "freezmo",
- "version": "0.7.8",
+ "version": "0.7.9",
"private": true,
"license": "GPL-3.0-only",
"scripts": {
diff --git a/public/sw.js b/public/sw.js
index f46b661..76cf046 100644
--- a/public/sw.js
+++ b/public/sw.js
@@ -9,7 +9,7 @@
*
* Bumping CACHE_VERSION is how a new deploy invalidates old assets.
*/
-const CACHE_VERSION = "freezmo-v100";
+const CACHE_VERSION = "freezmo-v101";
const SHELL_CACHE = `${CACHE_VERSION}-shell`;
const ASSET_CACHE = `${CACHE_VERSION}-assets`;
diff --git a/tests/aero-music-test.mjs b/tests/aero-music-test.mjs
index e5d8bed..ad18cbc 100644
--- a/tests/aero-music-test.mjs
+++ b/tests/aero-music-test.mjs
@@ -208,6 +208,47 @@ async function stubSoundCloud(page) {
await context.close();
}
+/* 4b. the podcast player: the control rides above that overlay -------- */
+{
+ const { context, page } = await makePage(browser, { width: 390, height: 844, deviceScaleFactor: 1, isMobile: true, hasTouch: true });
+ await stubSoundCloud(page);
+ await seed(page, {
+ theme: "aero",
+ decks: [],
+ extra: {
+ "freezmo.podcasts.v1": JSON.stringify([
+ {
+ id: "pod_music",
+ title: "While you listen",
+ summary: "",
+ language: "English",
+ sourceText: "x",
+ createdAt: 1,
+ updatedAt: 1,
+ sections: [{ heading: "One", text: "Look up on a clear day." }],
+ },
+ ]),
+ },
+ });
+ await page.goto(`${BASE}/podcast/pod_music`, { waitUntil: "networkidle" });
+ await page.waitForTimeout(1500);
+
+ const circle = page.getByRole("button", { name: "Frutiger Aero music" });
+ check("podcast: the music control is there while listening", (await circle.count()) === 1);
+ const reachable = await page.evaluate(() => {
+ const button = document.querySelector("[data-testid=aero-music] button");
+ if (!button) return "missing";
+ const box = button.getBoundingClientRect();
+ const top = document.elementFromPoint(box.x + box.width / 2, box.y + box.height / 2);
+ return button.contains(top) || top === button ? "on-top" : `covered by ${top?.className?.toString().slice(0, 40)}`;
+ });
+ check("podcast: nothing covers it (it rides above the overlay)", reachable === "on-top", reachable);
+ await circle.click();
+ await page.waitForTimeout(400);
+ check("podcast: tapping it expands the credit bar", (await page.getByText("SoundCloud ·", { exact: false }).count()) >= 1);
+ await context.close();
+}
+
/* 5. immersive overlays step aside, the music keeps playing ----------- */
{
const { context, page } = await makePage(browser, { width: 1280, height: 731 });