diff --git a/architecture/brand-marks.md b/architecture/brand-marks.md
index 42908d7..bfac012 100644
--- a/architecture/brand-marks.md
+++ b/architecture/brand-marks.md
@@ -20,10 +20,28 @@ dimensions in `render.py`.
One large-format logo per repo: the constant green+gold snake-frame
(`geometry.py::project_frame`, margin 9 / arm 53 / stroke 11) with a single
gold inner symbol (`symbols.py`) chosen per repo in `projects.py::MANIFEST`.
-Two-colour (green + gold); repos differ by symbol shape, not colour. The two
-project templates reuse the org chevron. `modern-di-faststream` is the only
-mark using a partner's literal logo path (FastStream's, recoloured); other
-integration cues are redrawn evocations. Outputs: `mark.svg`,
+Two-colour (green + gold); repos differ by symbol shape, not colour. Every mark
+renders on a transparent background, so `CREAM` is only ever a knockout painted
+ON TOP OF a `GOLD` shape, never standalone ink directly on the background:
+standalone cream is invisible on light surfaces (cream-on-cream) but shows as
+stray bright-white ink once the same `mark.svg` sits on a dark one, e.g.
+`lockup-dark.svg`'s README banner half. Enforced for **every** manifest repo, with
+no exemptions, by `tests/test_projects.py::test_no_cream_on_transparent`, which
+rasters each mark with its cream knockouts stripped and fails on any pixel that
+turns transparent instead of staying gold-backed. `_cyl`'s cream rim is inset by
+half its stroke width for exactly this reason — centred on the top cap's ellipse,
+half the stroke would have fallen outside the cylinder's silhouette. The two
+project templates reuse the org chevron. Two marks are built from a partner's
+literal logo path, recoloured: `modern-di-faststream` (FastStream's) and
+`modern-di-flask` (Flask's horn — the Flask Artwork License permits the logo
+"or a modified version" to be used to refer to the project). Other
+integration cues are redrawn evocations, and three had nothing to redraw
+from: `arq` publishes no logo at all, while `aiogram`'s logo is a lettered
+disc and `taskiq`'s a wordmark, so those marks are inventions (a Telegram
+plane, a hopper, and taskiq's Q-creature). `modern-di-grpc` deliberately
+departs from gRPC's mark: theirs needs the arrow and the diamonds to be
+different inks, which the gold/cream palette cannot supply, so only the
+arrow survives. Outputs: `mark.svg`,
`lockup-light.svg`, `lockup-dark.svg`, `lockup.png` (+ `mark-512/1024.png`).
Regenerate via `uv run python -m brand.build.render`.
Repos with a live docs site (`projects.py::DOCS_REPOS`, a subset of `MANIFEST`)
diff --git a/brand/build/projects.py b/brand/build/projects.py
index c9bdb0c..752b969 100644
--- a/brand/build/projects.py
+++ b/brand/build/projects.py
@@ -19,11 +19,17 @@
"modern-di": lambda: sym.graph(_CX, _CY, R, dashed=True),
"that-depends": lambda: sym.graph(_CX, _CY, R, dashed=False),
"modern-di-aiohttp": lambda: sym.async_loop(_CX, _CY, R),
+ "modern-di-aiogram": lambda: sym.plane(_CX, _CY, R),
+ "modern-di-arq": lambda: sym.hopper(_CX, _CY, R),
+ "modern-di-celery": lambda: sym.celery_stalk(_CX, _CY, R),
"modern-di-fastapi": lambda: sym.bolt_disc(_CX, _CY, R),
"modern-di-faststream": lambda: sym.faststream(_CX, _CY, R),
+ "modern-di-flask": lambda: sym.flask_horn(_CX, _CY, R),
+ "modern-di-grpc": lambda: sym.rpc_arrow(_CX, _CY, R),
"modern-di-litestar": lambda: sym.star_disc(_CX, _CY, R),
"modern-di-pytest": lambda: sym.bars(_CX, _CY, R),
"modern-di-starlette": lambda: sym.sparkle_cluster(_CX, _CY, R),
+ "modern-di-taskiq": lambda: sym.task_q(_CX, _CY, R),
"modern-di-typer": lambda: sym.terminal(_CX, _CY, R),
# templates — reuse the org chevron
"fastapi-sqlalchemy-template": lambda: sym.chevron(_CX, _CY, R - 1),
@@ -38,6 +44,7 @@
"db-retry": lambda: sym.db_retry(_CX, _CY, R),
"eof-fixer": lambda: sym.eof_fixer(_CX, _CY, R),
"semvertag": lambda: sym.tag(_CX, _CY, R),
+ "compose2pod": lambda: sym.pod(_CX, _CY, R),
}
diff --git a/brand/build/symbols.py b/brand/build/symbols.py
index 0a9b95f..c63ae40 100644
--- a/brand/build/symbols.py
+++ b/brand/build/symbols.py
@@ -23,12 +23,20 @@ def _cyl(
) -> str:
"""Database cylinder centred on (cx,cy)."""
rx = 0.5 * r * w
+ ry = 0.16 * r
+ top = cy - h / 2 * r
+ # The top cap's upper arc IS the cylinder's silhouette, so a rim stroke
+ # centred on that ellipse would put half its width outside the gold —
+ # invisible on cream, but white ink on dark. Shrink the rim's radii by half
+ # the stroke width (plus a hair of margin) so the whole stroke lands inside.
+ rim_w = 0.8
+ inset = rim_w / 2 + 0.1
return (
- f''
- f''
- f''
- f''
+ f''
+ f''
+ f''
+ f''
)
@@ -76,6 +84,30 @@ def _sparkle4(cx: float, cy: float, radius: float, color: str, inner: float = 0.
return f''
+def _box(cx: float, cy: float, s: float, fill: str = GOLD) -> str:
+ """Rounded square (a container / job token) centred on (cx,cy)."""
+ return (
+ f''
+ )
+
+
+def _ngon(cx: float, cy: float, rad: float, n: int, start: float, w: float) -> str:
+ """Regular n-gon outline; `start` is the angle (deg) of the first vertex."""
+ pts = [
+ (
+ cx + rad * math.cos(math.radians(start + i * 360 / n)),
+ cy + rad * math.sin(math.radians(start + i * 360 / n)),
+ )
+ for i in range(n)
+ ]
+ body = " ".join(f"{x:.1f},{y:.1f}" for x, y in pts)
+ return (
+ f''
+ )
+
+
def sparkle_cluster(cx: float, cy: float, r: float) -> str:
"""Starlette cue: a large four-point sparkle with a small companion
(a "little star" — starlette)."""
@@ -134,6 +166,26 @@ def async_loop(cx: float, cy: float, r: float) -> str:
"h370.45c51.9,0,84.33-56.18,58.38-101.12Z"
)
+# Flask's own horn, from Pallets' docs/_static/flask-icon.svg (500x500 space).
+# Their Flask Artwork License permits the logo "or a modified version" to be used
+# by anyone to refer to the Flask project, so this recolour is sanctioned.
+FLASK_PATHS = (
+ "M224.446,59.975c-0.056,-4.151 -0.483,-5.543 -2.7,-6.823c-2.104,-1.393 -5.288,-1.421 "
+ "-8.329,-0.085l-204.674,87.64c-3.042,1.336 -5.913,4.008 -7.448,6.908c-1.535,2.899 "
+ "-1.705,5.97 -0.511,8.158l17.084,31.384l0.228,0.369c1.847,2.928 6.026,3.696 10.29,1.82"
+ "l1.251,-0.54c5.344,22.4 14.1,50.429 25.783,70.413l178.294,-79.794c-2.559,-23.14 "
+ "-9.552,-89.602 -9.268,-119.479l0,0.029Z",
+ "M238.603,205.776l-171.698,76.838c10.091,19.132 22.542,39.428 37.722,58.986c50.429,"
+ "-25.698 100.887,-51.396 151.316,-77.094c-3.269,-8.471 -6.452,-17.653 -17.34,-58.73Z",
+ "M497.601,388.846l-12.139,-18.535c-1.819,-2.018 -4.633,-2.786 -7.106,-1.791l-15.578,"
+ "5.999c-1.848,-2.047 -4.52,-2.815 -7.135,-1.791c-5.089,1.99 -10.206,4.008 -15.294,5.998"
+ "c-1.649,0.625 -2.104,1.847 -1.791,3.439l0.995,4.861c-28.711,3.099 -77.236,1.564 "
+ "-120.701,-32.577c-19.216,-15.066 -37.239,-36.386 -52.277,-66.206l-144.75,73.768"
+ "c26.466,29.08 59.697,54.864 100.973,70.385c57.422,21.633 130.593,23.679 222.838,-13.475"
+ "l0.512,2.616c0.455,2.928 3.98,6.026 8.755,4.15l15.323,-5.97c5.258,-1.99 5.287,-6.026 "
+ "4.519,-8.641l19.729,-7.704c2.217,-0.853 9.096,-6.169 3.183,-14.526l-0.056,-0Z",
+)
+
def bolt_disc(cx: float, cy: float, r: float) -> str:
"""FastAPI cue: lightning bolt knocked out of a gold disc."""
@@ -364,3 +416,146 @@ def tag(cx: float, cy: float, r: float) -> str:
f'L{cx - 0.75 * r:.1f} {cy:.1f} Z" fill="{GOLD}"/>'
f''
)
+
+
+def plane(cx: float, cy: float, r: float) -> str:
+ """aiogram cue: Telegram's paper plane. aiogram's own logo is a lettered
+ disc with no pictorial element, so the honest cue is Telegram itself."""
+ hull = ((-0.95, 0.02), (0.95, -0.78), (0.30, 0.86), (0.02, 0.28))
+ body = " ".join(f"{cx + dx * r:.1f},{cy + dy * r:.1f}" for dx, dy in hull)
+ # Crease runs fold-point -> nose (hull[3] -> hull[1]), a chord that meets
+ # the hull exactly at those two vertices. Use a butt cap, not round, so
+ # the stroke's own width can't spill past the polygon.
+ #
+ # The two ends need different insets. hull[1] (nose) is a sharp,
+ # non-reflex vertex — a butt cap placed at the vertex itself pokes the
+ # 0.11*r-wide stroke past both adjoining edges, so it needs a real inset
+ # (~0.154*r is the minimum for this vertex's angle; 0.20*r keeps margin).
+ # hull[3] (fold) is the polygon's REFLEX vertex (interior angle ~229deg),
+ # and the crease direction sits ~23deg clear of the nearest edge, so a
+ # butt cap placed essentially at the fold vertex already stays inside the
+ # gold (~0.021*r of margin) — only a token inset is needed there. Insetting
+ # it as much as the nose end made the crease read as a floating stripe
+ # instead of a fold running notch-to-nose.
+ fold, nose = hull[3], hull[1]
+ ex, ey = nose[0] - fold[0], nose[1] - fold[1]
+ elen = math.hypot(ex, ey)
+ ux, uy = ex / elen, ey / elen
+ inset_fold = 0.05 * r
+ inset_nose = 0.20 * r
+ x0, y0 = cx + fold[0] * r + ux * inset_fold, cy + fold[1] * r + uy * inset_fold
+ x1, y1 = cx + nose[0] * r - ux * inset_nose, cy + nose[1] * r - uy * inset_nose
+ crease = (
+ f''
+ )
+ return f'{crease}'
+
+
+def hopper(cx: float, cy: float, r: float) -> str:
+ """arq cue: jobs dropping into a hopper, one emerging — a queue being
+ drained. arq ships no logo, so this is drawn from what arq is."""
+ jobs = "".join(
+ _box(cx + dx * r, cy - 0.72 * r, r * 0.34) for dx in (-0.52, 0.0, 0.52)
+ )
+ hull = ((-0.86, -0.30), (0.86, -0.30), (0.20, 0.34), (-0.20, 0.34))
+ funnel = " ".join(f"{cx + dx * r:.1f},{cy + dy * r:.1f}" for dx, dy in hull)
+ return (
+ f'{jobs}'
+ + _box(cx, cy + 0.76 * r, r * 0.34)
+ )
+
+
+def pod(cx: float, cy: float, r: float) -> str:
+ """compose2pod cue: Podman's heptagon holding three composed containers —
+ both ends of the Compose-to-Podman conversion in one shape."""
+ return _ngon(cx, cy, r * 0.98, 7, 90, r * 0.15) + (
+ _box(cx - 0.34 * r, cy - 0.20 * r, r * 0.36)
+ + _box(cx + 0.30 * r, cy - 0.20 * r, r * 0.36)
+ + _box(cx - 0.02 * r, cy + 0.30 * r, r * 0.36)
+ )
+
+
+def celery_stalk(cx: float, cy: float, r: float) -> str:
+ """Celery cue: their icon redrawn — a banded capsule with an open C,
+ stroked the same gold as the body, at its right end. Measured off
+ celery_512.png; Celery ships no SVG (see celery/celery#5981), and the C
+ is *open* (facing right), not a closed ring."""
+ s = (2.35 * r) / 512.0
+ body = (
+ ''
+ )
+ band_w = 9.0
+ # The capsule's left cap is a semicircle (centre 103,256 r=102), so its
+ # boundary curves inward away from y=256. A plain x1="8" pokes past that
+ # curve for the off-centre bands. Inset each band's start to the cap's
+ # boundary at the band's *farthest* edge (accounting for the stroke's own
+ # width), plus a small safety margin, so the whole stroke — not just its
+ # centreline — stays inside the gold. max() keeps the already-safe centre
+ # band (whose boundary is well left of 8) visually unchanged.
+ bands = "".join(
+ f''
+ for y in (205, 256, 307)
+ )
+ arc = (
+ ''
+ )
+ return (
+ f'{body}{bands}{arc}'
+ )
+
+
+def task_q(cx: float, cy: float, r: float) -> str:
+ """taskiq cue: the Q-creature from their wordmark — a thick ring with a
+ descender crossing it. taskiq publishes no icon-only mark."""
+ ang = math.radians(48)
+ dx, dy = math.cos(ang), math.sin(ang)
+ ring_y = cy - 0.06 * r
+ ring = (
+ f''
+ )
+ tail = (
+ f''
+ )
+ return ring + tail
+
+
+def flask_horn(cx: float, cy: float, r: float) -> str:
+ """Flask cue: Flask's own drinking horn (a rhyton — not a lab flask),
+ recoloured gold. The bands are negative space in the source artwork."""
+ sc = (2.04 * r) / 500.0
+ body = "".join(f'' for d in FLASK_PATHS)
+ return (
+ f'{body}'
+ )
+
+
+def rpc_arrow(cx: float, cy: float, r: float) -> str:
+ """gRPC cue: a bidirectional call. gRPC's real mark (two diamonds pierced by
+ an arrow) needs two inks that contrast with each other *and* with the page;
+ our two-colour palette cannot supply that, so the mark keeps the arrow alone,
+ with the heads symmetrized (gRPC's own are unequal, sized to their diamonds)."""
+ w = r * 0.17
+ half, reach, rise = 0.98 * r, 0.34 * r, 0.46 * r
+ out = (
+ f''
+ )
+ for sgn in (-1, 1):
+ tx = cx + sgn * half
+ out += (
+ f''
+ )
+ return out
diff --git a/brand/projects/compose2pod/lockup-dark.svg b/brand/projects/compose2pod/lockup-dark.svg
new file mode 100644
index 0000000..f5b1bae
--- /dev/null
+++ b/brand/projects/compose2pod/lockup-dark.svg
@@ -0,0 +1 @@
+
diff --git a/brand/projects/compose2pod/lockup-light.svg b/brand/projects/compose2pod/lockup-light.svg
new file mode 100644
index 0000000..0850ba6
--- /dev/null
+++ b/brand/projects/compose2pod/lockup-light.svg
@@ -0,0 +1 @@
+
diff --git a/brand/projects/compose2pod/lockup.png b/brand/projects/compose2pod/lockup.png
new file mode 100644
index 0000000..43b5e2c
Binary files /dev/null and b/brand/projects/compose2pod/lockup.png differ
diff --git a/brand/projects/compose2pod/mark-1024.png b/brand/projects/compose2pod/mark-1024.png
new file mode 100644
index 0000000..90d53fe
Binary files /dev/null and b/brand/projects/compose2pod/mark-1024.png differ
diff --git a/brand/projects/compose2pod/mark-512.png b/brand/projects/compose2pod/mark-512.png
new file mode 100644
index 0000000..2e83db4
Binary files /dev/null and b/brand/projects/compose2pod/mark-512.png differ
diff --git a/brand/projects/compose2pod/mark.svg b/brand/projects/compose2pod/mark.svg
new file mode 100644
index 0000000..ebe1890
--- /dev/null
+++ b/brand/projects/compose2pod/mark.svg
@@ -0,0 +1 @@
+
diff --git a/brand/projects/db-retry/lockup-dark.svg b/brand/projects/db-retry/lockup-dark.svg
index be22819..9ae1b9f 100644
--- a/brand/projects/db-retry/lockup-dark.svg
+++ b/brand/projects/db-retry/lockup-dark.svg
@@ -1 +1 @@
-
+
diff --git a/brand/projects/db-retry/lockup-light.svg b/brand/projects/db-retry/lockup-light.svg
index ad84956..7ad437d 100644
--- a/brand/projects/db-retry/lockup-light.svg
+++ b/brand/projects/db-retry/lockup-light.svg
@@ -1 +1 @@
-
+
diff --git a/brand/projects/db-retry/lockup.png b/brand/projects/db-retry/lockup.png
index 9b04bac..eda26a3 100644
Binary files a/brand/projects/db-retry/lockup.png and b/brand/projects/db-retry/lockup.png differ
diff --git a/brand/projects/db-retry/mark-1024.png b/brand/projects/db-retry/mark-1024.png
index 2805038..d5d4e8b 100644
Binary files a/brand/projects/db-retry/mark-1024.png and b/brand/projects/db-retry/mark-1024.png differ
diff --git a/brand/projects/db-retry/mark-512.png b/brand/projects/db-retry/mark-512.png
index 57bb47c..c7dc8eb 100644
Binary files a/brand/projects/db-retry/mark-512.png and b/brand/projects/db-retry/mark-512.png differ
diff --git a/brand/projects/db-retry/mark.svg b/brand/projects/db-retry/mark.svg
index d485876..26d9d28 100644
--- a/brand/projects/db-retry/mark.svg
+++ b/brand/projects/db-retry/mark.svg
@@ -1 +1 @@
-
+
diff --git a/brand/projects/faststream-outbox/lockup-dark.svg b/brand/projects/faststream-outbox/lockup-dark.svg
index bf0970b..7f54e4d 100644
--- a/brand/projects/faststream-outbox/lockup-dark.svg
+++ b/brand/projects/faststream-outbox/lockup-dark.svg
@@ -1 +1 @@
-
+
diff --git a/brand/projects/faststream-outbox/lockup-light.svg b/brand/projects/faststream-outbox/lockup-light.svg
index a5f9ac9..11a94ce 100644
--- a/brand/projects/faststream-outbox/lockup-light.svg
+++ b/brand/projects/faststream-outbox/lockup-light.svg
@@ -1 +1 @@
-
+
diff --git a/brand/projects/faststream-outbox/lockup.png b/brand/projects/faststream-outbox/lockup.png
index f3543d0..caf9e7e 100644
Binary files a/brand/projects/faststream-outbox/lockup.png and b/brand/projects/faststream-outbox/lockup.png differ
diff --git a/brand/projects/faststream-outbox/mark-1024.png b/brand/projects/faststream-outbox/mark-1024.png
index 046f5dc..0a6c3a4 100644
Binary files a/brand/projects/faststream-outbox/mark-1024.png and b/brand/projects/faststream-outbox/mark-1024.png differ
diff --git a/brand/projects/faststream-outbox/mark-512.png b/brand/projects/faststream-outbox/mark-512.png
index 07e6875..68c6c23 100644
Binary files a/brand/projects/faststream-outbox/mark-512.png and b/brand/projects/faststream-outbox/mark-512.png differ
diff --git a/brand/projects/faststream-outbox/mark.svg b/brand/projects/faststream-outbox/mark.svg
index 10d3337..1481550 100644
--- a/brand/projects/faststream-outbox/mark.svg
+++ b/brand/projects/faststream-outbox/mark.svg
@@ -1 +1 @@
-
+
diff --git a/brand/projects/faststream-outbox/social-card.png b/brand/projects/faststream-outbox/social-card.png
index ed87a5d..14145e4 100644
Binary files a/brand/projects/faststream-outbox/social-card.png and b/brand/projects/faststream-outbox/social-card.png differ
diff --git a/brand/projects/faststream-outbox/social-card.svg b/brand/projects/faststream-outbox/social-card.svg
index 41d3a51..0769625 100644
--- a/brand/projects/faststream-outbox/social-card.svg
+++ b/brand/projects/faststream-outbox/social-card.svg
@@ -1 +1 @@
-
+
diff --git a/brand/projects/modern-di-aiogram/lockup-dark.svg b/brand/projects/modern-di-aiogram/lockup-dark.svg
new file mode 100644
index 0000000..2286676
--- /dev/null
+++ b/brand/projects/modern-di-aiogram/lockup-dark.svg
@@ -0,0 +1 @@
+
diff --git a/brand/projects/modern-di-aiogram/lockup-light.svg b/brand/projects/modern-di-aiogram/lockup-light.svg
new file mode 100644
index 0000000..f3a55e9
--- /dev/null
+++ b/brand/projects/modern-di-aiogram/lockup-light.svg
@@ -0,0 +1 @@
+
diff --git a/brand/projects/modern-di-aiogram/lockup.png b/brand/projects/modern-di-aiogram/lockup.png
new file mode 100644
index 0000000..bb07cbb
Binary files /dev/null and b/brand/projects/modern-di-aiogram/lockup.png differ
diff --git a/brand/projects/modern-di-aiogram/mark-1024.png b/brand/projects/modern-di-aiogram/mark-1024.png
new file mode 100644
index 0000000..9f7e5df
Binary files /dev/null and b/brand/projects/modern-di-aiogram/mark-1024.png differ
diff --git a/brand/projects/modern-di-aiogram/mark-512.png b/brand/projects/modern-di-aiogram/mark-512.png
new file mode 100644
index 0000000..60bba14
Binary files /dev/null and b/brand/projects/modern-di-aiogram/mark-512.png differ
diff --git a/brand/projects/modern-di-aiogram/mark.svg b/brand/projects/modern-di-aiogram/mark.svg
new file mode 100644
index 0000000..b929cac
--- /dev/null
+++ b/brand/projects/modern-di-aiogram/mark.svg
@@ -0,0 +1 @@
+
diff --git a/brand/projects/modern-di-arq/lockup-dark.svg b/brand/projects/modern-di-arq/lockup-dark.svg
new file mode 100644
index 0000000..ed076c9
--- /dev/null
+++ b/brand/projects/modern-di-arq/lockup-dark.svg
@@ -0,0 +1 @@
+
diff --git a/brand/projects/modern-di-arq/lockup-light.svg b/brand/projects/modern-di-arq/lockup-light.svg
new file mode 100644
index 0000000..7589a9a
--- /dev/null
+++ b/brand/projects/modern-di-arq/lockup-light.svg
@@ -0,0 +1 @@
+
diff --git a/brand/projects/modern-di-arq/lockup.png b/brand/projects/modern-di-arq/lockup.png
new file mode 100644
index 0000000..695098f
Binary files /dev/null and b/brand/projects/modern-di-arq/lockup.png differ
diff --git a/brand/projects/modern-di-arq/mark-1024.png b/brand/projects/modern-di-arq/mark-1024.png
new file mode 100644
index 0000000..b18edc9
Binary files /dev/null and b/brand/projects/modern-di-arq/mark-1024.png differ
diff --git a/brand/projects/modern-di-arq/mark-512.png b/brand/projects/modern-di-arq/mark-512.png
new file mode 100644
index 0000000..bdc64f8
Binary files /dev/null and b/brand/projects/modern-di-arq/mark-512.png differ
diff --git a/brand/projects/modern-di-arq/mark.svg b/brand/projects/modern-di-arq/mark.svg
new file mode 100644
index 0000000..1ab4d13
--- /dev/null
+++ b/brand/projects/modern-di-arq/mark.svg
@@ -0,0 +1 @@
+
diff --git a/brand/projects/modern-di-celery/lockup-dark.svg b/brand/projects/modern-di-celery/lockup-dark.svg
new file mode 100644
index 0000000..275dcea
--- /dev/null
+++ b/brand/projects/modern-di-celery/lockup-dark.svg
@@ -0,0 +1 @@
+
diff --git a/brand/projects/modern-di-celery/lockup-light.svg b/brand/projects/modern-di-celery/lockup-light.svg
new file mode 100644
index 0000000..558743a
--- /dev/null
+++ b/brand/projects/modern-di-celery/lockup-light.svg
@@ -0,0 +1 @@
+
diff --git a/brand/projects/modern-di-celery/lockup.png b/brand/projects/modern-di-celery/lockup.png
new file mode 100644
index 0000000..2dc93cb
Binary files /dev/null and b/brand/projects/modern-di-celery/lockup.png differ
diff --git a/brand/projects/modern-di-celery/mark-1024.png b/brand/projects/modern-di-celery/mark-1024.png
new file mode 100644
index 0000000..438161e
Binary files /dev/null and b/brand/projects/modern-di-celery/mark-1024.png differ
diff --git a/brand/projects/modern-di-celery/mark-512.png b/brand/projects/modern-di-celery/mark-512.png
new file mode 100644
index 0000000..0859af7
Binary files /dev/null and b/brand/projects/modern-di-celery/mark-512.png differ
diff --git a/brand/projects/modern-di-celery/mark.svg b/brand/projects/modern-di-celery/mark.svg
new file mode 100644
index 0000000..0582ebf
--- /dev/null
+++ b/brand/projects/modern-di-celery/mark.svg
@@ -0,0 +1 @@
+
diff --git a/brand/projects/modern-di-flask/lockup-dark.svg b/brand/projects/modern-di-flask/lockup-dark.svg
new file mode 100644
index 0000000..946c5cb
--- /dev/null
+++ b/brand/projects/modern-di-flask/lockup-dark.svg
@@ -0,0 +1 @@
+
diff --git a/brand/projects/modern-di-flask/lockup-light.svg b/brand/projects/modern-di-flask/lockup-light.svg
new file mode 100644
index 0000000..3805246
--- /dev/null
+++ b/brand/projects/modern-di-flask/lockup-light.svg
@@ -0,0 +1 @@
+
diff --git a/brand/projects/modern-di-flask/lockup.png b/brand/projects/modern-di-flask/lockup.png
new file mode 100644
index 0000000..3582433
Binary files /dev/null and b/brand/projects/modern-di-flask/lockup.png differ
diff --git a/brand/projects/modern-di-flask/mark-1024.png b/brand/projects/modern-di-flask/mark-1024.png
new file mode 100644
index 0000000..f062d59
Binary files /dev/null and b/brand/projects/modern-di-flask/mark-1024.png differ
diff --git a/brand/projects/modern-di-flask/mark-512.png b/brand/projects/modern-di-flask/mark-512.png
new file mode 100644
index 0000000..fd191dc
Binary files /dev/null and b/brand/projects/modern-di-flask/mark-512.png differ
diff --git a/brand/projects/modern-di-flask/mark.svg b/brand/projects/modern-di-flask/mark.svg
new file mode 100644
index 0000000..48dbd54
--- /dev/null
+++ b/brand/projects/modern-di-flask/mark.svg
@@ -0,0 +1 @@
+
diff --git a/brand/projects/modern-di-grpc/lockup-dark.svg b/brand/projects/modern-di-grpc/lockup-dark.svg
new file mode 100644
index 0000000..f853f1b
--- /dev/null
+++ b/brand/projects/modern-di-grpc/lockup-dark.svg
@@ -0,0 +1 @@
+
diff --git a/brand/projects/modern-di-grpc/lockup-light.svg b/brand/projects/modern-di-grpc/lockup-light.svg
new file mode 100644
index 0000000..65b5c4c
--- /dev/null
+++ b/brand/projects/modern-di-grpc/lockup-light.svg
@@ -0,0 +1 @@
+
diff --git a/brand/projects/modern-di-grpc/lockup.png b/brand/projects/modern-di-grpc/lockup.png
new file mode 100644
index 0000000..8d6f3b1
Binary files /dev/null and b/brand/projects/modern-di-grpc/lockup.png differ
diff --git a/brand/projects/modern-di-grpc/mark-1024.png b/brand/projects/modern-di-grpc/mark-1024.png
new file mode 100644
index 0000000..3397b71
Binary files /dev/null and b/brand/projects/modern-di-grpc/mark-1024.png differ
diff --git a/brand/projects/modern-di-grpc/mark-512.png b/brand/projects/modern-di-grpc/mark-512.png
new file mode 100644
index 0000000..50e2b5b
Binary files /dev/null and b/brand/projects/modern-di-grpc/mark-512.png differ
diff --git a/brand/projects/modern-di-grpc/mark.svg b/brand/projects/modern-di-grpc/mark.svg
new file mode 100644
index 0000000..a710c11
--- /dev/null
+++ b/brand/projects/modern-di-grpc/mark.svg
@@ -0,0 +1 @@
+
diff --git a/brand/projects/modern-di-taskiq/lockup-dark.svg b/brand/projects/modern-di-taskiq/lockup-dark.svg
new file mode 100644
index 0000000..d2e29f9
--- /dev/null
+++ b/brand/projects/modern-di-taskiq/lockup-dark.svg
@@ -0,0 +1 @@
+
diff --git a/brand/projects/modern-di-taskiq/lockup-light.svg b/brand/projects/modern-di-taskiq/lockup-light.svg
new file mode 100644
index 0000000..c768627
--- /dev/null
+++ b/brand/projects/modern-di-taskiq/lockup-light.svg
@@ -0,0 +1 @@
+
diff --git a/brand/projects/modern-di-taskiq/lockup.png b/brand/projects/modern-di-taskiq/lockup.png
new file mode 100644
index 0000000..72f1c50
Binary files /dev/null and b/brand/projects/modern-di-taskiq/lockup.png differ
diff --git a/brand/projects/modern-di-taskiq/mark-1024.png b/brand/projects/modern-di-taskiq/mark-1024.png
new file mode 100644
index 0000000..09520ec
Binary files /dev/null and b/brand/projects/modern-di-taskiq/mark-1024.png differ
diff --git a/brand/projects/modern-di-taskiq/mark-512.png b/brand/projects/modern-di-taskiq/mark-512.png
new file mode 100644
index 0000000..93d453d
Binary files /dev/null and b/brand/projects/modern-di-taskiq/mark-512.png differ
diff --git a/brand/projects/modern-di-taskiq/mark.svg b/brand/projects/modern-di-taskiq/mark.svg
new file mode 100644
index 0000000..1247168
--- /dev/null
+++ b/brand/projects/modern-di-taskiq/mark.svg
@@ -0,0 +1 @@
+
diff --git a/planning/changes/2026-07-12.01-seven-new-project-marks.md b/planning/changes/2026-07-12.01-seven-new-project-marks.md
new file mode 100644
index 0000000..1a2200d
--- /dev/null
+++ b/planning/changes/2026-07-12.01-seven-new-project-marks.md
@@ -0,0 +1,150 @@
+---
+summary: Mint brand marks for the seven repos that have none — six modern-di integrations (aiogram, arq, celery, flask, grpc, taskiq) and compose2pod — adding their symbols, MANIFEST entries and tests, and regenerating the brand kit.
+---
+
+# Design: Brand marks for the seven un-branded repos
+
+## Summary
+
+Seven repos in the org have no entry in the brand kit: `modern-di-aiogram`,
+`modern-di-arq`, `modern-di-celery`, `modern-di-flask`, `modern-di-grpc`,
+`modern-di-taskiq`, and `compose2pod`. This change mints a symbol for each,
+registers them in `projects.py::MANIFEST`, extends the test set, and regenerates
+`brand/projects//`. Scope is the brand kit only — nothing in
+`profile/README.md`, `docs/`, or GitHub repo settings.
+
+## Motivation
+
+Every other repo in the org has a mark, a lockup pair, and a PNG fallback under
+`brand/projects//`; these seven have nothing, so they cannot carry the
+README `` banner their siblings all use. The gap is the whole reason
+for the change — there is no broken link to fix yet (unlike the
+`modern-di-starlette` onboarding, whose README already pointed at absent files),
+but the seven repos cannot adopt the banner convention until the assets exist.
+
+Each mark was chosen against the partner's **actual** logo, verified by fetching
+the upstream asset rather than working from memory. That turned up three facts
+that shaped the design:
+
+- **arq ships no logo at all** — its README is badges only. Its mark is
+ necessarily an invention from what arq *is*, not a quotation.
+- **aiogram's logo is a lettered blue disc** with no pictorial element, and
+ **taskiq's is a wordmark** whose only glyph is a tailed Q. Neither offers a
+ symbol to borrow, so both marks are evocations.
+- **Flask's logo is a drinking horn (rhyton), not a lab flask** — and Flask's
+ Artwork License states the logo "or a modified version may be used by anyone
+ to refer to the Flask project". That permission makes a literal recolour the
+ correct choice, and it removes the honesty-versus-legibility tension that a
+ hand-drawn horn could not resolve.
+
+## Design
+
+### 1. Seven symbols — `brand/build/symbols.py`
+
+Each follows the existing contract: `(cx, cy, r) -> str`, bare markup, `GOLD`
+fills with `CREAM` knockouts, so `test_only_allowed_colours` passes unchanged.
+
+| repo | symbol | cue |
+| --- | --- | --- |
+| `modern-di-aiogram` | `plane` | Telegram's paper plane (aiogram itself has no pictorial mark) |
+| `modern-di-arq` | `hopper` | jobs falling into a hopper, one emerging — a queue being drained |
+| `modern-di-celery` | `celery_stalk` | Celery's icon redrawn: banded capsule + right-facing open C |
+| `modern-di-flask` | `flask_horn` | Flask's literal `flask-icon.svg` path, recoloured |
+| `modern-di-grpc` | `rpc_arrow` | a symmetric bidirectional arrow |
+| `modern-di-taskiq` | `task_q` | taskiq's Q-creature: thick ring, crossing descender |
+| `compose2pod` | `pod` | Podman's heptagon holding three composed containers |
+
+Two of these need explanation.
+
+**`flask_horn` embeds a partner's literal path.** The three paths from Pallets'
+`flask-icon.svg` become a module constant beside the existing `FASTSTREAM_PATH`,
+scaled and recoloured gold. The icon is a single flat fill whose bands are pure
+negative space, so a recolour needs no reconstruction. This makes Flask the
+*second* mark built from a partner's real artwork, which contradicts a sentence
+in `architecture/brand-marks.md` — see §4.
+
+**`rpc_arrow` is redrawn, not traced.** gRPC publishes no icon-only asset (every
+file they and the CNCF label "icon" contains the wordmark), and their true mark —
+two diamonds pierced by an arrow — cannot survive our two-colour palette: the
+arrow is a *different colour* from the diamonds upstream (dark slate on teal),
+but our inner symbols only get gold and cream, and cream fails in the gap
+between the diamonds where the background is already cream. Every faithful
+rendering turned to mush. The mark therefore keeps only the arrow, symmetrized
+(gRPC's own heads are deliberately unequal, sized to their unequal diamonds; that
+asymmetry reads as a defect once the diamonds are gone). The accepted cost: the
+mark is no longer a quotation of gRPC's logo, just a generic "calls both ways".
+
+### 2. Manifest — `brand/build/projects.py`
+
+Seven `MANIFEST` entries. The six `modern-di-*` integrations join the
+dependency-injection block; `compose2pod` joins the utilities block.
+
+### 3. Tests — `tests/test_projects.py`
+
+Add all seven to `EXPECTED_REPOS`. That set is asserted equal to `MANIFEST`, and
+the valid-SVG, allowed-colours, and render tests are parametrized off it, so no
+other test edits are required. `DOCS_EXPECTED` is unchanged.
+
+### 4. Architecture — `architecture/brand-marks.md`
+
+The per-project section states that `modern-di-faststream` "is the only mark
+using a partner's literal logo path". Flask's horn makes that false; the sentence
+is corrected to name both, and to record that Pallets' Artwork License permits
+the derivative.
+
+### 5. Regenerate — `just sync-assets`
+
+Produces `mark.svg`, `mark-512.png`, `mark-1024.png`, `lockup-light.svg`,
+`lockup-dark.svg`, and `lockup.png` for each of the seven. Generated files are
+committed.
+
+## Non-goals
+
+- **No social cards.** None of the seven ships an `mkdocs.yml`, so none has a
+ docs site and none belongs in `DOCS_REPOS`.
+- **No profile or docs-site listings**, and no GitHub description/topics/website
+ changes. Those surfaces are a separate change.
+- **No edits to the seven repos themselves** — the README `` banners
+ hotlink these assets and can be added per-repo afterwards.
+
+## Testing
+
+- `just test` green — in particular `test_manifest_covers_every_repo`
+ (`MANIFEST == EXPECTED_REPOS`), `test_only_allowed_colours`,
+ `test_project_mark_is_valid_svg`, and the render tests, all now covering 26
+ repos.
+- `just check-planning` passes before pushing.
+- Visually inspect the regenerated `mark.svg` and both lockups for each of the
+ seven, and check the seven against the 19 existing marks as a set.
+
+## Risk
+
+- **Low: a mark reads as something unintended.** `compose2pod`'s three containers
+ inside the heptagon can read as a face. Accepted knowingly; the shape encodes
+ both ends of the Compose-to-Podman conversion and no cleaner alternative did.
+- **Low: Celery's proportions.** Its real icon is a wide, short bar (~2.5:1), so
+ it fills the square frame less fully than its siblings. Acceptable — per-project
+ marks are large-format logos (README banners, lockups); only the *org* mark is
+ used at favicon size.
+- **Low: Flask's artwork terms drift.** The Artwork License granting derivative
+ use was dropped from the Pallets repo after Flask 1.0 and nothing replaced it,
+ so today's assets ship with no stated terms. The grant we rely on is the
+ historical one, and it was explicit. If Pallets later publishes restrictive
+ terms, the horn is one function to redraw.
+- **Low: stray colour fails the palette test.** All seven use only `GOLD` and
+ `CREAM`; `test_only_allowed_colours` is the guardrail.
+- **Medium (realized, fixed): CREAM must only knock out of GOLD, never sit
+ directly on the transparent background.** Marks render on dark surfaces too
+ (`lockup-dark.svg` is the dark half of every README `` banner), so
+ standalone cream ink — invisible on a cream/light page — becomes bright
+ white ink on dark. Three of the seven symbols shipped this bug (`task_q`'s
+ eye sat in the ring's hollow counter with no gold beneath it; `celery_stalk`'s
+ bands poked past the capsule's curved cap; `plane`'s round-capped crease
+ spilled past the hull at the nose) and were fixed in a follow-up pass, with
+ `test_no_cream_on_transparent` added as the regression guard. Auditing the
+ guard across the whole manifest then exposed two *pre-existing* marks with the
+ same defect — `db-retry` and `faststream-outbox`, whose shared `_cyl` helper
+ stroked its cream rim centred on the top cap's ellipse, putting half the stroke
+ outside the cylinder's silhouette. `_cyl`'s rim is now inset by half its stroke
+ width, so the guard covers every mark with no exemptions. The durable lesson:
+ composite every mark onto a dark background before it ships, not just a light one.
diff --git a/tests/test_projects.py b/tests/test_projects.py
index c93ee6c..d618258 100644
--- a/tests/test_projects.py
+++ b/tests/test_projects.py
@@ -1,10 +1,13 @@
import re
+import shutil
from pathlib import Path
from xml.dom import minidom
import pytest
+from PIL import Image
from brand.build import geometry as g
from brand.build import tokens as t
from brand.build import projects as p
+from brand.build.raster import export_png
def test_project_frame_parses_and_uses_tokens() -> None:
@@ -25,6 +28,7 @@ def test_project_frame_parses_and_uses_tokens() -> None:
"modern-di-faststream",
"modern-di-typer",
"modern-di-pytest",
+ "modern-di-aiogram",
"fastapi-sqlalchemy-template",
"litestar-sqlalchemy-template",
"lite-bootstrap",
@@ -35,6 +39,12 @@ def test_project_frame_parses_and_uses_tokens() -> None:
"db-retry",
"eof-fixer",
"semvertag",
+ "modern-di-arq",
+ "compose2pod",
+ "modern-di-celery",
+ "modern-di-taskiq",
+ "modern-di-flask",
+ "modern-di-grpc",
}
@@ -153,3 +163,74 @@ def test_render_projects_writes_cards_for_docs_repos_only(tmp_path: Path) -> Non
non_docs = set(p.MANIFEST) - DOCS_EXPECTED
for repo in non_docs:
assert not (tmp_path / repo / "social-card.svg").exists()
+
+
+# Regression guard for the transparent-background invariant: every mark is
+# drawn on a transparent background, and CREAM is only ever a knockout
+# painted ON TOP OF a GOLD shape — never standalone ink directly on the
+# background. Violate that and the mark looks fine on a cream/light page
+# (cream-on-cream is invisible) but shows stray bright-white ink once the
+# same mark.svg is placed on a dark surface (e.g. lockup-dark.svg's README
+# banner half). Parametrized over EXPECTED_REPOS (the full manifest, like
+# every other guard in this file) so a repo added later is covered
+# automatically instead of needing to be opted in.
+#
+def _cream_pixels_without_gold_beneath(
+ repo: str, tmp_path: Path, size: int = 256
+) -> list[tuple[int, int]]:
+ """Rasterize `repo`'s mark twice — once normally, once with every CREAM
+ knockout stripped to `none` — and return the (x, y) of every pixel that
+ is CREAM in the normal render but transparent once CREAM is stripped.
+ Such a pixel has no gold underneath it: it is standalone cream ink
+ sitting directly on the transparent background."""
+ svg = p.project_mark(repo)
+ normal_svg = tmp_path / f"{repo}-normal.svg"
+ normal_svg.write_text(svg, encoding="utf-8")
+ normal_png = tmp_path / f"{repo}-normal.png"
+ assert export_png(normal_svg, normal_png, width=size, height=size)
+
+ stripped = svg.replace(t.CREAM, "none")
+ # A literal, case-sensitive replace silently no-ops if cream were ever
+ # written in a different form (e.g. uppercase hex, rgb()) — make that
+ # loud instead of letting the guard pass vacuously against an unchanged
+ # (still-opaque) render.
+ if t.CREAM in svg:
+ assert stripped != svg, (
+ f"{repo}: stripping {t.CREAM} left the SVG unchanged — "
+ "the guard would compare a render against itself"
+ )
+ stripped_svg = tmp_path / f"{repo}-stripped.svg"
+ stripped_svg.write_text(stripped, encoding="utf-8")
+ stripped_png = tmp_path / f"{repo}-stripped.png"
+ assert export_png(stripped_svg, stripped_png, width=size, height=size)
+
+ normal_im = Image.open(normal_png).convert("RGBA")
+ stripped_im = Image.open(stripped_png).convert("RGBA")
+ cream_rgb = tuple(int(t.CREAM[i : i + 2], 16) for i in (1, 3, 5))
+ tol = 10
+ bad: list[tuple[int, int]] = []
+ for idx, ((r, gg, b, a), (_, _, _, sa)) in enumerate(
+ zip(normal_im.getdata(), stripped_im.getdata())
+ ):
+ if a < 200:
+ continue
+ if (
+ abs(r - cream_rgb[0]) < tol
+ and abs(gg - cream_rgb[1]) < tol
+ and abs(b - cream_rgb[2]) < tol
+ and sa < 50
+ ):
+ bad.append((idx % size, idx // size))
+ return bad
+
+
+@pytest.mark.skipif(
+ shutil.which("rsvg-convert") is None, reason="rsvg-convert not installed"
+)
+@pytest.mark.parametrize("repo", sorted(EXPECTED_REPOS))
+def test_no_cream_on_transparent(repo: str, tmp_path: Path) -> None:
+ bad = _cream_pixels_without_gold_beneath(repo, tmp_path)
+ assert not bad, (
+ f"{repo}: {len(bad)} cream pixel(s) with no gold beneath, e.g. {bad[:5]} "
+ "— this mark will show stray white ink on dark surfaces"
+ )
diff --git a/tests/test_symbols.py b/tests/test_symbols.py
index 1f228ae..2f3dd2d 100644
--- a/tests/test_symbols.py
+++ b/tests/test_symbols.py
@@ -49,3 +49,24 @@ def test_msg_symbol_parses(name: str) -> None:
@pytest.mark.parametrize("name", UTIL_SYMBOLS)
def test_util_symbol_parses(name: str) -> None:
minidom.parseString(_wrap(getattr(sym, name)(50, 50, 23)))
+
+
+NEW_SYMBOLS = [
+ "plane",
+ "hopper",
+ "pod",
+ "celery_stalk",
+ "task_q",
+ "flask_horn",
+ "rpc_arrow",
+]
+
+
+@pytest.mark.parametrize("name", NEW_SYMBOLS)
+def test_new_symbol_parses(name: str) -> None:
+ minidom.parseString(_wrap(getattr(sym, name)(50, 50, 23)))
+
+
+def test_box_and_ngon_emit_parseable_svg() -> None:
+ minidom.parseString(_wrap(sym._box(50, 50, 20)))
+ minidom.parseString(_wrap(sym._ngon(50, 50, 22, 7, 90, 3.5)))