diff --git a/README.md b/README.md
index e6292f5..ee02fb2 100644
--- a/README.md
+++ b/README.md
@@ -59,6 +59,12 @@ The APK lands in `app/build/outputs/apk/debug/`. Or just Run ▶ from Android St
> First build compiles llama.cpp from source, so it takes a while and needs
> network (CMake fetches the pinned llama.cpp).
+## Install (sideload)
+
+The debug APK is signed with the debug key, so you can sideload it directly:
+copy `app-debug.apk` to the phone, enable "install unknown apps" for your file
+manager, and tap it. First launch downloads the ~1.9 GB model over Wi-Fi.
+
## Where things live
| What | Where |
@@ -72,12 +78,12 @@ The APK lands in `app/build/outputs/apk/debug/`. Or just Run ▶ from Android St
## TODO / notes
-- **Logo:** juli is on it. The header logo is a placeholder `🐟` in
- `assets/web/index.html` (`#logo`), and there's no launcher icon yet — add an
- `android:icon` in `AndroidManifest.xml` + a `mipmap` when the artwork is ready.
+- **Branding:** launcher icon (adaptive + legacy densities) is generated from
+ `piti-icon.png` via `python3 tools/gen_launcher_icons.py`; the header wordmark is
+ `piti-logo.png` (copied to `assets/web/logo.png`). Source art lives at the repo root.
- The model may emit `…` blocks; the UI strips them from the
display and from history (`stripThink` in `app.js`).
- Bumping the llama.cpp tag? Re-check the C API calls in `llama-jni.cpp` against
that tag's `include/llama.h` — it uses the raw C API directly.
-- Not yet compiled end-to-end in CI; first real build is the acid test for the
- native layer.
+- **Build verified:** `./gradlew assembleDebug` produces a ~13 MB `arm64-v8a` APK
+ (native libs stripped; the ~1.9 GB model downloads on first launch).
diff --git a/tools/gen_launcher_icons.py b/tools/gen_launcher_icons.py
new file mode 100644
index 0000000..6e2c5a0
--- /dev/null
+++ b/tools/gen_launcher_icons.py
@@ -0,0 +1,71 @@
+#!/usr/bin/env python3
+"""Regenerate Android launcher icons from piti-icon.png.
+
+Usage: python3 tools/gen_launcher_icons.py
+Requires: Pillow (pip install Pillow)
+
+Produces, under app/src/main/res/:
+ - mipmap-/ic_launcher.png legacy square (rounded), all densities
+ - mipmap-/ic_launcher_round.png legacy round, all densities
+ - mipmap-/ic_launcher_foreground.png adaptive-icon foreground (padded)
+The adaptive XMLs (mipmap-anydpi-v26/) and the black background drawable are
+checked in and don't need regenerating.
+"""
+import os
+from PIL import Image, ImageDraw
+
+ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+SRC = os.path.join(ROOT, "piti-icon.png")
+RES = os.path.join(ROOT, "app", "src", "main", "res")
+BG = (0, 0, 0, 255) # matches the app's black theme / ic_launcher_background.xml
+
+brain = Image.open(SRC).convert("RGBA")
+
+LEGACY = {"mdpi": 48, "hdpi": 72, "xhdpi": 96, "xxhdpi": 144, "xxxhdpi": 192}
+ADAPTIVE = {"mdpi": 108, "hdpi": 162, "xhdpi": 216, "xxhdpi": 324, "xxxhdpi": 432}
+
+
+def fit(img, box):
+ w, h = img.size
+ s = box / max(w, h)
+ return img.resize((max(1, int(w * s)), max(1, int(h * s))), Image.LANCZOS)
+
+
+def rounded_mask(size, radius):
+ m = Image.new("L", (size, size), 0)
+ ImageDraw.Draw(m).rounded_rectangle([0, 0, size - 1, size - 1], radius=radius, fill=255)
+ return m
+
+
+def circle_mask(size):
+ m = Image.new("L", (size, size), 0)
+ ImageDraw.Draw(m).ellipse([0, 0, size - 1, size - 1], fill=255)
+ return m
+
+
+def compose(size, mask, content_scale):
+ canvas = Image.new("RGBA", (size, size), BG)
+ fg = fit(brain, int(size * content_scale))
+ canvas.paste(fg, ((size - fg.width) // 2, (size - fg.height) // 2), fg)
+ out = Image.new("RGBA", (size, size), (0, 0, 0, 0))
+ out.paste(canvas, (0, 0), mask)
+ return out
+
+
+def save(img, folder, name):
+ d = os.path.join(RES, folder)
+ os.makedirs(d, exist_ok=True)
+ img.save(os.path.join(d, name))
+
+
+for bucket, size in LEGACY.items():
+ save(compose(size, rounded_mask(size, int(size * 0.18)), 0.86), f"mipmap-{bucket}", "ic_launcher.png")
+ save(compose(size, circle_mask(size), 0.86), f"mipmap-{bucket}", "ic_launcher_round.png")
+
+for bucket, size in ADAPTIVE.items():
+ fg_canvas = Image.new("RGBA", (size, size), (0, 0, 0, 0))
+ fg = fit(brain, int(size * 0.64)) # keep within the adaptive-icon safe zone
+ fg_canvas.paste(fg, ((size - fg.width) // 2, (size - fg.height) // 2), fg)
+ save(fg_canvas, f"mipmap-{bucket}", "ic_launcher_foreground.png")
+
+print("launcher icons regenerated under app/src/main/res/")