diff --git a/public/brand/favicon.svg b/public/brand/favicon.svg
new file mode 100644
index 0000000..b5f29a9
--- /dev/null
+++ b/public/brand/favicon.svg
@@ -0,0 +1,5 @@
+
diff --git a/public/brand/logo-light.svg b/public/brand/logo-light.svg
new file mode 100644
index 0000000..ebc67d3
--- /dev/null
+++ b/public/brand/logo-light.svg
@@ -0,0 +1,10 @@
+
diff --git a/public/brand/logo-mono.svg b/public/brand/logo-mono.svg
new file mode 100644
index 0000000..13a7213
--- /dev/null
+++ b/public/brand/logo-mono.svg
@@ -0,0 +1,11 @@
+
diff --git a/public/brand/logo.svg b/public/brand/logo.svg
new file mode 100644
index 0000000..d5a2de1
--- /dev/null
+++ b/public/brand/logo.svg
@@ -0,0 +1,10 @@
+
diff --git a/public/icons/apple-touch-icon.png b/public/icons/apple-touch-icon.png
new file mode 100644
index 0000000..3a95d68
Binary files /dev/null and b/public/icons/apple-touch-icon.png differ
diff --git a/public/icons/favicon-16.png b/public/icons/favicon-16.png
new file mode 100644
index 0000000..194b301
Binary files /dev/null and b/public/icons/favicon-16.png differ
diff --git a/public/icons/favicon-32.png b/public/icons/favicon-32.png
new file mode 100644
index 0000000..0b612cf
Binary files /dev/null and b/public/icons/favicon-32.png differ
diff --git a/public/icons/icon-192.png b/public/icons/icon-192.png
new file mode 100644
index 0000000..195f7a7
Binary files /dev/null and b/public/icons/icon-192.png differ
diff --git a/public/icons/icon-512.png b/public/icons/icon-512.png
new file mode 100644
index 0000000..480ddf2
Binary files /dev/null and b/public/icons/icon-512.png differ
diff --git a/scripts/icons.mjs b/scripts/icons.mjs
new file mode 100644
index 0000000..1579def
--- /dev/null
+++ b/scripts/icons.mjs
@@ -0,0 +1,99 @@
+// Erzeugt alle Raster-Icons aus den SVG-Vorlagen in public/brand/ (#002).
+//
+// Die PNGs und das favicon.ico werden eingecheckt, damit Build und Docker-Abbild
+// kein sharp zur Laufzeit brauchen. Nach jeder Logo-Aenderung: `pnpm icons`.
+import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
+import { dirname, join } from "node:path";
+import { fileURLToPath } from "node:url";
+import sharp from "sharp";
+
+const WURZEL = join(dirname(fileURLToPath(import.meta.url)), "..");
+const BRAND = join(WURZEL, "public/brand");
+const ICONS = join(WURZEL, "public/icons");
+const APP = join(WURZEL, "src/app");
+
+// Seitenhintergrund (--bg); App-Icons stehen auf dieser Flaeche statt transparent,
+// weil iOS und Android transparente Bereiche sonst schwarz oder weiss fuellen.
+const HINTERGRUND = "#0D1013";
+
+/** Rendert ein SVG scharf in der Zielgroesse (density skaliert die Vektoren statt Pixel). */
+async function rendern(svg, groesse) {
+ return sharp(svg, { density: Math.ceil((72 * groesse) / 32) * 2 })
+ .resize(groesse, groesse)
+ .png()
+ .toBuffer();
+}
+
+/** Logo mittig auf --bg mit Rand, damit es auf Homescreens nicht an die Kante stoesst. */
+async function aufFlaeche(svg, groesse, anteil) {
+ const innen = Math.round(groesse * anteil);
+ const logo = await rendern(svg, innen);
+ return sharp({
+ create: { width: groesse, height: groesse, channels: 4, background: HINTERGRUND },
+ })
+ .composite([{ input: logo, gravity: "center" }])
+ .png()
+ .toBuffer();
+}
+
+/** Minimaler ICO-Container mit PNG-Eintraegen (seit Windows Vista von allen Browsern gelesen). */
+function icoBauen(bilder) {
+ const kopf = Buffer.alloc(6);
+ kopf.writeUInt16LE(0, 0);
+ kopf.writeUInt16LE(1, 2);
+ kopf.writeUInt16LE(bilder.length, 4);
+ const eintraege = [];
+ let versatz = 6 + 16 * bilder.length;
+ for (const { groesse, daten } of bilder) {
+ const e = Buffer.alloc(16);
+ e.writeUInt8(groesse >= 256 ? 0 : groesse, 0);
+ e.writeUInt8(groesse >= 256 ? 0 : groesse, 1);
+ e.writeUInt8(0, 2);
+ e.writeUInt8(0, 3);
+ e.writeUInt16LE(1, 4);
+ e.writeUInt16LE(32, 6);
+ e.writeUInt32LE(daten.length, 8);
+ e.writeUInt32LE(versatz, 12);
+ versatz += daten.length;
+ eintraege.push(e);
+ }
+ return Buffer.concat([kopf, ...eintraege, ...bilder.map((b) => b.daten)]);
+}
+
+async function main() {
+ await mkdir(ICONS, { recursive: true });
+ const logo = await readFile(join(BRAND, "logo.svg"));
+ // Unter 32 px wird die reduzierte Variante ohne Zahlenring und Bolzen genutzt,
+ // sonst verschwimmen die Details zu grauem Rauschen.
+ const favicon = await readFile(join(BRAND, "favicon.svg"));
+
+ const icon192 = await aufFlaeche(logo, 192, 0.72);
+ const icon512 = await aufFlaeche(logo, 512, 0.72);
+ const apple = await aufFlaeche(logo, 180, 0.72);
+ const fav32 = await rendern(favicon, 32);
+ const fav16 = await rendern(favicon, 16);
+ const fav48 = await rendern(favicon, 48);
+
+ await writeFile(join(ICONS, "icon-192.png"), icon192);
+ await writeFile(join(ICONS, "icon-512.png"), icon512);
+ await writeFile(join(ICONS, "apple-touch-icon.png"), apple);
+ await writeFile(join(ICONS, "favicon-32.png"), fav32);
+ await writeFile(join(ICONS, "favicon-16.png"), fav16);
+
+ // Next-Metadaten-Dateien: favicon.ico, icon.svg und apple-icon.png in src/app
+ // erzeugen die -Tags automatisch.
+ await writeFile(
+ join(APP, "favicon.ico"),
+ icoBauen([
+ { groesse: 16, daten: fav16 },
+ { groesse: 32, daten: fav32 },
+ { groesse: 48, daten: fav48 },
+ ]),
+ );
+ await copyFile(join(BRAND, "favicon.svg"), join(APP, "icon.svg"));
+ await writeFile(join(APP, "apple-icon.png"), apple);
+
+ console.log("Icons erzeugt: public/icons/*, src/app/favicon.ico, src/app/icon.svg, src/app/apple-icon.png");
+}
+
+await main();
diff --git a/src/app/apple-icon.png b/src/app/apple-icon.png
new file mode 100644
index 0000000..3a95d68
Binary files /dev/null and b/src/app/apple-icon.png differ
diff --git a/src/app/favicon.ico b/src/app/favicon.ico
new file mode 100644
index 0000000..ffeddae
Binary files /dev/null and b/src/app/favicon.ico differ
diff --git a/src/app/icon.svg b/src/app/icon.svg
new file mode 100644
index 0000000..b5f29a9
--- /dev/null
+++ b/src/app/icon.svg
@@ -0,0 +1,5 @@
+
diff --git a/src/app/manifest.ts b/src/app/manifest.ts
new file mode 100644
index 0000000..7e6ee11
--- /dev/null
+++ b/src/app/manifest.ts
@@ -0,0 +1,20 @@
+import type { MetadataRoute } from "next";
+
+/** Web-App-Manifest; die Icons erzeugt scripts/icons.mjs (#002). */
+export default function manifest(): MetadataRoute.Manifest {
+ return {
+ name: "SSHKeyVault",
+ short_name: "sshkv",
+ description: "Zentrale Verwaltung von SSH-Schlüsseln",
+ lang: "de",
+ start_url: "/",
+ display: "standalone",
+ background_color: "#0D1013",
+ theme_color: "#0D1013",
+ icons: [
+ { src: "/icons/icon-192.png", sizes: "192x192", type: "image/png" },
+ { src: "/icons/icon-512.png", sizes: "512x512", type: "image/png" },
+ { src: "/brand/favicon.svg", sizes: "any", type: "image/svg+xml" },
+ ],
+ };
+}