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