Leumas Brand Marks
The creature each product and domain wears — briefing a mascot, turning it into a favicon, and the four surfaces it reaches. Read before touching any favicon, icon link or domain icon.
Creature marks
Every Leumas surface that answers "who am I" used to answer it the same way. Measured before ops/tools/brand-marks existed:
products/leumas-web/public/favicon.svgandproducts/leumas-studio/public/favicon.svgwere the
same drawing — one azure→violet "L", with the gradient stops swapped.
products/leumas-admin/index.htmllinked/logo.svginto a product with **nopublic/
directory at all**. That link had 404'd for as long as it existed.
- No domain anywhere carried a mark, a colour or an emoji — only a glyph NAME, so two domains
reading icon: 'coins' were indistinguishable by construction.
All three are invisible by design: a dead icon link renders the browser's blank-page glyph with no console error and no failed build, and a duplicated one just looks like a house style.
The one command
pnpm marks:plan # which of the 15 have art, which do not
pnpm marks:prompts --json # the ip-as-logo briefs, six per mark
pnpm marks — ingest <file> --mark domain:ai # accept the candidate you picked
pnpm marks:encode # masters → favicons + manifest
pnpm check:brand-marks # tracked · present · unique · links resolve · in budget
plan · prompts · ingest · encode · verify need no server, no auth, no credits and no generation API. Only prompts --json is meant to be fed to one. A tool that needed credits to tell you what art you already have would be useless on the day you need it most.
The fifteen
shared/apps/marks.js is the register — hand-authored, zero imports, read during Studio's first paint. Never quote a count from here; run pnpm marks:plan.
| namespace | what it names | examples |
|---|---|---|
brand: | the house mark, and the fallback for everything unmapped | brand:leumas — phoenix |
product: | a directory under products/ | product:web spider · product:studio octopus · product:admin lion |
domain: | a row in shared/apps/domains.js or Studio's nav.manifest.js | domain:hosting turtle · domain:ai owl |
manifest: | a nav manifest ROOT — not a domain | manifest:studio hammer |
surface: | a route with no domain of its own | surface:dashboard globe (/admin) |
The namespace has to be honest about what a row is. manifest:studio exists because there is no /admin/d/studio route and no studio row in DOMAINS — id: 'studio' in nav.manifest.js is the STUDIO_NAV root. Calling it domain: would have made markFor('studio') answer with it through MARKS_BY_DOMAIN, which is a lie that renders perfectly.
Two owls are deliberate. domain:ai is forward-facing in violet, product:docs is side-perched in emerald. Same species reads as siblings; they never collide in a 32×32 tab.
Studio wears two creatures on purpose. The octopus is the browser tab — eight arms on eight domains, what Studio is from outside. The hammer is the in-app nav glyph — the tool you are holding, what Studio is from inside.
How a mark is made
Adapted from s1dashu/ip-as-logo-skill (MIT): flat, cute, personified square mascots built to survive 32×32. That constraint is the favicon constraint, which is why it fits.
shared/apps/marks.js creature + accent, declared
↓ lib/brief.js six briefs — 3 directions × 2 lower corners, A1/A2/B1/B2/C1/C2
an image model seedream_v5_pro via the Higgsfield MCP, 1:1, 1k
↓ YOU pick one the source skill is explicit: a stochastic draw, no auto-ranking
ingest → .inbox/ re-encoded to a 512² webp, measured
↓ encode
shared/services/datacenter/src/library-assets/logos/<slug>-mark-<sha8>.webp → /library/logos/…
products/<p>/public/ favicon-32.png · favicon-192.png · apple-touch-icon.png · favicon.ico
↓ and rewrites
shared/apps/marks.generated.js ← GENERATED. Never hand-edit.
The model
seedream_v5_pro, chosen by measurement rather than preference. It was probed head to head against recraft_v4_1 (model_type: utility_vector) on the same phoenix brief: Recraft returns real SVG and lets you pin the palette exactly, which sounds decisive — but it drew thin strokes and hollow eye outlines that turn to mud at 32px, against a rule the brief states as "one solid silhouette with no outline". It also costs 2.5 credits against Seedream's 1.5. Re-probe before switching; do not switch on the argument that vector output must be better.
The rules that are not style preferences
Every one is in HOUSE in ops/tools/brand-marks/lib/brief.js, byte-identical in all fifteen prompts. That invariance is what makes fifteen independently-drawn pictures read as one set.
- Never say "logo", "icon", "brand mark", "favicon" or "emblem" in a generation prompt. Those
words pull hard toward flat corporate geometry — a swoosh, a monogram, a shield — and away from the personified character this whole system is. assertNoBannedWords refuses to emit a prompt containing one, because a single accidental "logo" produces a picture that looks fine and ships.
- Readable at 32×32 is the arbiter. Anything invisible there is omitted rather than shrunk.
- Both members of every pair are drawn. A model asked for simplicity drops one, and a one-winged
bird is the most obviously-wrong output this pipeline can produce.
- Exactly three colours, and they come from
ACCENT_PRESETSin@leumas/theme— the accent's
value and strong plus a #12141c ground. Not "a limited palette": three, named, per mark.
- Emerging from an assigned lower corner at 85–95% fill. A centred mascot at 32×32 is a small
blob ringed by dead background.
- No text in the art, ever. Every surface renders the thing's name in the DOM beside it.
Rules for the files
- Never
.svgin a served category./library/*emits bytes with no CSP, so a served SVG is
executable markup on our own origin. logos is already on the CATEGORIES allowlist in shared/services/datacenter/src/index.js — a category that is not on it 404s every file with no error anywhere. Product-local public/ is a different path; those are PNG and ICO.
- Content-addressed filenames, always.
/library/*is servedimmutable, max-age=31536000. A
stable name would make a re-render invisible for a year. encode deletes the superseded file.
favicon.icois hand-built, with no dependency.sharpcannot encode ICO;lib/ico.jswrites
the ICONDIR envelope around three PNG payloads. A browser asks for /favicon.ico whether or not any page links it.
- One opaque asset serves every surface. A transparent mark looks better in exactly one place (a
light-themed tab strip) and worse in the taskbar, the nav, and a dark tab.
- Never hand-edit
marks.generated.js. It is rebuilt from the directory listing, which is what
makes it incapable of naming a file that is not there.
Where a mark reaches — and the one resolver
markIcon(id, fallbackGlyph) in shared/apps/mark-icon.js is the only thing that decides which picture something wears. It returns the creature's url when one has been drawn and the caller's existing glyph name until then, so wiring a mark in is a no-op until the picture exists.
Never paste a mark url into a file. The urls are content-addressed, so a pasted one goes stale on the next re-render, silently, in whichever file nobody remembered.
renderIcon (shared/packages/ui/src/icons/resolve.jsx) already accepts a root-relative image URL as well as a glyph name — which is why three of the four surfaces below needed no new resolver:
| surface | wired at |
|---|---|
| browser tab, per product | the <link rel="icon"> set in each products/<p>/index.html |
| browser tab, per domain | useFavicon at DomainRoutes.jsx's activeDomain — Studio is one document that is forty places |
| the door's entrance | DefaultFallback in DomainRoutes.jsx, over the console skeleton |
| nav, doors, store, OS launcher | icon: in nav.manifest.js and defineDomainApp |
useFavicon in @leumas/features takes a URL, not a mark id — resolving one needs @leumas/apps, and features deliberately keeps no edge to it. [critical] It never sets an empty href: assigning '' makes the browser request the page URL as an icon, which returns HTML, fails to decode, and leaves the tab blank with nothing in the console.
[critical] The door's placeholder has its own stylesheet, routes/doorload.css, imported by DomainRoutes.jsx. domainhud.css is inside the door's lazy chunk, so styling the fallback from there means it renders unstyled for its entire life and the rules land just in time to be discarded.
The guard
pnpm check:brand-marks — six checks, each closing one silent regression.
- Tracked-ness, from git, FIRST, unconditionally. Every other check reads the working tree, so
on the machine that generated the art they all pass and a clean clone ships nothing. check-deck-assets.mjs once went green on 89 files that had never been added.
- Coverage — a mark declared and never generated.
- Uniqueness by content hash — the exact failure the two identical "L" favicons were.
- The manifest and the disk agree.
- Every
<link rel="icon">href resolves. The check that would have caught Admin on day one. - Per-file and total caps, and nothing but
.webpunderlogos/.
Check 5 reports a link as pending rather than dead when its product's mark has no master yet — encode writes every favicon from that master, so check 2 has already failed on it by name, and reporting one fact as two failures makes the second noise. Pending links are still counted out loud.
Adding a mark
Add a row to shared/apps/marks.js, then pnpm marks:prompts --mark <id>, generate, ingest, encode. Wire it with markIcon('<id>', '<the glyph it currently uses>') — never a pasted url. The guard's coverage check fails the build until the art exists, which is the point.
Related
ops/tools/brand-marks/README.md— the tool's own reference.leumas-app-art— the other art pipeline. app-art enumeratesshared/apps/registry.jsand draws
deterministically from sha256(appId): free, never needs a human, and never calls a model. This one enumerates marks.js, cannot render anything on its own, and has a person in the middle picking one of six. Two different jobs; they share sharp and the naming rule and nothing else.
shared/packages/ui/src/icons/resolve.jsx—renderIcon, the resolver that makes a url and a
glyph name interchangeable everywhere an icon: is read.