diff --git a/favicon.svg b/favicon.svg
index 976ca42..4ebff3b 100644
--- a/favicon.svg
+++ b/favicon.svg
@@ -1 +1,8 @@
-
+
diff --git a/icons/icon.html b/icons/icon.html
new file mode 100644
index 0000000..e890749
--- /dev/null
+++ b/icons/icon.html
@@ -0,0 +1,5 @@
+
+
+
icon
+
+
diff --git a/icons/icon.svg b/icons/icon.svg
new file mode 100644
index 0000000..fa854cd
--- /dev/null
+++ b/icons/icon.svg
@@ -0,0 +1,8 @@
+
diff --git a/manifest.json b/manifest.json
new file mode 100644
index 0000000..6af38ba
--- /dev/null
+++ b/manifest.json
@@ -0,0 +1,16 @@
+{
+ "name": "gandora",
+ "short_name": "gandora",
+ "description": "Type a seed. Get a station. Every song is made for you as it plays.",
+ "start_url": "/",
+ "scope": "/",
+ "display": "standalone",
+ "orientation": "portrait",
+ "background_color": "#0b0b0e",
+ "theme_color": "#0b0b0e",
+ "icons": [
+ { "src": "icons/icon.svg", "sizes": "any", "type": "image/svg+xml", "purpose": "any maskable" },
+ { "src": "icons/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any maskable" },
+ { "src": "icons/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any maskable" }
+ ]
+}
diff --git a/sw.js b/sw.js
new file mode 100644
index 0000000..2ca8288
--- /dev/null
+++ b/sw.js
@@ -0,0 +1,13 @@
+// gandora's service worker: the shell is cached so the app opens and the library plays with no signal.
+// Tracks live in IndexedDB already; the DJ and new renders need the network and the page says so.
+const SHELL = "gandora-shell-v1";
+const FILES = ["/", "/index.html", "/brain.js", "/favicon.svg", "/manifest.json", "/icons/icon.svg"];
+self.addEventListener("install", (e) => { e.waitUntil(caches.open(SHELL).then((c) => c.addAll(FILES)).then(() => self.skipWaiting())); });
+self.addEventListener("activate", (e) => { e.waitUntil(caches.keys().then((ks) => Promise.all(ks.filter((k) => k !== SHELL).map((k) => caches.delete(k)))).then(() => self.clients.claim())); });
+// same-origin GETs: network first (so a deploy shows up on the next open), cache as the fallback; everything else untouched
+self.addEventListener("fetch", (e) => {
+ const u = new URL(e.request.url);
+ if (e.request.method !== "GET" || u.origin !== location.origin) return;
+ e.respondWith(fetch(e.request).then((r) => { if (r.ok) caches.open(SHELL).then((c) => c.put(e.request, r.clone())); return r; })
+ .catch(() => caches.match(e.request, { ignoreSearch: true }).then((r) => r || caches.match("/"))));
+});