diff --git a/public_html/README.md b/public_html/README.md
index 6822bab..dd5d0aa 100644
--- a/public_html/README.md
+++ b/public_html/README.md
@@ -15,12 +15,29 @@ GitHub Pages is free and takes about two minutes:
2. Upload all six files to the root: `index.html`, `manifest.webmanifest`,
`sw.js`, `icon-192.png`, `icon-512.png`, `icon-maskable-512.png`.
3. **Settings → Pages → Source: Deploy from a branch → `main` / `(root)` → Save.**
-4. Wait a minute, then open `https://.github.io/bgclock/` in Chrome on Android.
-5. Chrome menu (⋮) → **Add to Home screen** → **Install**.
+4. Wait a minute, then open `https://.github.io/bgclock/` on your phone.
+5. **Android, Chrome:** menu (⋮) → **Add to Home screen** → **Install**.
+ **iOS, Safari:** Share (□↑) → **Add to Home Screen** → **Add**. It has to be
+ Safari — Chrome and Firefox on iOS can't install a web app.
Open it from the home-screen icon and it runs fullscreen with no browser
chrome, offline, with the screen kept awake while a game is on.
+On iOS a few things behave differently, none of them fatal:
+
+- **Install from Safari, and play from the home-screen icon.** In a Safari tab
+ you lose a strip of the screen to the toolbar, and iOS clears the saved
+ settings of a site you haven't visited for a week. The installed app keeps
+ its own storage and doesn't get pruned that way.
+- **There's no vibration** — iOS gives web pages no haptics at all, so the
+ turn-change buzz is silent there. The sounds still play.
+- **The ringer switch doesn't mute the clock.** The page claims a playback
+ audio session so you can hear the delay tick with the phone silenced, which
+ in exchange pauses any music you had going. If you'd rather keep the music,
+ delete the `navigator.audioSession` line in `index.html`.
+- **Screen wake needs iOS 16.4 or newer.** Below that the screen dims on its
+ usual schedule mid-game.
+
If you're using a different path than `/bgclock/`, edit the `"id"` field in
`manifest.webmanifest` to match, or just delete that line.
@@ -62,7 +79,8 @@ Brass is the colour in the chess.com app, if you want the exact original look.
Changing time or delay resets the clock. Settings are kept in `localStorage`, so
they survive closing the tab, force-quitting the installed app and rebooting the
phone — the app also asks for persistent storage so they aren't evicted when the
-device runs low on space. Only the settings are stored; a game in progress isn't.
+device runs low on space (Chrome honours that request; Safari ignores it). Only
+the settings are stored; a game in progress isn't.
## Sounds
diff --git a/public_html/index.html b/public_html/index.html
index 79811ca..24f62b8 100644
--- a/public_html/index.html
+++ b/public_html/index.html
@@ -7,6 +7,7 @@
+
@@ -40,13 +41,16 @@
font-feature-settings:"tnum" 1;
-webkit-tap-highlight-color:transparent;
-webkit-user-select:none;user-select:none;
+ -webkit-touch-callout:none; /* no iOS long-press callout on a panel */
overscroll-behavior:none;
touch-action:manipulation;
}
/* ---------- frame ---------- */
.app{
- height:100dvh;height:100vh;
+ height:100vh;height:100dvh; /* dvh must win — 100vh on iOS Safari is the
+ large viewport, so the bar and the bottom
+ panel end up under the toolbar */
display:grid;
grid-template-rows:1fr auto 1fr;
}
@@ -193,7 +197,8 @@
padding:8px 22px max(22px,env(safe-area-inset-bottom));
transform:translateY(101%);
transition:transform .26s cubic-bezier(.32,.72,0,1);
- max-height:88dvh;overflow-y:auto;
+ max-height:88vh;max-height:88dvh;overflow-y:auto;
+ overscroll-behavior:contain; /* scrolling the sheet can't rubber-band the app */
box-shadow:0 -18px 50px rgba(0,0,0,.45);
}
.sheet.on{transform:translateY(0)}
@@ -478,8 +483,18 @@
if(ac.state === "suspended") ac.resume();
return ac;
}
+ // iOS silences Web Audio through the ringer switch unless the page claims a
+ // playback session. Trade-off: it also interrupts whatever else is playing,
+ // so it's only claimed the first time the clock actually makes a sound.
+ var claimedSession = false;
+ function claimAudioSession(){
+ if(claimedSession) return;
+ claimedSession = true;
+ try{ if(navigator.audioSession) navigator.audioSession.type = "playback"; }catch(e){}
+ }
function tone(freq, at, dur, peak, type, partial){
var c = audio(); if(!c) return;
+ claimAudioSession();
var t0 = c.currentTime + at;
var o = c.createOscillator(), g = c.createGain();
o.type = type || "sine";
@@ -773,11 +788,22 @@
/* ============ keep the screen awake ============ */
var lock = null;
function keepAwake(){
- if(!navigator.wakeLock) return;
- navigator.wakeLock.request("screen").then(function(l){ lock = l; }).catch(function(){});
+ if(!navigator.wakeLock || lock) return;
+ if(document.visibilityState !== "visible") return; // request() rejects when hidden
+ navigator.wakeLock.request("screen").then(function(l){
+ lock = l;
+ // the OS drops the lock whenever the app goes to the background; without
+ // clearing it here the stale handle blocks every later re-request
+ l.addEventListener("release", function(){ lock = null; });
+ }).catch(function(){});
}
document.addEventListener("visibilitychange", function(){
- if(document.visibilityState === "visible" && !lock) keepAwake();
+ if(document.visibilityState !== "visible") return;
+ keepAwake();
+ // iOS interrupts the audio context on backgrounding; nudge it back so the
+ // flag still sounds even if nobody has tapped since returning
+ // (old webkitAudioContext.resume() returns undefined, so don't chain on it)
+ if(ac && ac.state === "suspended"){ try{ ac.resume(); }catch(e){} }
});
keepAwake();
diff --git a/public_html/sw.js b/public_html/sw.js
index 07b0ebe..692cda0 100644
--- a/public_html/sw.js
+++ b/public_html/sw.js
@@ -1,5 +1,5 @@
/* Cache-first: once installed the clock never touches the network again. */
-const CACHE = "bgclock-v2";
+const CACHE = "bgclock-v3";
const FILES = [
"./",
"./index.html",