diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml
index 07d5da9..d204a4e 100644
--- a/app/src/main/res/drawable/ic_launcher_background.xml
+++ b/app/src/main/res/drawable/ic_launcher_background.xml
@@ -4,167 +4,25 @@
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
+
+ android:fillColor="#102B2F"
+ android:pathData="M0,0 L108,0 L108,108 L0,108 Z" />
+
+
+ android:fillColor="#163A40"
+ android:pathData="M0,0h108v44c-18,6 -34,12 -50,24c-18,13 -33,29 -58,34z" />
+
+
+ android:fillColor="#1E6D70"
+ android:pathData="M74,-8h42v124h-72z"
+ android:fillAlpha="0.55" />
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ android:fillColor="#8DE6D5"
+ android:pathData="M0,82c17,-10 29,-14 45,-14c24,0 37,12 63,5v35h-108z"
+ android:fillAlpha="0.16" />
diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml
index 2b068d1..1469602 100644
--- a/app/src/main/res/drawable/ic_launcher_foreground.xml
+++ b/app/src/main/res/drawable/ic_launcher_foreground.xml
@@ -1,30 +1,44 @@
+
-
-
-
-
-
-
-
-
+
-
\ No newline at end of file
+ android:fillColor="#000000"
+ android:pathData="M1,40 L109,40 A0,0 0,0 1 109,40 L109,72 A0,0 0,0 1 109,72 L1,72 A0,0 0,0 1 1,72 L1,40 A0,0 0,0 1 1,40 Z"
+ android:fillAlpha="0.33" />
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/scripts/generate_launcher_icon.py b/scripts/generate_launcher_icon.py
new file mode 100644
index 0000000..f0ef5ef
--- /dev/null
+++ b/scripts/generate_launcher_icon.py
@@ -0,0 +1,323 @@
+#!/usr/bin/env python3
+"""Generate the NavButtons launcher icon and an SVG browser preview.
+
+Edit the constants below, run this script, then open:
+
+ build/launcher-icon-preview.svg
+
+The Android VectorDrawable files are generated from the same geometry, so the
+browser preview and app icon stay in sync.
+"""
+
+from __future__ import annotations
+
+from dataclasses import dataclass
+from pathlib import Path
+from xml.sax.saxutils import escape
+
+ROOT = Path(__file__).resolve().parents[1]
+
+FOREGROUND = ROOT / "app/src/main/res/drawable/ic_launcher_foreground.xml"
+BACKGROUND = ROOT / "app/src/main/res/drawable/ic_launcher_background.xml"
+PREVIEW = ROOT / "build/launcher-icon-preview.svg"
+
+VIEWPORT = 108
+PREVIEW_SCALE = 4
+
+# Horizontal white bar / pill.
+BAR_X = 0
+BAR_Y = 38
+BAR_WIDTH = 108
+BAR_HEIGHT = 32
+BAR_RADIUS = 0
+BAR_FILL = "#EFFFFB"
+BAR_SHADOW_X = 1
+BAR_SHADOW_Y = 2
+BAR_SHADOW_FILL = "#000000"
+BAR_SHADOW_ALPHA = 0.33
+
+# Each symbol uses center x/y plus total width/height and stroke width.
+# The Back icon is a hollow triangle, Home is a hollow circle, Recents is a
+# hollow square. Coordinates are in the 108x108 adaptive-icon viewport.
+ALL_SIZE = 20
+
+BACK_X = 54-27
+BACK_Y = 54
+BACK_WIDTH = (ALL_SIZE**2 - (ALL_SIZE*0.5)**2)**0.5
+BACK_HEIGHT = ALL_SIZE
+BACK_STROKE = 6
+BACK_X = BACK_X + (ALL_SIZE - BACK_WIDTH)*0.5
+
+HOME_X = 54
+HOME_Y = 54
+HOME_WIDTH = ALL_SIZE
+HOME_HEIGHT = ALL_SIZE
+HOME_STROKE = 6
+
+RECENTS_X = 54+27
+RECENTS_Y = 54
+RECENTS_WIDTH = ALL_SIZE
+RECENTS_HEIGHT = ALL_SIZE
+RECENTS_STROKE = 6
+
+SYMBOL_COLOR = "#1B454A"
+
+# Background.
+BG_BASE = "#102B2F"
+BG_CURVE = "#163A40"
+BG_SLASH = "#1E6D70"
+BG_SLASH_ALPHA = 0.55
+BG_WAVE = "#8DE6D5"
+BG_WAVE_ALPHA = 0.16
+
+# Adaptive icon safe-zone preview. Android guarantees roughly the central
+# 72x72 area survives masks, so this uses radius 36.
+SAFE_RADIUS = 36
+
+
+@dataclass(frozen=True)
+class Style:
+ fill: str = "#00000000"
+ fill_alpha: float | None = None
+ stroke: str | None = None
+ stroke_width: float | None = None
+ line_cap: str | None = None
+ line_join: str | None = None
+
+
+@dataclass(frozen=True)
+class IconPath:
+ data: str
+ style: Style
+ comment: str | None = None
+
+
+def fmt(value: float) -> str:
+ return f"{value:g}"
+
+
+def path(
+ data: str,
+ fill: str = "#00000000",
+ *,
+ fill_alpha: float | None = None,
+ stroke: str | None = None,
+ stroke_width: float | None = None,
+ line_cap: str | None = None,
+ line_join: str | None = None,
+ comment: str | None = None,
+) -> IconPath:
+ return IconPath(data, Style(fill, fill_alpha, stroke, stroke_width, line_cap, line_join), comment)
+
+
+def rect(x: float, y: float, width: float, height: float) -> str:
+ return (
+ f"M{fmt(x)},{fmt(y)} "
+ f"L{fmt(x + width)},{fmt(y)} "
+ f"L{fmt(x + width)},{fmt(y + height)} "
+ f"L{fmt(x)},{fmt(y + height)} Z"
+ )
+
+
+def round_rect(x: float, y: float, width: float, height: float, radius: float) -> str:
+ radius = min(radius, width / 2, height / 2)
+ return (
+ f"M{fmt(x + radius)},{fmt(y)} "
+ f"L{fmt(x + width - radius)},{fmt(y)} "
+ f"A{fmt(radius)},{fmt(radius)} 0,0 1 {fmt(x + width)},{fmt(y + radius)} "
+ f"L{fmt(x + width)},{fmt(y + height - radius)} "
+ f"A{fmt(radius)},{fmt(radius)} 0,0 1 {fmt(x + width - radius)},{fmt(y + height)} "
+ f"L{fmt(x + radius)},{fmt(y + height)} "
+ f"A{fmt(radius)},{fmt(radius)} 0,0 1 {fmt(x)},{fmt(y + height - radius)} "
+ f"L{fmt(x)},{fmt(y + radius)} "
+ f"A{fmt(radius)},{fmt(radius)} 0,0 1 {fmt(x + radius)},{fmt(y)} Z"
+ )
+
+
+def circle(cx: float, cy: float, width: float, height: float) -> str:
+ rx = width / 2
+ ry = height / 2
+ return (
+ f"M{fmt(cx - rx)},{fmt(cy)} "
+ f"A{fmt(rx)},{fmt(ry)} 0,1 0 {fmt(cx + rx)},{fmt(cy)} "
+ f"A{fmt(rx)},{fmt(ry)} 0,1 0 {fmt(cx - rx)},{fmt(cy)}"
+ )
+
+
+def triangle_left(cx: float, cy: float, width: float, height: float) -> str:
+ left = cx - width / 2
+ right = cx + width / 2
+ top = cy - height / 2
+ bottom = cy + height / 2
+ return f"M{fmt(left)},{fmt(cy)} L{fmt(right)},{fmt(top)} L{fmt(right)},{fmt(bottom)} Z"
+
+
+def stroke_style(width: float) -> dict[str, object]:
+ return {
+ "stroke": SYMBOL_COLOR,
+ "stroke_width": width,
+ "line_cap": "round",
+ "line_join": "round",
+ }
+
+
+def background_paths() -> list[IconPath]:
+ return [
+ path(rect(0, 0, VIEWPORT, VIEWPORT), BG_BASE, comment="Base"),
+ path(
+ "M0,0h108v44c-18,6 -34,12 -50,24c-18,13 -33,29 -58,34z",
+ BG_CURVE,
+ comment="Soft curve",
+ ),
+ path(
+ "M74,-8h42v124h-72z",
+ BG_SLASH,
+ fill_alpha=BG_SLASH_ALPHA,
+ comment="Diagonal accent",
+ ),
+ path(
+ "M0,82c17,-10 29,-14 45,-14c24,0 37,12 63,5v35h-108z",
+ BG_WAVE,
+ fill_alpha=BG_WAVE_ALPHA,
+ comment="Bottom wave",
+ ),
+ ]
+
+
+def foreground_paths() -> list[IconPath]:
+ return [
+ path(
+ round_rect(BAR_X + BAR_SHADOW_X, BAR_Y + BAR_SHADOW_Y, BAR_WIDTH, BAR_HEIGHT, BAR_RADIUS),
+ BAR_SHADOW_FILL,
+ fill_alpha=BAR_SHADOW_ALPHA,
+ comment="Bar shadow",
+ ),
+ path(
+ round_rect(BAR_X, BAR_Y, BAR_WIDTH, BAR_HEIGHT, BAR_RADIUS),
+ BAR_FILL,
+ comment="Bar",
+ ),
+ path(
+ triangle_left(BACK_X, BACK_Y, BACK_WIDTH, BACK_HEIGHT),
+ comment="Back",
+ **stroke_style(BACK_STROKE),
+ ),
+ path(
+ circle(HOME_X, HOME_Y, HOME_WIDTH, HOME_HEIGHT),
+ comment="Home",
+ **stroke_style(HOME_STROKE),
+ ),
+ path(
+ rect(
+ RECENTS_X - RECENTS_WIDTH / 2,
+ RECENTS_Y - RECENTS_HEIGHT / 2,
+ RECENTS_WIDTH,
+ RECENTS_HEIGHT,
+ ),
+ comment="Recents",
+ **stroke_style(RECENTS_STROKE),
+ ),
+ ]
+
+
+def android_path_xml(icon_path: IconPath) -> str:
+ attrs = [
+ f'android:fillColor="{icon_path.style.fill}"',
+ f'android:pathData="{escape(icon_path.data)}"',
+ ]
+ if icon_path.style.fill_alpha is not None:
+ attrs.append(f'android:fillAlpha="{fmt(icon_path.style.fill_alpha)}"')
+ if icon_path.style.stroke:
+ attrs.append(f'android:strokeColor="{icon_path.style.stroke}"')
+ if icon_path.style.stroke_width is not None:
+ attrs.append(f'android:strokeWidth="{fmt(icon_path.style.stroke_width)}"')
+ if icon_path.style.line_cap:
+ attrs.append(f'android:strokeLineCap="{icon_path.style.line_cap}"')
+ if icon_path.style.line_join:
+ attrs.append(f'android:strokeLineJoin="{icon_path.style.line_join}"')
+ return " "
+
+
+def write_vector(destination: Path, paths: list[IconPath]) -> None:
+ body = "\n".join(
+ (f"\n \n" if item.comment else "\n")
+ + android_path_xml(item)
+ for item in paths
+ )
+ destination.write_text(
+ '\n'
+ ''
+ f"{body}\n"
+ "\n",
+ encoding="utf-8",
+ )
+
+
+def svg_path_xml(icon_path: IconPath) -> str:
+ attrs = [f'd="{escape(icon_path.data)}"', f'fill="{icon_path.style.fill}"']
+ if icon_path.style.fill_alpha is not None:
+ attrs.append(f'fill-opacity="{fmt(icon_path.style.fill_alpha)}"')
+ if icon_path.style.stroke:
+ attrs.append(f'stroke="{icon_path.style.stroke}"')
+ if icon_path.style.stroke_width is not None:
+ attrs.append(f'stroke-width="{fmt(icon_path.style.stroke_width)}"')
+ if icon_path.style.line_cap:
+ attrs.append(f'stroke-linecap="{icon_path.style.line_cap}"')
+ if icon_path.style.line_join:
+ attrs.append(f'stroke-linejoin="{icon_path.style.line_join}"')
+ return f''
+
+
+def icon_svg_body() -> str:
+ return "\n ".join(svg_path_xml(item) for item in background_paths() + foreground_paths())
+
+
+def write_preview() -> None:
+ PREVIEW.parent.mkdir(parents=True, exist_ok=True)
+ body = icon_svg_body()
+ canvas_width = 360
+ canvas_height = 136
+ preview_width = canvas_width * PREVIEW_SCALE
+ preview_height = canvas_height * PREVIEW_SCALE
+ PREVIEW.write_text(
+ f'\n",
+ encoding="utf-8",
+ )
+
+
+def main() -> None:
+ write_vector(BACKGROUND, background_paths())
+ write_vector(FOREGROUND, foreground_paths())
+ write_preview()
+ print(f"Wrote {BACKGROUND.relative_to(ROOT)}")
+ print(f"Wrote {FOREGROUND.relative_to(ROOT)}")
+ print(f"Wrote {PREVIEW.relative_to(ROOT)}")
+
+
+if __name__ == "__main__":
+ main()