The AgentDeck mark: an ace-cut card carrying the A. The notched corner reads as both a playing card and a file, so "deck" lands twice without drawing a literal card - it is the best idea in the system and the reason the mark survives at one colour.
| File | Use it for |
|---|---|
logo.svg |
the mark with its spark - headers, READMEs, anywhere above ~48px |
logo-mark.svg |
the card alone - small sizes, dense UI, anywhere the spark would be noise |
logo-full.svg |
the three-colour lockup - blue card, Canvas A, Ace Red spark |
logo-blue.svg |
mono Agent Blue, for anywhere it is a file rather than markup |
logo-white.svg |
mono Canvas, same reason, for a dark ground |
logo-black.svg |
mono Night, same reason, for a light ground |
favicon.svg |
square and centred on the card - favicons, app icons, avatars |
logo-traced-original.svg |
provenance only - see below |
contributor-welcome.svg |
the card the bot posts on a first pull request |
contributor-merged.svg |
the card it posts after a first merged one |
Inlined as markup, logo.svg and logo-mark.svg need no variants at all: they are
fill="currentColor" and take the colour of whatever they sit in.
Everything else in that list exists because the proviso is real. An SVG loaded through <img src>
has no parent to inherit from, so currentColor falls back to black, and on a dark ground the
mark is then black on near-black. Verified side by side rather than assumed. So a file dropped
somewhere gets a variant that names its own colour: logo-blue.svg reads on both themes because
the A's counter is transparent and each background shows through it, logo-white.svg and
logo-black.svg cover the grounds where blue is wrong, and logo-full.svg is the lockup itself
for anywhere that can carry three fills.
favicon.svg is full colour for the same reason taken to its conclusion: a favicon is only ever
loaded as a file, so currentColor was never going to work there. It rendered black on dark
browser chrome until 2026-08-15.
<span style="color: #2563FF"> <!-- Agent Blue -->
<svg …>…</svg>
</span>| Part | What it is |
|---|---|
components/card.svg |
the ace-cut card carrying the A, which is a hole in the path |
components/spark.svg |
the spark alone, drawn geometry, box 880 152.5 196.8 196.7 |
components/wordmark.svg |
agentdeck as nine outlines from Poppins-SemiBold |
The spark is drawn geometry rather than a cleaned trace, so it is the one part of the mark that
no longer descends from logo-traced-original.svg. It is drawn rather than approximated, and it
replaced the traced spark everywhere at once, because two near-identical sparks is the worse thing
to maintain.
Every part is drawn in the mark space 246 145 832 933, so a composition pastes the <path>
with no transform at all. Position and scale live in the coordinates, and the only thing a
composition varies is the viewBox it frames them with. logo.svg is now two paths and nothing
else. The wordmark is the one exception: it is drawn in its own 0 95 516.86 143 space and placed
with a single translate, since it is not part of the mark.
Colour also belongs to the composition. The parts are currentColor; the dark-mode lockup is a
blue card, a white A and a red spark, and the part files do not know that. Every fill a
composition sets is a --brand-* token from the site's brand.css, which the test enforces:
the A counter sat at #ffffff beside a #fafbfe headline until it did.
Copy from here, never from a finished card. Every other SVG in this directory is a composition
of these parts, and tests/test_brand_assets.py fails if one carries geometry that is in no
component, or sets the wordmark as live text instead of outlines.
Copy, rather than reference, because reference does not survive the renderer:
| Mechanism | Result |
|---|---|
<use href="other.svg#id">, default Chrome flags |
renders blank, no error |
<use href="#id"> where the enclosing <svg> viewBox is tighter than the part's own space |
disappears, no error |
Both failures are silent and land in a PNG that nobody looks at twice. A single source of truth that survives every renderer would have to be generation rather than reference, and the test above buys most of what that would, for none of the build step.
Live <text> needs the font wherever it renders, and a renderer without it substitutes silently
rather than failing. Outlines remove that dependency, at the cost of the text no longer being
editable, which is the right trade for a word that never changes and the wrong one for card copy.
Take the positions from the browser, not from the font. Computing advances means reimplementing kerning, and the drift only shows when the two are overlaid.
// 1. what Chrome actually did, read off the live-text version
const t = document.querySelector('text')
for (let i = 0; i < t.textContent.length; i++) console.log(t.getStartPositionOfChar(i))# 2. the outline for each glyph, at those positions. fontTools stays out of the project:
# uv run --no-project --with fonttools python -
from fontTools.ttLib import TTFont
from fontTools.pens.svgPathPen import SVGPathPen
font = TTFont(FONT)
glyphs, cmap = font.getGlyphSet(), font.getBestCmap()
scale = SIZE / font["head"].unitsPerEm # font-size over unitsPerEm
for char, x, y in positions:
pen = SVGPathPen(glyphs)
glyphs[cmap[ord(char)]].draw(pen)
# glyph space is y-up, the canvas is y-down, so y scales negative
emit(f'<path transform="translate({x},{y}) scale({scale},{-scale})" d="{pen.getCommands()}"/>')Check the result by rendering both and diffing. Antialiasing on glyph edges is expected and
disappears under magick compare -fuzz 25%; anything surviving that is a real positioning error.
The rule and its reasoning are in docs/coding-standards.md §11. What it means here: the
wordmark PNG, the mark PNG and the fourteen-panel brand sheet live outside the repository,
with the designer's source files, and .gitignore keeps docs/brand/*.{png,webp,jpg} out of the
tree. Only vectors are tracked, because a vector is text: reviewable in a pull request, and a few
kilobytes. The mark loses nothing by this, since logo.svg supersedes its PNG entirely.
Rasters that a GitHub comment or the social-preview upload genuinely needs are the one exception,
and they belong under .github/assets/, not here. Everything in this directory regenerates from
a recipe, so treat a PNG next to these files as a build artifact that escaped.
Delivered. It is nine glyph outlines from Poppins-SemiBold, set the way this asked: from the real font, not traced from a screenshot. Positions are the browser's own, read back withwordmark.svg.getStartPositionOfCharso the kerning is the engine's rather than a reimplementation of it, at the weight and tracking the site already used. Recipe under Setting type as outlines.- The original vector for the mark.
logo-traced-original.svgis what was handed over: a potrace trace of a PNG, not an export. The tells are the SVG 1.0 DTD, thetranslate(0,1254) scale(0.1,-0.1)flip, and a viewBox of exactly the PNG's pixel width. The files here are cleaned from that trace - unitless,currentColor, titled, correctly cropped, each one rendered and measured in a browser rather than eyeballed. Two things a cleanup cannot fix: traced outlines are polygon approximations (invisible at UI sizes, visible on a hero or in print), and a crop fixes framing rather than drawing. - A 1280×640 social card. GitHub renders its grey default on every share to X, LinkedIn, Slack and Hacker News until one exists, and the brand sheet is not it - fourteen panels are unreadable at thumbnail size.
At 16px the spark is about three grey pixels - noise rather than signature. Keeping it would also force a non-square box, because it sits outside the card's top-right corner, and squaring that box shrinks the card to roughly two-thirds the width it could otherwise use. The brand sheet's own favicon panel shows the same shrinkage.
So favicon.svg is the card, centred in a square of its own height, filling the box edge to edge.
The card is already a rounded square, so it reads correctly under a platform's own icon mask. It
is Agent Blue with a Canvas A rather than currentColor, since nothing inlines a favicon.
docs-site/app/icon.svg is the same file under the name Next serves it by, and
tests/test_brand_assets.py holds the two together.
social-card.svg is the 1280×640 image GitHub shows under every link to this repository in X,
LinkedIn, Slack, Discord and Hacker News. Left unset - as it was until 2026-08-12 - GitHub renders
its grey default with the repository name in it.
It is not committed as a PNG, for the same reason nothing else here is: .gitignore:19 keeps
docs/brand/*.png out of the tree. Regenerate the upload artifact instead:
# Poppins (wordmark and tagline) and Inter (body) are live `<text>`, not paths, so they must be
# resolvable to the renderer. Skip if already installed system-wide.
mkdir -p ~/.local/share/fonts/agentdeck-render && cd "$_"
for f in ofl/poppins/Poppins-Bold.ttf ofl/poppins/Poppins-SemiBold.ttf ofl/inter/'Inter[opsz,wght].ttf'; do
curl -sSLO "https://github.com/google/fonts/raw/main/$f"
done && fc-cache -f
# Chrome shrinks a 640-tall SVG to make room for a scrollbar it then hides, so render into a
# taller window at 2x and crop. Rendering straight to 1280x640 silently clips the last line.
cd "$(git rev-parse --show-toplevel)/docs/brand"
{ printf '<!doctype html><meta charset="utf-8"><style>html,body{margin:0;background:#0b1220}svg{display:block;width:1280px;height:640px}</style>'; cat social-card.svg; } > /tmp/card.html
google-chrome --headless --disable-gpu --hide-scrollbars --force-device-scale-factor=2 \
--window-size=1280,800 --screenshot=/tmp/card-2x.png file:///tmp/card.html
magick /tmp/card-2x.png -crop 2560x1280+0+0 +repage -resize 1280x640 -strip social-card.pngThen upload social-card.png at Settings → General → Social preview. It is a manual upload;
no API sets it.
One thing in the SVG looks wrong and is not. The A is a hole in the card path rather than a
shape: both nonzero and evenodd knock it out, because its subpath winds against the outer
contour. The white rectangle behind the card is what the A shows through. Keep it inside the card
silhouette and below the cut corner, or it appears as a white edge.
The three cards each set the lockup as three fills (Agent Blue card, white A, Ace Red spark)
rather than the single currentColor the part files carry, which is the composition supplying
colour the parts deliberately do not know.
contributor-welcome.svg and contributor-merged.svg are what first-contribution.yml posts on
a contributor's first PR and after their first merged one. They are 1280×380 rather than the
social card's 1280×640: a comment renders them near 640px wide and near 320px on a phone, so the
type is sized for that render instead of scaled down from the social card.
Both carry their own copy of the lockup, same nested viewBox and same transform, taken from
components/ and held to it by tests/test_brand_assets.py. The merged card adds the spark alone and
enlarged on the right. Its tight box is x 880..1077, y 146..356 in the placed coordinate space,
measured with getBBox() rather than derived, since the nested viewBox is not square and
letterboxes under the default preserveAspectRatio.
Render them with the recipe above, changing only the height:
for card in contributor-welcome contributor-merged; do
{ printf '<!doctype html><meta charset="utf-8"><style>html,body{margin:0;background:#0b1220}svg{display:block;width:1280px;height:380px}</style>'; cat "$card.svg"; } > /tmp/$card.html
google-chrome --headless --disable-gpu --hide-scrollbars --force-device-scale-factor=2 \
--window-size=1280,560 --screenshot=/tmp/$card-2x.png file:///tmp/$card.html
magick /tmp/$card-2x.png -crop 2560x760+0+0 +repage -resize 1280x380 -strip /tmp/$card.png
doneThe PNGs belong under .github/assets/, which is the one place a tracked raster is allowed
(docs/coding-standards.md §11). They live there rather than here because a GitHub comment cannot
render an SVG.
The mark reads agentdeck; prose says AgentDeck SDK on first mention and AgentDeck
thereafter. The qualifier lives in titles, descriptions and the domain - not in the glyph, where it
would read as a sub-brand of a parent product that does not exist. See
docs/delivery/plan-adoption.md §1.
This was open - the sheet's §07 names Space Grotesk for headings while the wordmark looked like something else. Setting the social card decided it, because a wordmark either matches or it does not.
The wordmark is Poppins Bold. The a is single-storey, and Space Grotesk's is
double-storey - that alone rules it out, and the geometric e, the straight-tailed g, the
angled cut on the t and the straight k diagonals all agree. Rendered side by side against
wordmark.png, Poppins 700 matches and Space Grotesk 700 is not close.
So two of three sources always did agree, and the third is a label in the sheet contradicting the sheet's own artwork. Poppins is what the wordmark is, what the site loads, and what the card sets. Space Grotesk should be treated as a typo unless someone produces a wordmark drawn in it.
Amended 2026-08-15: the family holds, the weight does not. components/wordmark.svg is set in
Poppins SemiBold, not Bold. The site was already there and nobody noticed: app/layout.tsx
loads Poppins at weight: ['500', '600'] only, and sets its own AgentDeck wordmark at 600. So the
site could not render the Bold this section specifies even if asked, and the social card was the
only place the wordmark appeared at 700.
Tracking followed. The card had baked -0.0354em, a figure tuned against Bold and inherited by
the lighter weight unexamined, against the site's -0.02em. The outlines are regenerated at
-1.92, which is -0.02em at font-size 96, so the wordmark now matches the site in family,
weight and tracking.
The palette is not affected - the sheet's hexes and the site's brand.css already match exactly.
The wider presentation question stays with the docs-site design pass (#140).