diff --git a/icon-512.png b/icon-512.png
new file mode 100644
index 0000000..817c09d
Binary files /dev/null and b/icon-512.png differ
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..fefd266
--- /dev/null
+++ b/index.html
@@ -0,0 +1,1247 @@
+
+
+
+
+
+
+
+
+
+
+
+
+Business Trip Day Tracker
+
+
+
+
+Saved
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/manifest.webmanifest b/manifest.webmanifest
new file mode 100644
index 0000000..59cb231
--- /dev/null
+++ b/manifest.webmanifest
@@ -0,0 +1,15 @@
+{
+ "name": "Business Trip Day Tracker",
+ "short_name": "Trip Log",
+ "description": "Offline day-by-day record of a business trip for tax substantiation.",
+ "start_url": "./index.html",
+ "scope": "./",
+ "display": "standalone",
+ "orientation": "portrait",
+ "background_color": "#0b1220",
+ "theme_color": "#0b1220",
+ "icons": [
+ { "src": "icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any" },
+ { "src": "icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
+ ]
+}
diff --git a/sw.js b/sw.js
new file mode 100644
index 0000000..5cfaf16
--- /dev/null
+++ b/sw.js
@@ -0,0 +1,53 @@
+/* Business Trip Day Tracker — offline shell.
+ Cache-first on purpose: this app is a static document whose whole point is
+ working in airplane mode, so a stale-by-a-day shell beats a failed load.
+ Bump CACHE when index.html changes; activate() drops every older cache. */
+var CACHE = "trip-tracker-v3";
+var ASSETS = ["./", "./index.html", "./manifest.webmanifest", "./icon-512.png"];
+
+self.addEventListener("install", function(e){
+ e.waitUntil(
+ caches.open(CACHE).then(function(c){
+ /* addAll is all-or-nothing; one 404 would leave us with no offline copy
+ at all, so fetch them individually and keep whatever succeeds. */
+ return Promise.all(ASSETS.map(function(u){
+ return fetch(u, {cache:"reload"})
+ .then(function(r){ if(r && r.ok) return c.put(u, r); })
+ .catch(function(){});
+ }));
+ }).then(function(){ return self.skipWaiting(); })
+ );
+});
+
+self.addEventListener("activate", function(e){
+ e.waitUntil(
+ caches.keys().then(function(keys){
+ return Promise.all(keys.map(function(k){
+ return k === CACHE ? null : caches.delete(k);
+ }));
+ }).then(function(){ return self.clients.claim(); })
+ );
+});
+
+self.addEventListener("fetch", function(e){
+ var req = e.request;
+ if(req.method !== "GET") return;
+ if(new URL(req.url).origin !== self.location.origin) return;
+
+ e.respondWith(
+ caches.match(req, {ignoreSearch:true}).then(function(hit){
+ if(hit) return hit;
+ return fetch(req).then(function(res){
+ if(res && res.ok){
+ var copy = res.clone();
+ caches.open(CACHE).then(function(c){ c.put(req, copy); });
+ }
+ return res;
+ }).catch(function(){
+ /* Offline and not cached — a navigation still gets the app shell. */
+ if(req.mode === "navigate") return caches.match("./index.html");
+ throw new Error("offline");
+ });
+ })
+ );
+});