diff --git a/lyra/web/gen_icons.py b/lyra/web/gen_icons.py
new file mode 100644
index 0000000..6650122
--- /dev/null
+++ b/lyra/web/gen_icons.py
@@ -0,0 +1,75 @@
+#!/usr/bin/env python3
+"""Generate Lyra PWA icons with no third-party deps (pure stdlib PNG writer).
+
+Design: RTO warm/low-glow — near-black field, a soft orange ambient glow, and a
+luminous gold-orange ring (the "orb/portal"). iOS masks corners itself, so icons
+are full-bleed squares. Run from anywhere; writes PNGs into ./static.
+"""
+import math
+import os
+import struct
+import zlib
+
+HERE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "static")
+
+BG = (7, 7, 7) # #070707
+ORANGE = (255, 122, 0) # #ff7a00 accent
+GOLD = (255, 179, 71) # #ffb347 hot core
+
+
+def _png(width, height, rgb_rows):
+ def chunk(tag, data):
+ return (struct.pack(">I", len(data)) + tag + data
+ + struct.pack(">I", zlib.crc32(tag + data) & 0xFFFFFFFF))
+
+ raw = bytearray()
+ for row in rgb_rows:
+ raw.append(0) # filter type 0 (None)
+ raw.extend(row)
+ ihdr = struct.pack(">IIBBBBB", width, height, 8, 2, 0, 0, 0) # 8-bit RGB
+ return (b"\x89PNG\r\n\x1a\n"
+ + chunk(b"IHDR", ihdr)
+ + chunk(b"IDAT", zlib.compress(bytes(raw), 9))
+ + chunk(b"IEND", b""))
+
+
+def render(n):
+ c = (n - 1) / 2.0
+ sigma_glow = n * 0.30
+ ring_r = n * 0.30
+ ring_w = n * 0.050
+ core_sigma = n * 0.11
+ rows = []
+ for y in range(n):
+ row = bytearray()
+ for x in range(n):
+ dx, dy = x - c, y - c
+ d = math.hypot(dx, dy)
+ r, g, b = BG
+ # ambient orange glow
+ glow = math.exp(-(d * d) / (2 * sigma_glow * sigma_glow)) * 0.50
+ # soft hot core
+ core = math.exp(-(d * d) / (2 * core_sigma * core_sigma)) * 0.45
+ # luminous ring
+ rr = d - ring_r
+ ring = math.exp(-(rr * rr) / (2 * ring_w * ring_w))
+ r += ORANGE[0] * glow + GOLD[0] * (ring + core)
+ g += ORANGE[1] * glow + GOLD[1] * (ring + core)
+ b += ORANGE[2] * glow + GOLD[2] * (ring + core)
+ row += bytes((min(255, int(r)), min(255, int(g)), min(255, int(b))))
+ rows.append(row)
+ return rows
+
+
+def write(name, n):
+ rows = render(n)
+ with open(os.path.join(HERE, name), "wb") as f:
+ f.write(_png(n, n, rows))
+ print(f"wrote {name} ({n}x{n})")
+
+
+if __name__ == "__main__":
+ write("icon-512.png", 512)
+ write("icon-192.png", 192)
+ write("apple-touch-icon.png", 180)
+ write("icon-maskable-512.png", 512)
diff --git a/lyra/web/static/apple-touch-icon.png b/lyra/web/static/apple-touch-icon.png
new file mode 100644
index 0000000..2a53470
Binary files /dev/null and b/lyra/web/static/apple-touch-icon.png differ
diff --git a/lyra/web/static/icon-192.png b/lyra/web/static/icon-192.png
new file mode 100644
index 0000000..dc79f75
Binary files /dev/null and b/lyra/web/static/icon-192.png differ
diff --git a/lyra/web/static/icon-512.png b/lyra/web/static/icon-512.png
new file mode 100644
index 0000000..64f7d81
Binary files /dev/null and b/lyra/web/static/icon-512.png differ
diff --git a/lyra/web/static/icon-maskable-512.png b/lyra/web/static/icon-maskable-512.png
new file mode 100644
index 0000000..64f7d81
Binary files /dev/null and b/lyra/web/static/icon-maskable-512.png differ
diff --git a/lyra/web/static/index.html b/lyra/web/static/index.html
index 801a93b..df3670e 100644
--- a/lyra/web/static/index.html
+++ b/lyra/web/static/index.html
@@ -5,10 +5,14 @@
Lyra Core Chat
-
+
+
+
+
+
@@ -55,6 +59,8 @@
+ Lyra
+