Norq

Brand

Norq’s brand system is a single brand.yaml file at your project root. It owns every visual choice – colors, typography, spacing, radii, named styles, fonts, voice – so notification authors reference tokens by name ({colors.brand}, {spacing.md}, {.button-primary}) instead of hardcoding hex codes.

The on-disk shape is a documented subset of the W3C Design Tokens Community Group spec (DTCG 2025.10), so you can import from / export to Figma Tokens Studio, Style Dictionary, and DTCG-aware tools without losing fidelity.

Quickstart

norq brand init               # scaffold brand.yaml from compiled-in defaults
norq brand show               # print the resolved brand (aliases + pipes flattened)
norq brand sync-agents        # regenerate AGENTS_BRAND.md (AI agent context)

Edit brand.yaml, run norq brand show to verify, then norq lint to catch issues. norq doctor surfaces a “Brand” section in its summary that combines lint counts + AGENTS_BRAND.md staleness.

File structure

# brand.yaml
$schema: https://norq.dev/schemas/norq-brand.schema.json
 
meta:
  name: Acme
 
tokens:
  colors:
    brand:           "#6d28d9"
    brand-text:      "#ffffff"
    brand-hover:     "{colors.brand | darken 10%}"
    body:            "#374151"
    background:      "#ffffff"
 
  typography:
    body:
      fontFamily: "{fonts.sans}"
      fontSize: 16
      lineHeight: 1.6
    heading-1:
      basedOn: body
      fontSize: 36
      fontWeight: 700
      lineHeight: 1.1
 
  spacing:
    sm: 8
    md: 16
    lg: 24
 
  radii:
    sm: 4
    full: 9999
 
  codeTheme: base16-ocean.dark
 
styles:
  button-primary:
    bg:     "{colors.brand}"
    color:  "{colors.brand-text}"
    radius: "{radii.sm}"
 
defaults:
  image: { padding: 0 }
  heading: { color: "{colors.brand}" }
 
element-typography:
  h1: heading-1
  p:  body
 
fonts:
  sans:
    family: Inter
    weights: [400, 700]
    source: google
 
modes:
  colorMode: auto
  dark:
    colors:
      brand: "#a78bfa"
 
voice:
  description: Warm, decisive, restrained.
  principles:
    - Use the brand color for the single most important CTA per email
    - Maintain WCAG AA contrast (4.5:1 for normal text)

Where the file lives

By default the loader looks for ./brand.yaml next to your norq.config.yaml. To put it elsewhere, set the brand: key in your config:

# norq.config.yaml
notifications: ./notifications
brand: ./design/brand.yaml          # subdirectory
# brand: ../../packages/shared-brand/brand.yaml   # monorepo sibling
# brand: node_modules/@acme/brand/brand.yaml      # npm package
# brand: $BRAND_DIR/brand.yaml                    # env var expansion

If neither path resolves, Norq silently falls back to compiled-in defaults and prints an info diagnostic.

Tokens

Five sections, all flat (no nested groups):

Section What it holds Sugar
colors sRGB hex strings, {ref}s, or piped expressions
typography composite tokens with fontFamily / fontSize / fontWeight / lineHeight / letterSpacing basedOn: for token-level inheritance
spacing dimensions (px / rem) bare numbers → px
radii dimensions (px / rem) bare numbers → px
codeTheme string name of a syntect TextMate theme

Colors

tokens:
  colors:
    brand:        "#6d28d9"          # literal sRGB hex
    primary:      "{colors.brand}"   # alias
    brand-hover:  "{colors.brand | darken 10%}"
    muted:        "{colors.body | alpha 0.6}"

v1 accepts only sRGB hex strings. #RRGGBB, #RRGGBBAA, #RGB, and #RGBA all parse. Wide-gamut authoring (display-p3, oklch, lab) is a v1.x follow-up; on import from DTCG-encoded sources, wide-gamut values are preserved via $extensions for round-trip but not used at compile time.

Color pipes

Five pipes are supported in v1:

