diff --git a/.codacy.yml b/.codacy.yml
new file mode 100644
index 00000000..e6569a25
--- /dev/null
+++ b/.codacy.yml
@@ -0,0 +1,7 @@
+---
+exclude_paths:
+ # Developer scripts that regenerate documentation assets (e.g. the README
+ # logo), not part of the installed package. They shell out to fixed
+ # commands like rsvg-convert, which Bandit flags (B404/B603/B607) even
+ # though no untrusted input is involved.
+ - "docs/figs/**"
diff --git a/README.md b/README.md
index f983cbed..c676c5ee 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,8 @@
# PGraph: graphs for Python
+
+ Mathematical graphs for Python
diff --git a/docs/figs/pgraph_logo.png b/docs/figs/pgraph_logo.png
new file mode 100644
index 00000000..4ffc3d85
Binary files /dev/null and b/docs/figs/pgraph_logo.png differ
diff --git a/docs/figs/pgraph_logo.py b/docs/figs/pgraph_logo.py
new file mode 100644
index 00000000..781900dc
--- /dev/null
+++ b/docs/figs/pgraph_logo.py
@@ -0,0 +1,161 @@
+"""
+Generate the pgraph logo.
+
+A snake follows a path through a small graph, its head acting as the arrowhead
+pointing at the goal vertex, beside the "pgraph" wordmark. The style matches
+the snake logos used across the RVC toolboxes (RTB, MVTB, bdsim, SMTB): a
+grey-to-teal body, yellow eyes and a magenta forked tongue.
+
+Renders ``pgraph_logo.png`` (2x scale, white background) next to this
+script, using ``rsvg-convert`` (from librsvg) to rasterize the SVG.
+
+The wordmark is SVG text in Roboto Medium, so Roboto must be installed for
+the PNG to render correctly. The PNG is the committed artifact used by the
+README.
+
+Usage::
+
+ python docs/figs/pgraph_logo.py
+"""
+
+import math
+import shutil
+import subprocess
+from pathlib import Path
+
+# palette
+TEAL = "#2bb39a" # snake head, end of body gradient
+GREY_TAIL = "#5c5c5c" # start of body gradient
+GREY_TEXT = "#4d4d4d"
+GREY_EDGE = "#9a9a9a"
+GREY_NODE = "#555555"
+EYE = "#ffee00"
+TONGUE = "#d14fc4"
+
+WIDTH, HEIGHT = 580, 210
+NODE_RADIUS = 11
+BODY_WIDTH = 14
+HEAD_SCALE = 1.3
+
+# graph: vertex coordinates, edges, and the path the snake follows
+VERTICES: dict[int, tuple[float, float]] = {
+ 1: (35, 165),
+ 2: (85, 55),
+ 3: (125, 150),
+ 4: (180, 90),
+ 5: (225, 175),
+ 6: (255, 40),
+}
+EDGES: list[tuple[int, int]] = [
+ (1, 2), (1, 3), (2, 3), (2, 4), (3, 4),
+ (3, 5), (4, 5), (4, 6), (2, 6), (5, 6),
+] # fmt: skip
+PATH: list[int] = [1, 3, 4, 6]
+
+
+def snake_head(x: float, y: float, angle: float, scale: float = 1.0) -> str:
+ """
+ Snake head as an SVG group.
+
+ :param x: x-coordinate of the neck
+ :param y: y-coordinate of the neck
+ :param angle: heading of the head, in radians
+ :param scale: scale factor applied to the head
+ :returns: SVG ```` element
+
+ The head is defined with its neck at the origin and nose pointing along +x,
+ then translated and rotated into place.
+ """
+ return f"""
+
+
+
+
+"""
+
+
+def logo_svg(font_weight: int = 500) -> str:
+ """
+ Build the logo as an SVG document.
+
+ :param font_weight: CSS font weight of the "pgraph" wordmark
+ :returns: SVG document
+ """
+ pts = [VERTICES[v] for v in PATH]
+ (x0, y0), (xN, yN) = pts[0], pts[-1]
+
+ # body gradient runs from the tail to near the neck, so the neck
+ # blends into the solid-colored head
+ gx = x0 + 0.8 * (xN - x0)
+ gy = y0 + 0.8 * (yN - y0)
+ out = [
+ f'")
+ return "\n".join(out)
+
+
+def main() -> None:
+ if shutil.which("rsvg-convert") is None:
+ raise SystemExit("rsvg-convert not found (brew install librsvg)")
+ png = Path(__file__).parent / "pgraph_logo.png"
+ subprocess.run(
+ ["rsvg-convert", "-z", "2", "-b", "white", "-o", str(png)],
+ input=logo_svg().encode(),
+ check=True,
+ )
+ print(f"wrote {png}")
+
+
+if __name__ == "__main__":
+ main()