diff --git a/web/src/App.tsx b/web/src/App.tsx
index 77a0aaa..c80ab0f 100644
--- a/web/src/App.tsx
+++ b/web/src/App.tsx
@@ -446,6 +446,7 @@ export function App() {
return (
diff --git a/web/src/styles/mobile.css b/web/src/styles/mobile.css
index 04a39de..6a85767 100644
--- a/web/src/styles/mobile.css
+++ b/web/src/styles/mobile.css
@@ -13,6 +13,22 @@
--m-dock-clearance: calc(96px + env(safe-area-inset-bottom));
}
+/* iOS can leave the layout viewport shorter than the screen - after the keyboard has
+ been up, `100dvh` in a home-screen app comes back one status bar short - and the
+ page behind the stage showed as a white strip along the bottom. Pinning the stage to
+ the viewport's edges does not depend on that height at all, and the sea colour
+ underneath covers whatever might still peek through. */
+html,
+body {
+ background: var(--stage-deep, #001b21);
+}
+
+.stage[data-phone] {
+ position: fixed;
+ inset: 0;
+ height: auto;
+}
+
.m-glass {
background: var(--m-glass-fill);
backdrop-filter: blur(14px) saturate(1.7);
@@ -39,7 +55,8 @@
overflow-y: auto;
overscroll-behavior: contain;
-webkit-overflow-scrolling: touch;
- padding: env(safe-area-inset-top) 0 calc(var(--m-dock-clearance) + 84px);
+ /* No top inset here: the sticky bar reaches up under the status bar itself. */
+ padding: 0 0 calc(var(--m-dock-clearance) + 84px);
animation: viewEnter 180ms ease-out;
}
@@ -49,7 +66,7 @@
display: flex;
align-items: center;
gap: 10px;
- padding: 14px var(--m-gutter) 4px;
+ padding: calc(14px + env(safe-area-inset-top)) var(--m-gutter) 4px;
}
.m-title-mascot {
@@ -82,21 +99,45 @@
color: oklch(80% 0.16 30);
}
-/* The bar that stays put while the page scrolls under it. Fades from the top of the
- sea into transparency rather than ending in a hard edge. */
+/* The bar that stays put while the page scrolls under it. Sticks to the very top of
+ the screen, under the status bar, so content never shows behind the clock.
+
+ Frosted rather than painted: the background behind it is the ambience canvas,
+ which takes its colour from whatever is playing, so any fixed fill was the wrong
+ colour as soon as an album with different art started. A blur with a neutral
+ darkening suits every one of them, and the mask fades it out instead of ending in
+ a hard edge. */
.m-sticky {
position: sticky;
top: 0;
z-index: 2;
- padding: 10px var(--m-gutter) 14px;
- background: linear-gradient(180deg, #2f6f7aee 0%, #2f6f7a99 65%, #2f6f7a00 100%);
+ padding: calc(10px + env(safe-area-inset-top)) var(--m-gutter) 14px;
+}
+
+/* Under the title row, overlap the row by the status bar's height: the bar keeps its
+ place in the layout but already covers the status bar once it sticks. */
+.m-title-row + .m-sticky {
+ margin-top: calc(-1 * env(safe-area-inset-top));
+}
+
+.m-sticky::before {
+ content: "";
+ position: absolute;
+ inset: 0;
+ z-index: -1;
+ background: oklch(0% 0 0 / 0.16);
+ backdrop-filter: blur(12px);
+ -webkit-backdrop-filter: blur(12px);
+ -webkit-mask-image: linear-gradient(180deg, #000 70%, transparent);
+ mask-image: linear-gradient(180deg, #000 70%, transparent);
+ pointer-events: none;
}
.m-navbar {
display: flex;
align-items: center;
gap: 10px;
- padding-top: 12px;
+ padding-top: calc(12px + env(safe-area-inset-top));
}
.m-navbar-title {