Pipe Args Example
darken <pct> percentage {colors.brand | darken 10%}
lighten <pct> percentage {colors.brand | lighten 20%}
alpha <0..1> float in [0, 1] {colors.body | alpha 0.6} (emits rgba(...))
mix <hex> <pct> hex + percentage {colors.brand | mix #ffffff 20%}
contrast <target> hex {colors.body | contrast #000} (picks one of two colors based on contrast against target)

saturate, desaturate, hue-rotate, and arithmetic on dimensions are deferred (v1.1+).

Typography

tokens:
  typography:
    body:
      fontFamily: "{fonts.sans}"
      fontSize: 16
      lineHeight: 1.6
    heading-1:
      basedOn: body
      fontSize: 36
      fontWeight: 700
      lineHeight: 1.1

basedOn: is Norq-specific token-level inheritance. The child deep-merges atop the parent (child wins per field). Cycles are detected and flagged as brand/cyclic-ref.

basedOn: is not DTCG $extends – DTCG $extends is a group-level feature with different semantics. The keyword difference is intentional.

fontFamily accepts a literal CSS font-family string OR a {fonts.X} reference. The reference form picks up fonts.X.family from the fonts: block, so font delivery and typography stay in sync.

lineHeight is multiplier-only in v1. DTCG dimension form is coerced to a multiplier with a warning during DTCG import.

Spacing and radii

tokens:
  spacing:
    xs: 4               # bare number → px
    sm: "8px"           # explicit px
    md: 16
    lg: "1rem"          # explicit rem
  radii:
    none: 0
    sm: 4
    full: 9999

em and % are rejected at parse time. Bare numbers are sugar for px.

Aliases and reference resolution

Any string token value of the form {section.name} is an alias. The resolver:

  1. Looks up section.name in the parsed brand.
  2. Recursively resolves the target’s own raw value (so chains like a → b → c → "#xxx" collapse to a single literal).
  3. Detects cycles (brand/cyclic-ref) and broken refs (brand/broken-ref).

Cross-section refs are allowed where they make semantic sense: typography.body.fontFamily: "{fonts.sans}" resolves to fonts.sans.family. dimension aliases stay within spacingradii.

Named styles

styles:
  button-primary:
    bg:     "{colors.brand}"
    color:  "{colors.brand-text}"
    radius: "{radii.sm}"
  hero-headline:
    typography: "{typography.heading-1}"
    color:      "{colors.heading}"

Apply to a block element via the {.name} syntax:

[Shop now](https://acme.com){.button-primary}
 
# Welcome{.hero-headline}

Multiple styles compose left-to-right; inline attrs override the bundle.

Cascade order

built-in compiler defaults
  ← brand.yaml defaults.<element-type>
  ← brand.yaml element-typography.<tag>   (typography-only)
  ← brand.yaml styles.<bundle>            (via {.name} on element)
  ← template frontmatter `brand:` block   (partial override of anything above)
  ← inline template attrs

Later rows win.

Per-element defaults

defaults:
  image:   { padding: 0 }
  button:  { radius: "{radii.sm}" }
  heading: { color: "{colors.brand}" }

Recognised element types: image, button, heading, text, divider, section. Anything else fires brand/unknown-element-type (warning).

Heading → typography mapping

element-typography:
  h1: heading-1
  h2: heading-2
  h3: heading-3
  h4: heading-4
  p:  body

This mapping is stable per project – it can’t be overridden in a per-template frontmatter brand: block. Letting one template remap h1heading-3 is an accident, not a feature.

Fonts

fonts:
  sans:
    family: Inter
    weights: [400, 700]
    source: google
  display:
    family: "Cal Sans"
    weights: [600, 700]
    source: url
    url: https://cdn.acme.com/fonts/cal-sans.css
  serif:
    family: Charter
    source: system          # no @import; assume the OS has it

source values: google (default), system, url (requires url:), bunny (Bunny Fonts CDN).

Set inject: false if your delivery pipeline already loads the font and you don’t want the email compiler emitting a <link> tag.

A typography token references a font by {fonts.X} (resolves to fonts.X.family):

typography:
  body:
    fontFamily: "{fonts.sans}"      # → "Inter"

If the font block is missing, lint fires brand/font-not-delivered (warning).

Modes

modes:
  colorMode: auto       # light | dark | auto
  dark:
    colors:
      brand:      "#a78bfa"
      heading:    "#e0e7ff"
      body:       "#d1d5db"
      background: "#0f172a"
      content:    "#1e293b"

auto emits @media (prefers-color-scheme: dark) CSS in the rendered email using the dark.colors overrides. dark forces a static dark render. light (default) skips the media query.

Only the email channel honours dark-mode overrides today. Other channels ignore them.

Voice

voice:
  description: Warm, decisive, restrained.
  principles:
    - Use the brand color for the single most important CTA per email
    - Maintain WCAG AA contrast (4.5:1 for normal text)
    - Imperative mood; lead with what changed

Free-form prose flows into the generated AGENTS_BRAND.md (regenerate with norq brand sync-agents). Recognised-rule registry that toggles additional lints from principles is a v1.1 follow-up.

Using brand tokens in template attrs

Inline bg= and color= attrs accept three forms:

Form Example Resolves to
Brand color name color="brand", color="secondary-text", bg="card" Looks up brand.tokens.colors.<name> and uses its resolved hex.
Hex literal color="#6d28d9" Passed through verbatim.

color= and bg= resolve in two tiers: literal #hex first, then any name in brand.tokens.colors. Unknown values fail with template/unknown-color-token / template/unknown-bg-token (severity Error).

::: highlight {bg="background" color="body"}
A subdued highlight using brand-defined colors.
:::
 
::: hero {bg="brand"}
[Get started](https://acme.com){.button-primary}
:::

Unknown values fire template/unknown-color-token / template/unknown-bg-token (Error) — silent failures used to render as empty backgrounds; now you get a diagnostic listing every valid option for your project.

The full {colors.X} brace-syntax used inside brand.yaml itself is not yet honoured in template attr values. Use the bare name form (bg="secondary") instead. Brace-syntax in templates is a v1.1 follow-up.

Per-template overrides

Individual templates can override any piece of brand.yaml by adding a brand: block in their frontmatter:

---
subject: "Flash sale — 48 hours"
 
brand:
  colors:
    brand:      "#ff4500"            # campaign orange
    background: "#fff7ed"
  styles:
    button-primary:                  # full pill for this campaign
      bg:     "{colors.brand}"
      color:  "#ffffff"
      radius: "{radii.full}"
  defaults:
    image: { padding: 16 }
---
 
# Flash sale{.hero-headline}
 
[Shop now](https://acme.com/sale){.button-primary}

{colors.brand} inside the override block resolves to #ff4500 (the override), not the project-level value. Standard rule: overrides are merged first, then references resolve against the merged brand.

What’s overridable: tokens.colors, tokens.typography, tokens.spacing, tokens.radii, tokens.codeTheme, styles, defaults.

What’s NOT overridable: element-typography (project-stable), modes.dark (project-wide), fonts (delivery concern), voice, meta.

Import / export

norq brand import DESIGN.md             # convert from DESIGN.md
norq brand import tokens.json           # auto-detects DTCG / Figma / Style Dictionary
norq brand export --format dtcg         # canonical DTCG 2025.10 JSON
norq brand export --format tailwind     # tailwind.config.js stub
norq brand export --format css          # :root { --colors-brand: #...; }

Pipes are pre-resolved before export – DTCG has no concept of computed tokens, so the exported file is a snapshot. Norq-specific bits (styles, voice, fonts, modes) ride along under $extensions.norq for lossless round-trip imports.

See norq brand for the full CLI surface.

Lints

Brand lints run once per project and attach diagnostics to brand.yaml. Full reference: Lint Rules.

Code Severity Description
brand/broken-ref error A {path} alias or basedOn: target doesn’t exist.
brand/cyclic-ref error Token alias chain or basedOn: chain forms a cycle.
brand/unknown-pipe error Color expression uses an unsupported pipe.
brand/invalid-pipe-arg error Pipe receives the wrong arg shape.
brand/bad-color error Color value is not a parseable sRGB hex.
brand/invalid-dimension error Dimension uses a unit other than px / rem.
brand/missing-primary warn No colors.brand token.
brand/missing-typography warn An element-typography.<tag> mapping points at undefined typography.
brand/font-not-delivered warn A typography fontFamily references {fonts.X} but the block is missing.
brand/orphaned-token warn A custom token is defined but unreferenced. The 17 EmailTheme color names + body/code typography are exempt.
brand/unknown-element-type warn defaults.<x> for an element the compiler doesn’t recognise.

email/low-contrast also walks brand.styles.* bundles that declare both bg and color and resolves them through the brand before the WCAG-AA check.

AI agent context

Run norq brand sync-agents to regenerate AGENTS_BRAND.md – a single Markdown file every AI coding agent reads as visual-identity context:

  • Voice + principles (from voice:)
  • Token reference list (every defined token by path)
  • Heading → typography mapping
  • Quickview (a few representative resolved values)

Use --scope rules for the smallest payload (principles only – system-prompt-friendly), --scope all for the full file (default), --check in CI to fail when the file is stale, --stdout to pipe straight into another process. norq init adds a ## Visual identity pointer to existing AGENTS.md / CLAUDE.md files at the project root.

DTCG conformance

Norq’s brand model maps to the DTCG 2025.10 spec for color, dimension, and typography token types. Known deviations:

DTCG concept Norq behaviour
Color $value as object with colorSpace + components Norq stores hex; synthesises object on DTCG export. v1.x opens up wide-gamut authoring.
Dimension units px, rem only Same, plus bare-number sugar → px. em / % rejected.
Group-level $extends Norq has token-level basedOn: on typography only.
Composite types (shadow, gradient, border, transition) Reserved schema slots; preserved via $extensions; not parsed/emitted. v1.1+.
fontFamily and fontWeight as first-class DTCG types Collapsed into typography.*.fontFamily / .fontWeight. DTCG imports flattened.
Pipes (darken, alpha, …) Norq extension, not DTCG. Pre-resolved before DTCG export.
$extensions pass-through Supported on every token shape.

Full conformance chapter: docs/spec/current/brand.md.

See also

  • norq brand CLI – init / show / import / export / diff / sync-agents
  • Configuration – the brand: key in norq.config.yaml
  • Whitelabeling – per-recipient brand at send time for multi-tenant SaaS
  • Lint Rules – full reference for brand and email rules