// The Marks the Logo may place, and the refusal when it may not. // // The Brand owns the drawing; the DS owns only the placement (ADR 0004). So // this module enumerates what the pinned Assets release ships and nothing // else: five layouts, two surfaces, and one hole where the release ships no // Mark. It never composes, recolors or substitutes a Mark — the asset licence // binds that literally, and a variant that does not exist upstream is a Brand // proposal, not a local file. // // A Mark is reached by importing the file, so it stays opaque bytes the whole // way through: the bundler emits the SVG as an asset and hands back its URL, // which is what the Logo renders as an . Nothing here parses the drawing, // and no code path can alter it — "reproduced unaltered" is therefore a // property of how the Logo works rather than a rule someone has to follow. // // `on` names the SURFACE behind the Mark, never the Theme: a dark hero on a // light marketing page takes `on="dark"` under the light Color Scheme, which is // exactly the case ADR 0004 fixed the prop for. import horizontalColor from "./marks/reddb-horizontal-color.svg"; import horizontalInverse from "./marks/reddb-horizontal-inverse.svg"; import iconColor from "./marks/reddb-icon-color.svg"; import stackedColor from "./marks/reddb-stacked-color.svg"; import stackedInverse from "./marks/reddb-stacked-inverse.svg"; import symbolBlack from "./marks/reddb-symbol-black.svg"; import symbolColor from "./marks/reddb-symbol-color.svg"; import symbolKnockout from "./marks/reddb-symbol-knockout.svg"; import symbolWhite from "./marks/reddb-symbol-white.svg"; import wordmarkBlack from "./marks/reddb-wordmark-black.svg"; import wordmarkWhite from "./marks/reddb-wordmark-white.svg"; /** * The pinned Brand Assets release these Marks came from * (`vendor/brand/brand.lock.json`). * * Written here as a literal because a Kit is vendorable source with no build * step and no lock to read at runtime — and kept honest rather than * remembered: `test/marks.test.ts` fails when it drifts from the lock, which * is the same edit re-vendoring makes anyway. */ export const BRAND_RELEASE = "2026.10"; /** How the Mark is laid out. */ export const LOGO_LAYOUTS = ["horizontal", "stacked", "icon", "symbol", "wordmark"] as const; export type LogoLayout = (typeof LOGO_LAYOUTS)[number]; /** * The surface the Mark sits on — the local background, not the global Theme. * `light` takes the color Mark, `dark` takes the inverse one. */ export const LOGO_SURFACES = ["light", "dark"] as const; export type LogoSurface = (typeof LOGO_SURFACES)[number]; /** One Mark as the release published it. */ export interface Mark { /** File name, exactly as the release names it. */ file: string; /** The URL the bundler emitted for the file. */ src: string; /** The drawing's own width, from its viewBox. */ width: number; /** The drawing's own height, from its viewBox. */ height: number; /** * How much of that height the SYMBOL — the red glyph — occupies. * * The symbol is the same drawing in every layout, which is what makes it the * thing to measure: clearspace is 25% of symbol height (ADR 0004), and one * minimum on the symbol gives every lockup a minimum without several numbers * anyone has to keep in step. In the horizontal, icon and symbol Marks the * symbol spans the full height; in the stacked one it sits above the * wordmark and occupies 100 of the drawing's 154.423 units. The standalone * wordmark has no symbol, so its own 71.4-unit ink box is the sizing * reference. */ symbolHeight: number; } /** * Every Mark the pinned release ships, by layout and surface. * * The hole is the point: `icon` has no `dark` entry because the release * publishes no `reddb-icon-inverse.svg`. It is left absent rather than filled * with the color Mark, so the gap is a fact the type system and the refusal * both read off one place. */ export const MARKS: Readonly>>> = { horizontal: { light: { file: "reddb-horizontal-color.svg", src: horizontalColor, width: 397.712, height: 100, symbolHeight: 100, }, dark: { file: "reddb-horizontal-inverse.svg", src: horizontalInverse, width: 397.712, height: 100, symbolHeight: 100, }, }, stacked: { light: { file: "reddb-stacked-color.svg", src: stackedColor, width: 140, height: 154.423, symbolHeight: 100, }, dark: { file: "reddb-stacked-inverse.svg", src: stackedInverse, width: 140, height: 154.423, symbolHeight: 100, }, }, icon: { light: { file: "reddb-icon-color.svg", src: iconColor, width: 100, height: 100, symbolHeight: 100, }, // No `dark`: the pinned release ships no icon inverse. The chatbot avatar // on a dark surface is blocked here until the Brand ships one (ADR 0004). }, symbol: { // The Brand names these two single-colour variants by their intended // grounds: black for light, white for dark. `color` is the primary // treatment and `knockout` describes transparency, not a third kind of // surface, so neither distorts the `on` vocabulary. light: { file: "reddb-symbol-black.svg", src: symbolBlack, width: 100, height: 100, symbolHeight: 100, }, dark: { file: "reddb-symbol-white.svg", src: symbolWhite, width: 100, height: 100, symbolHeight: 100, }, }, wordmark: { light: { file: "reddb-wordmark-black.svg", src: wordmarkBlack, width: 308.3, height: 71.4, symbolHeight: 71.4, }, dark: { file: "reddb-wordmark-white.svg", src: wordmarkWhite, width: 308.3, height: 71.4, symbolHeight: 71.4, }, }, }; /** * Every SVG drawing the pinned release publishes. * * Most are selected by layout and surface through `MARKS`. The color symbol * and knockout symbol are treatments rather than surfaces: they belong in the * catalogue and ship byte-identically with the Kit, but do not invent a third * `on` value or silently displace the Brand's explicit black/light and * white/dark mapping. */ export const MARK_CATALOGUE: readonly Mark[] = [ ...LOGO_LAYOUTS.flatMap((layout) => LOGO_SURFACES.map((on) => MARKS[layout][on]).filter( (mark): mark is Mark => mark !== undefined, ), ), { file: "reddb-symbol-color.svg", src: symbolColor, width: 100, height: 100, symbolHeight: 100, }, { file: "reddb-symbol-knockout.svg", src: symbolKnockout, width: 100, height: 100, symbolHeight: 100, }, ]; /** * The refusal: the Logo was asked for a Mark the pinned release does not ship. * * It names the gap and the release, because those are the two things whoever * hits it needs — what is missing, and which release it is missing from — and * because the fix is upstream: the message is the first draft of the Brand * proposal. */ export class MarkNotShippedError extends Error { /** The layout that was asked for. */ readonly layout: LogoLayout; /** The surface it was asked for. */ readonly on: LogoSurface; /** The Assets release that ships no such Mark. */ readonly release: string; constructor(layout: LogoLayout, on: LogoSurface, release: string = BRAND_RELEASE) { const alternatives = LOGO_LAYOUTS.filter((other) => MARKS[other][on] !== undefined); super( `Logo: the pinned Brand Assets release ${release} ships no ${layout} Mark for a ${on} surface.\n` + "The Logo refuses rather than falling back to the other surface's Mark or recoloring one: " + "the Brand owns the drawing and the DS owns only its placement (ADR 0004), so a variant " + "that does not exist upstream is a Brand proposal, not a local file.\n" + `Until the Brand ships it, place the ${layout} Mark on the surface it has ` + `(on="${on === "dark" ? "light" : "dark"}"), or use ` + `${alternatives.map((other) => `layout="${other}"`).join(" or ")} on a ${on} surface.`, ); this.name = "MarkNotShippedError"; this.layout = layout; this.on = on; this.release = release; } } /** * The Mark for one layout on one surface, or a loud refusal when the release * ships none. Never falls back, never invents: those are the two failures the * licence forbids, so neither has a code path here. */ export function selectMark(layout: LogoLayout, on: LogoSurface): Mark { const mark = MARKS[layout][on]; if (mark === undefined) throw new MarkNotShippedError(layout, on); return mark; } /** Does the pinned release ship a Mark for this layout on this surface? */ export function shipsMark(layout: LogoLayout, on: LogoSurface): boolean { return MARKS[layout][on] !== undefined; }