diff --git a/README.md b/README.md
index 46c37f1..023a15d 100644
--- a/README.md
+++ b/README.md
@@ -417,14 +417,15 @@ What's included:
| Piece | Detail |
| --- | --- |
| `app/manifest.ts` | `display: standalone`, portrait, theme/background colours, app shortcuts |
-| Icons | 192/512 px, a maskable 512 px icon, and 180/167/152 px `apple-touch-icon`s |
+| Icons | 192/512 px, a maskable 512 px icon, and 180/167/152 px `apple-touch-icon`s, all rendered from `assets/app-icon-source.jpeg` |
| iOS meta | `apple-mobile-web-app-capable`, `-title`, `-status-bar-style`, `viewport-fit=cover` |
| Service worker | `public/sw.js`, **production only**: cached app shell, stale-while-revalidate for static assets, network-first navigations with an `/offline` fallback. `/api/*` is never cached |
| Offline page | `/offline` — "your decks are still here, reconnect to generate" |
| Safe areas | `pb-safe` / `pt-safe` utilities plus `pb-tabbar` so nothing hides behind the tab bar |
-The icons are generated by `scripts/make-icons.mjs` (headless Chromium); it is a one-off tool and
-is not part of the app's dependencies.
+The icons are generated by `scripts/make-icons.mjs` from `assets/app-icon-source.jpeg` (headless
+Chromium); it is a one-off tool and is not part of the app's dependencies. It also writes
+`app/favicon.ico`, the Next.js file convention for the browser tab.
## Security notes
@@ -574,9 +575,11 @@ lib/
public/
sw.js service worker (production only)
icons/ PWA + iOS icons and splash screens
+assets/
+ app-icon-source.jpeg master artwork the icons are rendered from
scripts/
copy-pdf-worker.mjs copies the pdf.js worker to /public (runs on predev/prebuild)
- make-icons.mjs regenerates the PWA/iOS icons (one-off tool)
+ make-icons.mjs regenerates the PWA/iOS icons from the master (one-off tool)
```
---
diff --git a/app/favicon.ico b/app/favicon.ico
index 718d6fe..a1820db 100644
Binary files a/app/favicon.ico and b/app/favicon.ico differ
diff --git a/app/icon.svg b/app/icon.svg
deleted file mode 100644
index 02c15b6..0000000
--- a/app/icon.svg
+++ /dev/null
@@ -1,14 +0,0 @@
-
diff --git a/assets/app-icon-source.jpeg b/assets/app-icon-source.jpeg
new file mode 100644
index 0000000..568724d
Binary files /dev/null and b/assets/app-icon-source.jpeg differ
diff --git a/components/InstallSheet.tsx b/components/InstallSheet.tsx
index 6f370bd..32da146 100644
--- a/components/InstallSheet.tsx
+++ b/components/InstallSheet.tsx
@@ -12,7 +12,6 @@ import {
EllipsisIcon,
PlusIcon,
ShareIcon,
- SnowflakeIcon,
XIcon,
} from "@/components/icons";
import { Spinner } from "@/components/ui";
@@ -269,12 +268,13 @@ export function InstallSheet() {
{/* app card */}
-
-
-
+ className="size-11 shrink-0 rounded-[11px]"
+ />
Freezmo
diff --git a/public/icons/apple-touch-icon-152.png b/public/icons/apple-touch-icon-152.png
index 96925b1..66711f2 100644
Binary files a/public/icons/apple-touch-icon-152.png and b/public/icons/apple-touch-icon-152.png differ
diff --git a/public/icons/apple-touch-icon-167.png b/public/icons/apple-touch-icon-167.png
index 3b1cc53..9830837 100644
Binary files a/public/icons/apple-touch-icon-167.png and b/public/icons/apple-touch-icon-167.png differ
diff --git a/public/icons/apple-touch-icon.png b/public/icons/apple-touch-icon.png
index 1b6ed5e..a63d772 100644
Binary files a/public/icons/apple-touch-icon.png and b/public/icons/apple-touch-icon.png differ
diff --git a/public/icons/favicon-32.png b/public/icons/favicon-32.png
index 9dfc98a..8542258 100644
Binary files a/public/icons/favicon-32.png and b/public/icons/favicon-32.png differ
diff --git a/public/icons/icon-192.png b/public/icons/icon-192.png
index c3db25a..40e85d5 100644
Binary files a/public/icons/icon-192.png and b/public/icons/icon-192.png differ
diff --git a/public/icons/icon-512.png b/public/icons/icon-512.png
index 4d6a8d2..c276345 100644
Binary files a/public/icons/icon-512.png and b/public/icons/icon-512.png differ
diff --git a/public/icons/maskable-512.png b/public/icons/maskable-512.png
index 64f7072..8159539 100644
Binary files a/public/icons/maskable-512.png and b/public/icons/maskable-512.png differ
diff --git a/public/icons/splash-1170x2532.png b/public/icons/splash-1170x2532.png
index b65daf2..7ca01e8 100644
Binary files a/public/icons/splash-1170x2532.png and b/public/icons/splash-1170x2532.png differ
diff --git a/public/icons/splash-1179x2556.png b/public/icons/splash-1179x2556.png
index 1242dee..76f3b3c 100644
Binary files a/public/icons/splash-1179x2556.png and b/public/icons/splash-1179x2556.png differ
diff --git a/public/icons/splash-1284x2778.png b/public/icons/splash-1284x2778.png
index 0e46800..6d84b82 100644
Binary files a/public/icons/splash-1284x2778.png and b/public/icons/splash-1284x2778.png differ
diff --git a/public/icons/splash-1290x2796.png b/public/icons/splash-1290x2796.png
index 818e823..2b9957e 100644
Binary files a/public/icons/splash-1290x2796.png and b/public/icons/splash-1290x2796.png differ
diff --git a/public/icons/splash-1536x2048.png b/public/icons/splash-1536x2048.png
index aa642b2..f1bbb14 100644
Binary files a/public/icons/splash-1536x2048.png and b/public/icons/splash-1536x2048.png differ
diff --git a/public/icons/splash-1668x2388.png b/public/icons/splash-1668x2388.png
index c41ed25..c20c2cc 100644
Binary files a/public/icons/splash-1668x2388.png and b/public/icons/splash-1668x2388.png differ
diff --git a/public/icons/splash-750x1334.png b/public/icons/splash-750x1334.png
index 762f792..af08089 100644
Binary files a/public/icons/splash-750x1334.png and b/public/icons/splash-750x1334.png differ
diff --git a/public/icons/splash-828x1792.png b/public/icons/splash-828x1792.png
index 4520641..f186617 100644
Binary files a/public/icons/splash-828x1792.png and b/public/icons/splash-828x1792.png differ
diff --git a/public/sw.js b/public/sw.js
index f93a9b3..eb2c435 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-v2";
+const CACHE_VERSION = "freezmo-v3";
const SHELL_CACHE = `${CACHE_VERSION}-shell`;
const ASSET_CACHE = `${CACHE_VERSION}-assets`;
diff --git a/scripts/make-icons.mjs b/scripts/make-icons.mjs
index 2e4ab28..c3f16b8 100644
--- a/scripts/make-icons.mjs
+++ b/scripts/make-icons.mjs
@@ -1,64 +1,121 @@
/**
- * Renders the Freezmo app icons (PWA + iOS) with headless Chromium.
- * Run: node scripts/make-icons.mjs (requires the Playwright browser cache)
+ * Renders the Freezmo app icons (PWA + iOS) from the master artwork with
+ * headless Chromium.
+ * Run: node scripts/make-icons.mjs (requires a Playwright Chromium cache)
+ *
+ * Master: assets/app-icon-source.jpeg — a 1024x1024 navy rounded square.
+ * 1. The master is masked at full resolution first (a rounded rect 4px
+ * inside the artwork's own corner radius), so the JPEG's anti-aliased
+ * white edge never survives and small sizes downscale with clean alpha.
+ * 2. "any" icons use that masked master as-is.
+ * Apple touch icons are full-bleed, because iOS paints transparency
+ * black; the artwork is zoomed 20% so its corners fall outside the
+ * square (the master's corners are ~85px, so nothing else is cropped).
+ * The maskable icon keeps the artwork inside the 80% safe circle on a
+ * matching navy field.
+ * Also writes app/favicon.ico, the Next.js file convention for the browser
+ * tab (the larger sizes are declared in app/layout.tsx metadata).
*/
import { chromium } from "playwright-core";
-import { mkdirSync, existsSync } from "node:fs";
+import { mkdirSync, existsSync, readFileSync, writeFileSync } from "node:fs";
const CHROME = process.env.CHROME ?? "/home/martin/.cache/ms-playwright/chromium-1243/chrome-linux64/chrome";
-const OUT = new URL("../public/icons/", import.meta.url).pathname;
+const ROOT = new URL("../", import.meta.url).pathname;
+const OUT = `${ROOT}public/icons/`;
+const SOURCE = `${ROOT}assets/app-icon-source.jpeg`;
mkdirSync(OUT, { recursive: true });
-const SNOWFLAKE = (size, stroke, inset) => `
- `;
+/** Sampled from the master artwork: background colour, size, corner radius. */
+const NAVY = "#101528";
+const SIZE = 1024;
+const CORNER = 253;
+const ERODE = 4; // keeps the clip inside the artwork's anti-aliased edge
-function pageHtml({ size, radius, inset, background, stroke }) {
+const sourceUri = `data:image/jpeg;base64,${readFileSync(SOURCE).toString("base64")}`;
+
+const browser = await chromium.launch({ executablePath: CHROME, args: ["--no-sandbox"] });
+
+/* Stage 1: mask the master at full resolution. */
+const masterHtml = `
+`;
+const masterPage = await browser.newPage({ viewport: { width: SIZE, height: SIZE }, deviceScaleFactor: 1 });
+await masterPage.setContent(masterHtml, { waitUntil: "load" });
+const master = await masterPage.screenshot({ omitBackground: true });
+await masterPage.close();
+const masterUri = `data:image/png;base64,${master.toString("base64")}`;
+
+/* Stage 2: render each target from the masked master. */
+function pageHtml(size, mode) {
+ const img = mode === "bleed" ? sourceUri : masterUri;
return `