---
version: alpha
name: Intentface
description: Dark-mode-first design system for Intentface. Translucent surfaces over a soft iridescent gradient, Intent Sans for display and body paired with IBM Plex Mono for utility.

colors:
  # Brand accent (used as diffuse gradient atmosphere, never as flat fill)
  brand-pink: "#f7aef8"
  brand-sky: "#ade8f5"
  brand-cream: "#fff4c5"

  # Dark mode (default). Muted text is white-at-low-opacity rather than a named
  # gray hex so it stays neutral on the brand gradient.
  background: "#18181c"
  on-surface: "#fafafa"
  on-surface-muted: "rgb(255 255 255 / 0.55)"
  border: "rgb(255 255 255 / 0.08)"
  primary: "#fafafa"
  on-primary: "#18181c"
  destructive: "hsl(0 62.8% 30.6%)"

  # Light mode — symmetric to dark, with black at low opacity for muted text.
  # The light background is a cool off-white, not pure white, so the gradient
  # atmospherics read cleanly on both themes.
  background-light: "#e8e8ea"
  on-surface-light: "#18181c"
  on-surface-muted-light: "rgb(0 0 0 / 0.55)"
  border-light: "rgb(0 0 0 / 0.1)"
  primary-light: "#18181c"
  on-primary-light: "#fafafa"

typography:
  display-xl:
    fontFamily: Intent Sans
    fontSize: 72px
    fontWeight: 500
    lineHeight: 1.05
    letterSpacing: -0.02em
  display-lg:
    fontFamily: Intent Sans
    fontSize: 48px
    fontWeight: 500
    lineHeight: 1.1
    letterSpacing: -0.01em
  heading-md:
    fontFamily: Intent Sans
    fontSize: 24px
    fontWeight: 500
    lineHeight: 1.3
  heading-sm:
    fontFamily: Intent Sans
    fontSize: 18px
    fontWeight: 500
    lineHeight: 1.4
  body-lg:
    fontFamily: Intent Sans
    fontSize: 18px
    fontWeight: 400
    lineHeight: 1.5
  body-md:
    fontFamily: Intent Sans
    fontSize: 16px
    fontWeight: 400
    lineHeight: 1.5
  mono-md:
    fontFamily: IBM Plex Mono
    fontSize: 14px
    fontWeight: 400
    lineHeight: 1.5
  mono-sm:
    fontFamily: IBM Plex Mono
    fontSize: 12px
    fontWeight: 400
    lineHeight: 1.4
  label-sm:
    fontFamily: IBM Plex Mono
    fontSize: 12px
    fontWeight: 400
    letterSpacing: 0.05em

rounded:
  none: 0
  sm: 4px
  md: 8px
  lg: 16px
  xl: 24px
  full: 9999px

spacing:
  none: 0
  xs: 4px
  sm: 8px
  md: 16px
  lg: 24px
  xl: 32px
  2xl: 48px
  3xl: 64px
  4xl: 96px
  5xl: 128px

components:
  card:
    backgroundColor: "{colors.on-surface}"
    backgroundOpacity: 0.04
    borderWidth: 0
    rounded: "{rounded.lg}"
    padding: "{spacing.lg}"
    bodyTypography: "{typography.body-md}"
    titleTypography: "{typography.heading-md}"
  button-primary:
    backgroundColor: "{colors.primary}"
    textColor: "{colors.on-primary}"
    rounded: "{rounded.full}"
    padding: 12px 24px
    typography: "{typography.body-md}"
  button-secondary:
    backgroundColor: transparent
    textColor: "{colors.on-surface}"
    borderColor: "{colors.border}"
    borderWidth: 1px
    rounded: "{rounded.full}"
    padding: 12px 24px
    typography: "{typography.body-md}"
  button-ghost-pill:
    backgroundColor: "{colors.on-surface}"
    backgroundOpacity: 0.9
    textColor: "{colors.on-primary}"
    rounded: "{rounded.full}"
    padding: 8px 16px
    typography: "{typography.body-md}"
  tag-pill:
    backgroundColor: transparent
    textColor: "{colors.on-surface-muted}"
    borderColor: "{colors.border}"
    borderWidth: 1px
    rounded: "{rounded.full}"
    padding: 4px 10px
    typography: "{typography.mono-sm}"
  section-label:
    backgroundColor: transparent
    textColor: "{colors.on-surface-muted}"
    typography: "{typography.label-sm}"
  header:
    backgroundColor: transparent
    position: relative
    # No bottom border. The header sits transparently in-flow so the gradient
    # behind the hero reads as one continuous atmosphere. Dividers belong inside
    # the page, not under the chrome.
  logo:
    component: src/components/ui/intentface-logo.tsx
    defaultWidth: 108px
    fill: currentColor # wordmark inverts with text color; gradient asterisk fixed

breakpoints:
  sm: 480px
  md: 768px
  lg: 1024px
  xl: 1280px
---

# Design System

## Overview

The visual language is dark-mode-first, technical, and confident. Bold display type carries the headline; monospace utility text frames it; everything sits on a soft iridescent gradient that hints at the generative domain without resorting to typical AI imagery.

Three rules carry most of the weight:

1. Dark mode is the default. Design dark first, then adapt.
2. Monospace next to Intent Sans is the signature pairing. Mono frames, Intent Sans states.
3. The brand gradient is atmosphere, never chrome. Surfaces are translucent so the gradient passes through them.

**Theme toggle ships at launch.** Both dark and light tokens are live in `@theme`; the toggle in the header (`src/components/ui/theme-toggle.tsx`) writes `data-theme` on `<html>` and persists the choice. Dark is the unconditional default — the no-flash script in `app/layout.tsx` does not read `prefers-color-scheme`, only an explicit user choice flips to light.

## Colors

### Dark mode (default)

- **Background** (`#18181c`): The base color of the page — a warm near-black (zinc-900 territory). The conic gradient sits on top of this base as atmospheric blobs.
- **On-surface** (`#fafafa`): Primary text and icons. Also the base color for translucent surfaces (cards, panels) at low opacity.
- **On-surface muted** (`rgb(255 255 255 / 0.55)`): Secondary text, captions, metadata, descriptions inside cards. White-at-alpha rather than a named gray, so it stays neutral on the gradient.
- **Border** (`rgb(255 255 255 / 0.08)`): Hairline borders on cards, dividers, secondary buttons.
- **Primary** (`#fafafa`): Filled CTA buttons. Inverts to on-primary text.
- **On-primary** (`#18181c`): Text on filled primary buttons.

### Light mode

Mirrors the dark palette. Same structural roles, inverted values. The background is **not** pure white — it is a cool off-white so the iridescent gradient still reads on light.

- **Background-light** (`#e8e8ea`)
- **On-surface-light** (`#18181c`)
- **On-surface-muted-light** (`rgb(0 0 0 / 0.55)`)
- **Border-light** (`rgb(0 0 0 / 0.1)`)
- **Primary-light** (`#18181c`)
- **On-primary-light** (`#fafafa`)

### Brand accent (gradient)

A conic gradient sweeps through three soft hues:

- **Pink** `#f7aef8`
- **Sky** `#ade8f5`
- **Cream** `#fff4c5`

Used in three places, nowhere else:

- A diffuse glow behind the hero headline (low opacity, heavily blurred).
- Inside the asterisk glyph next to the logo.
- An animated vertical accent strip (rare, decorative).

Never use these as solid fills, button backgrounds, or text colors.

### No hard surface colors

There are no `surface` or `card-background` hex tokens. Surfaces are built by overlaying `on-surface` on the background at low opacity, so the gradient atmospherics pass through.

- Card fill: `bg-foreground/5` in Tailwind terms (or `on-surface` at ~4% alpha).
- Header fill: `bg-background/60` with a backdrop blur, so the gradient still bleeds through the header.
- Tag pills, secondary buttons: fully transparent fill, hairline border only.

This is non-negotiable. Solid surface colors flatten the look and kill the gradient.

## Typography

Two families, two jobs. The split is strict.

- **Intent Sans** — Display and body. Headlines, page titles, card titles at large sizes (medium weight, 500). Body copy, descriptions, and long-form text at regular weight (400). A geometric sans with a slightly mechanical feel that holds up at both 72px and 16px. Intent Sans is Intentface's own minor version of TASA Orbiter V1 (Weizhong Zhang / Local Remote, SIL OFL 1.1): identical outlines at normal width, plus a width axis (`wdth` 90–125), an optical-size axis (`opsz` 8–60), and a lighter Text Regular. Specimen, playground, and downloads live at `/brand/intent-sans`; the files ship from `public/fonts/IntentSans-VF.{woff2,ttf}`. Fallback stack when the web font fails: Helvetica Neue, then Arial, then the system sans.
- **IBM Plex Mono** — Utility. Subtitles, taglines, tag pills, section labels, technical metadata, code, timestamps.

The pairing rule: Intent Sans at large weight is what you read first. Plex Mono frames it (subtitle, label, tag). Intent Sans at regular weight is what you read at length.

Base size is 16px below 480px, 18px above.

### Type roles in practice

- **Hero headline**: `display-xl` in Intent Sans Medium.
- **Hero subtitle directly under headline**: `body-lg` in Intent Sans Regular. Plex Mono is reserved for utility text (section labels, tag pills, the optional eyebrow on the podcast hero) — not for the hero subtitle.
- **Section heading** ("Latest articles", "More projects"): `display-lg` in Intent Sans Medium.
- **Card title** ("AI-powered recruitment campaigns in minutes"): `display-lg` or `heading-md` depending on hierarchy.
- **Card body and description**: `body-md` in Intent Sans Regular.
- **Section label** ("WE WORK WITH AMAZING CLIENTS", "THE CLIENT", "OUR APPROACH"): `label-sm` in Plex Mono, uppercase, letter-spaced, muted color. Apply uppercase via CSS, not by storing pre-uppercased strings.
- **Tag pill** ("IMAGE GENERATION", "LLMS"): `mono-sm` in Plex Mono, uppercase.

## Layout

Layout is a centered max-width container (`max-w-6xl`, ~1056px) with generous outer margins on desktop (`px-6 lg:px-8`). Mobile is fluid full-width with side gutters of `spacing.md` (16px).

Vertical rhythm is loose, not tight. Hero sections breathe. Cards have heavy internal padding (`spacing.lg`). Section spacing is `spacing.4xl` (96px) or `spacing.5xl` (128px) on desktop, scaled down on mobile.

The spacing scale (`xs` through `5xl`) is the only allowed set of vertical and horizontal spacing values. No off-scale numbers.

## Elevation & Depth

There are no shadows. Elevation is achieved by two means:

1. **Translucent layering.** Cards and panels are `on-surface` at 3-8% alpha over the background gradient. The lift is created by the slight color shift plus the hairline border, not by a drop shadow.
2. **Tonal contrast.** The page background is the darkest layer. Cards sit slightly above it via the translucent overlay. The brightest surfaces are filled primary buttons (`primary` solid).

The header is a special case: translucent background fill (60% alpha) plus a 12px backdrop blur, so the gradient still passes through but the content behind stays legible.

Do not introduce `box-shadow` for elevation. If something needs to feel higher, increase the border contrast or the alpha of the surface overlay.

## Gradient atmospherics

The iridescent gradient is a brand fixture, not a decoration. The implementation lives in [src/components/ui/animated-gradient.tsx](src/components/ui/animated-gradient.tsx) and the per-route hues in [src/lib/gradient-palettes.ts](src/lib/gradient-palettes.ts) — both files encode the rules below. Read them as one specification, not as two independent systems.

### Anatomy

Each gradient is made of three layers, painted in this order:

1. **Background fill.** A solid base color, painted edge-to-edge. On dark this is near `#18181c`; on light it's a tinted off-white (`#e8e8ea` and family) — never pure white. Per-route palettes can shift the base by a few RGB units to set mood (slightly warmer for `/work`, cooler for `/services`) but the shift stays within ~10 units of the canonical base. Big bg jumps between routes read as a flash, not an atmosphere.
2. **Three soft radial blobs.** Drawn over the base, never edge-to-edge. Each blob is a multi-lobe radial sprite with heavy falloff (alpha stops `1 → 0.82 → 0.5 → 0.25 → 0.08 → 0.02 → 0`) so the silhouette is an irregular organic puff, not a perfect circle. Blobs are pre-baked as sprites and reused per frame — the cost is one fillRect + three drawImage per paint.
3. **Soft-light noise grain.** A 128px monochrome dither tile tiled over everything at ~40% opacity in `mix-blend-mode: soft-light`. The noise nudges luminance by ±16 units around mid-grey; it modulates the blob colours from within rather than sitting on top as visible hiss. Without it the gradient banding shows on modern displays.

### Never use a full-opacity gradient

Blobs are drawn with a global alpha of **0.3–0.5**, never 1.0. The reference values are:

| Mood                          | Palette alpha (dark) | Palette alpha (light) |
| ----------------------------- | -------------------- | --------------------- |
| Quiet (about, contact, team)  | 0.34                 | 0.30                  |
| Default (home, fallback)      | 0.40                 | 0.40                  |
| Saturated (labs, brand pages) | 0.46–0.50            | 0.46–0.50             |

If the gradient ever reads as a flat colored panel rather than atmosphere passing through the surface, the alpha is too high. Pull it back.

### Hue composition

A palette is **three colours**, in fixed slots:

- **Slot 0 — the pink anchor.** The Intentface brand pink follows the user through every route, so the site keeps a single recognisable signature. The exact RGB differs by theme: light uses the canonical `#f7aef8` (`[247, 174, 248]`); dark uses a warmer rose `[220, 130, 175]` because the literal candy pink washes out on near-black. One page swaps the anchor for its subject's own hue: the McDonald's Magic Stories entry (gold). It licences no second.
- **Slot 1 + slot 2 — per-route hues.** These carry the variation between routes. They must **harmonise with pink** — stay in the violet/rose/cream family, or use the canonical brand Sky+Cream pairing. Blue earns its keep on `/services`, `/partnerships` and `/labs`, where pink + sky is the documented brand pairing, and on `/latch`, whose atmosphere is the product mark's own Sky → Pink → Cream gradient opened out to page scale. Everywhere else, cyan and pink fight each other; do not mix them.

Do not introduce a fourth blob. The three-blob composition is the brand silhouette — a fourth point makes the cluster read as decoration, not atmosphere.

### Layout, motion, and lifecycle

The blob layout is **fixed across every route**. Positions, sizes, and drift speeds are constant; only the hues and the background tint change between routes. This means route transitions cross-fade colour and nothing else — the gradient reads as a tonal shift, not a layout reshuffle.

- **Cluster position.** Upper-right quadrant (roughly `x ∈ [0.6, 0.95]`, `y ∈ [0.12, 0.22]`), so the bloom sits as a focal glow behind the headline, not a full-viewport wash. The lower-left stays dark/quiet — that's where the content lives.
- **Drift speeds.** Slow (`speedX/Y ∈ [0.3, 0.65]`). Anything faster reads as a screensaver. The site is selling intent, not motion.
- **Frame rate.** Capped at ~30fps. At blob scale this is indistinguishable from 60fps and halves CPU on long pages.
- **Cross-fade.** 700ms `easeOutCubic` on route or theme change, with the snapshot of the currently-rendered palette as the "from" state so the lerp starts from where the eye actually sees the gradient.
- **Scroll fade.** Full opacity at the top of the page, ~0.2 mid-scroll, full again near the bottom. The gradient frames the hero and the footer CTA, then gets out of the way of the prose.
- **Pause when invisible.** RAF early-returns while the tab is hidden or the scroll opacity is below 0.05.
- **`prefers-reduced-motion`.** Paint a single static frame; palette swaps snap instead of lerping.

### Where the gradient is NOT allowed

- Not as a button background. Not as a card fill. Not behind text that needs to be read at length.
- Not as a hard linear/conic edge. If you can see where the gradient stops, it's broken — the falloff isn't doing its job.
- Not at full saturation as a flat color. `filter: saturate(1.3)` is applied once at the canvas level to compensate for the blur — do not double-saturate by also picking max-saturation hues in the palette.
- On the website, preserve the living grain and drift. The brand tools can export static PNGs for social posts and decks, or animated MP4s when motion is appropriate.

### Adding a per-route palette

To give a route or a client case study its own atmosphere, add an entry to [src/lib/gradient-palettes.ts](src/lib/gradient-palettes.ts) via the `pair("name", { bg, alpha, hues }, { bg, alpha, hues })` helper. Provide three colours per theme — slot 0 is filled in for you by the anchor. Layout is already taken care of by the fixed `SLOTS` table. Resist the urge to override positions or speeds; the fixed layout is what makes route cross-fades feel cohesive.

## Shapes

Two corner radii are used in interactive elements:

- **Pill** (`rounded.full`, 9999px): Buttons, tag pills, ghost buttons.
- **Card radius** (`rounded.lg`, 16px): Cards, panels, image frames inside content blocks.

`rounded.sm` (4px) is reserved for inputs and small inline elements. `rounded.xl` (24px) is reserved for hero-scale containers if ever needed.

The shape language is binary: pill for interactive atoms, soft rectangle for containers. Do not mix.

## Component decision tree

Before adding a new primitive, check whether the result composes from what exists:

| You need… | Use |
|---|---|
| Boxed block with a heading | `Card` |
| Page-top H1 + supporting line + CTAs | `Hero` |
| Two- or three-column MDX layout | `Row` + `Col` |
| Footnote-style aside | `Note` |
| Long-form MDX prose styling | `Prose` |
| Small mono label above a section | `SectionLabel` |
| Filter / taxonomy chip | `TagPill` |
| Pill-shaped CTA | `Button` (`primary` / `secondary` / `ghost`) |

Add a new primitive only when the same composition appears in 3+ places. See [src/components/AGENTS.md](src/components/AGENTS.md) for naming + structure conventions.

## Components

### Buttons

Pill-shaped, padded `12px × 24px`. Label uses Intent Sans Regular or Plex Mono; never Intent Sans Medium/Semibold inside a button (that weight is reserved for display). Trailing arrow glyph (`→`) on forward-action labels: "Contact us →", "Book an Intentface Sprint →", "View our projects →".

- **Primary** (`button-primary`): Filled with `primary` color, inverted text. Used for the headline CTA per page.
- **Secondary** (`button-secondary`): Transparent fill, 1px border in `border` color. Sits next to primary for ancillary actions ("About us", "Our services").
- **Ghost pill** (`button-ghost-pill`): Light, slightly translucent fill on dark backgrounds for "View case" labels on project cards.

### Cards

Translucent, not solid. **Use the [`Card`](src/components/ui/card.tsx) primitive — don't hand-roll the class string.** Every card in the site derives from the same surface recipe; if you find yourself writing `rounded-2xl bg-(--on-surface)/4 …` inline, you're rebuilding `Card`.

- Background: `on-surface` at ~4% alpha (`bg-(--on-surface)/4`). Hover lifts to ~6% — never higher (8% reads as a chip, not a surface).
- **No border.** The card is a translucent overlay on the gradient; a hairline border boxes it in and kills the bleed. The `border` token is for *content dividers between sections* and for the secondary-button outline — not for surfaces.
- Radius: `rounded-2xl` (16px). Hero CTA blocks use the same radius; only padding scales (`p-8` via `emphasis`).
- Padding: `spacing.lg` (24px) minimum; `spacing.xl` (32px) for hero CTAs.
- No drop shadows. Elevation is tonal — adjust the alpha, not the shadow.

#### Card content rules

- **No eyebrow above the title.** The page-eyebrow ban (see *Hero composition*) applies inside cards too. A small mono `FULL-TIME · REMOTE` line above the role title reads as filler and shrinks the headline. Put metadata *after* the title and body, at `text-sm text-(--on-surface)/55` — that's where supporting facts live.
- **Title is the first element.** `font-display`, regular weight, `text-xl` minimum, `text-2xl` when the card is the primary read on the section (role listings, project tiles, partnership cards). Avoid `font-medium`/`font-semibold` on card titles — display weight is reserved for the hero headline.
- **Body uses `text-base` (16px), not `text-sm`.** Card descriptions are read at length; `text-sm` (14px) makes the card feel like a tooltip. Reserve `text-sm` for metadata (date, category, attribution) and tag pills.
- **Three blocks max per card.** Title, body, metadata. If you reach for a fourth, you're rebuilding a page section inside a card — split it out.

Used for testimonials, project entries, role listings, the "Get started with Intentface Sprint" CTA block.

### Tag pills

Fully transparent fill, 1px border, `mono-sm` Plex Mono, uppercase. Small (~12px text, ~4px vertical / 10px horizontal padding). Used for project taxonomy: `IMAGE GENERATION`, `LLMS`, `PRODUCT DESIGN`, `AUTOMATION`, `RAG`, `MEDICAL`.

### Section labels

`label-sm` in Plex Mono, uppercase, letter-spaced, `on-surface-muted` color. No background, no border. Pure text. Sits above the section title or above a block of content.

### Header

Transparent fill, **no bottom border**. The header is positioned in-flow (not fixed), so the gradient behind the hero passes through it as one continuous atmosphere — a divider under the chrome would visually amputate the gradient and is off-brand. Logo left, primary nav center-right, theme toggle far right.

### Dividers

Hairline borders are reserved for *content structure inside a page* — between major sections that genuinely need separation, around translucent surfaces (cards, panels), and on the secondary button outline. Use them sparingly: a divider every screen-height kills the airy feel. Color is `border` (white-at-8% on dark, black-at-10% on light) — never a solid mid-gray line. The chrome (header, footer chrome) does not get bordered.

### Logo

The logo is an inline SVG React component at [src/components/ui/intentface-logo.tsx](src/components/ui/intentface-logo.tsx), not a static asset file. It contains the `intentface` wordmark and the iridescent asterisk together as `<path>` elements within a single `viewBox="0 0 622 137"`.

- Wordmark paths use `fill="currentColor"` so the logo inherits the surrounding text color (`text-(--on-surface)`) and inverts automatically with the theme.
- Default render width in the navbar is `width={108}`; height scales proportionally from the viewBox aspect ratio.
- Do not recreate the logo in HTML/CSS by typesetting the wordmark in Intent Sans and appending a separate asterisk. The `IntentfaceLogo` component is the source of truth.
- The asterisk inside the SVG is the only place where the brand gradient is foregrounded. Its stops read `--asterisk-stop-{1,2,3}` with the triad as fallback so a product route can retint it — `/os` does, nothing else should.

### Latch avatar

The Latch product mark lives at [src/components/latch/latch-avatar.tsx](src/components/latch/latch-avatar.tsx) as two exports: `LatchMark` (the bare glyph, for illustrations and diagram nodes) and `LatchAvatar` (the glyph seated in an app-icon tile). Like the logo, it is an inline SVG component, not a static asset.

- The gradient is the mark's canonical paint. Pass `fill` to override it with any CSS paint (`currentColor`, a token, a hex) when the mark has to sit inside monochrome artwork.
- The mark is a face: `state="idle"` blinks the two eyes every few seconds off `@keyframes latch-blink` in [app/globals.css](app/globals.css); `state="working"` treats the empty square as the face: it slides round the room one side at a time so the bracket stays an L, the eyes ride inside it, and the mark stands in for a spinner while an agent runs (the corner is square in this state so the walls can slide as plain rects); `state="still"` freezes it. Motion is CSS and state is a prop, so the component stays server-rendered and the caller owns the timing. New states are a union member plus one `[data-state="…"]` block — not a new component.
- The tile is the documented exception to the card rules, because an icon is artwork rather than a surface. Its radius is `22.5%` rather than `rounded-2xl` so the silhouette holds from 32px to 256px (at the default 64px it lands on 14.4px, i.e. card radius anyway); its ground is a fixed dark that does **not** invert with the theme, since the pastel mark has no contrast on a light tile — translucent (~72%) with a backdrop blur, so the route gradient still passes through and it never reads as a flat hole; the brand hues pool in the corners and leave the middle clean; and its edge is a 1px gradient light falling from the top-left, not a uniform border. The recipe is `.latch-tile` in [app/globals.css](app/globals.css), with `.latch-tile-lg` for the hero's responsive scale. None of these licences extend to cards.

### Loredex mark and OS mark

Two marks, one geometry, in [src/lib/loredex-geometry.ts](src/lib/loredex-geometry.ts).

- **Loredex** — [src/components/os/loredex-mark.tsx](src/components/os/loredex-mark.tsx), `LoredexMark`. An open book drawn in strokes: spine, front cover, three fanned pages. It is a state machine (`still`, `thinking`, `writing`, `adding`, `success`, `closed`) and parametric: the geometry draws every frame, and the opening angle is a parameter because the OS ring needs books opened wider than 90°. `LoredexGlyph` is the book as a bare `<g>` for composing; `useLoredexMark().flash()` plays a one-shot state and returns.
- **Intentface OS** — [src/components/os/os-avatar.tsx](src/components/os/os-avatar.tsx), `OsMark` and `OsAvatar` (the mark in the shared app-icon tile, `.os-tile`). A ring of Loredex books with their hinge corners on the vertices of a hexagon and their pages fanning inward, so the covers *are* the hexagon. Six books is the logo; `books` takes 3–8 for illustration, and every count keeps one footprint. In a moving state the ring runs a beat: it turns one vertex, eased (`spin`), and as the turn lands a random book takes up the motion for four or five cycles while the rest sit in the fan (`solo`, the default; `solo={false}` animates every book, offset round the ring); then a rest, then the next turn. Idle flows into `thinking` without a crossfade — the fanned pages themselves start to fall, and settle back into the fan when the state ends. In diagrams the mark stands alone as the node, no label. The `/os` hero uses `thinking`; the `/os` OG images render the resting ring from the same helpers. Everywhere else, including the knowledge-work and for-agents back-links and any illustration carrying several marks, use `still`.
- **Paint.** Peach → pink → yellow (`OS_GRADIENT_STOPS`; the pink is the brand pink, a dash of it mid-mark), warm on purpose: this is the workspace, not the harness. The ring paints its books through a mask so one gradient spans the whole ring however the books are turned, and the gradient itself sweeps slowly through the shape in every state (`sweep`, one turn per 14 s) — the logo is alive even when the books are still. The family is `OS_HUES` (peach, pink, yellow) and under `/os` it is the whole route's accent: the mesh palette runs peach and cream either side of the pink anchor, the `.os-tile` corners pool the three hues, and [app/os/layout.tsx](app/os/layout.tsx) retints the header wordmark's asterisk through the `--asterisk-stop-{1,2,3}` variables `IntentfaceLogo` exposes. That layout is the only place those variables are set; leaving `/os` restores the brand triad. Pass `color` to override either mark's paint with any CSS paint when it has to sit inside monochrome artwork.
- **Motion.** Client components, unlike `LatchMark`: the hinge angle is tweened in JavaScript because the spine path and the page states both derive from it. Everything else is CSS, generated from the geometry and hoisted into `<head>` once. Reduced motion collapses every state to the resting fan in CSS, so the server render is already the right picture.

### Illustration components

Figures built as components — [`src/components/os/`](src/components/os/), [`src/components/latch/`](src/components/latch/), [`src/components/interactive/`](src/components/interactive/) — are pictures, not documentation.

**Avoid text.** Default to shape. A reader recognises a phone, a chat, a file list or a folder tree without reading a word, and every label inside a figure competes with the paragraph beside it. The card's own title and body already carry the argument, so a figure that explains itself says it twice. Use real words only where the words *are* the subject: the one filename the story lands on, the tool a model called, a badge that exists only because it is the point.

- **Don't caption the surface.** No "AI client", "MCP", "Dashboard" chrome labels — the card title already says which surface this is.
- **Don't invent UI we don't ship.** A figure standing in for someone else's product shows only what that product actually shows. No MCP client displays our trust badges, so an MCP figure doesn't either.
- **One scenario per figure.** Two cards in a row must not stage the same example with different chrome; if the Slack card asks about the discount policy, the MCP card asks something else.
- **Read [`src/components/demo/AGENTS.md`](src/components/demo/AGENTS.md) first.** It is the primitive-level version of this rule and goes further on mono, alpha, and reserved height.

### Theme toggle

Soft-rectangle icon button (`rounded-md`, 8px radius) in the top-right of the header. Sun glyph in dark mode (click to go light), moon glyph in light mode (click to go dark). Borderless with a hover background tint.

### Hero composition

Display headline left-aligned, 2-3 lines maximum, in Intent Sans Regular (not Medium — the thin display cut). When the headline carries two beats, render the second as a muted continuation on the same H1 at ~50% opacity. Optional subtitle directly beneath in body Regular. CTAs immediately below (primary + secondary side by side). Soft iridescent gradient blurred behind the headline.

**Avoid eyebrows.** Don't stack a small mono label ("WRITING", "PEOPLE", etc.) above the H1 — the headline alone almost always carries the page, and a generic eyebrow reads as AI-website filler. The exception is when the eyebrow earns its keep by stating something the headline genuinely can't (e.g. the podcast page's `A PODCAST BY INTENTFACE` situates the show inside the company — the headline is a tagline, not a page label). If you reach for an eyebrow, justify it; otherwise drop it.

#### Centred heroes — the exceptions

Posters rather than marketing pages: the podcast, and the product landing pages. Each centres its hero. None of them licences another: articles, case studies, services, work, and contact all use the standard left-aligned, sentence-case hero.

- **Podcast.** The show title is centered, all-caps, in Intent Sans at display scale, with an eyebrow ("A PODCAST BY INTENTFACE") above it. The one place in the site where centered *and* uppercase is on-brand.
- **`/latch` and `/os`.** Icon, name, one supporting line, two buttons, centered — the composition an app page has. Sentence case, no eyebrow: the app icon does what an eyebrow would have, and does it better. The two product marks share one tile recipe (`.latch-tile` / `.os-tile`) so they read as a set.

### Testimonial carousel

Three cards visible at desktop width, one or two on smaller screens. Outline circular arrow buttons on the sides for navigation. Quote in Intent Sans Regular, attribution below in Plex Mono muted. Client logo top-left of each card. Cards are translucent like all other cards.

## Do's and Don'ts

- Do design dark-first. Light mode is a careful adaptation, not the starting point.
- Do use translucent overlays for every surface. The gradient must pass through.
- Do use Plex Mono for the hero subtitle. It is a signature.
- Do treat the brand gradient as atmosphere: behind hero content, inside the asterisk, nowhere else.
- Do use the trailing arrow glyph (`→`) for forward actions.
- Do keep elevation tonal: hairline borders plus translucent overlays. No shadows.

- Don't use solid hex fills for surfaces. `bg-zinc-900` or any flat surface color kills the gradient and is off-brand.
- Don't use Intent Sans Medium or Semibold for body copy. Medium/Semibold are display weights. Body is Intent Sans Regular.
- Don't introduce bright pure-color buttons or backgrounds. The palette is grayscale plus iridescent accent.
- Don't use emoji or decorative icons in UI labels.
- Don't join bits of text with a " · " separator ("Choice · Which team", "2.11 · Frustrated"). It's lazy. Give each piece its own place in the layout (a title and a right-aligned tag, two lines, a column) or write a sentence.
- Don't mix square and pill shapes in interactive elements. Pills for buttons and tags, 16px for cards, nothing in between.
- Don't introduce a third typeface. The two-family system (Intent Sans + Plex Mono) is the brand.
- Don't typeset the logo in HTML. Use the logo asset.
- Don't render the gradient as a flat color anywhere. If it stops being soft and diffuse, it stops being on-brand.
- Don't add `box-shadow` for elevation. Increase border contrast or surface alpha instead.
- Don't put a border on a card. Surfaces are translucent fills; the hairline `border` token is for content dividers and secondary buttons only.
- Don't put an eyebrow above a card title. Same rule as the page-headline eyebrow ban — metadata goes after the title, not before, and never in uppercase mono at the top.
- Don't use `text-sm` (14px) for card body copy. Body reads at `text-base` (16px); `text-sm` is for metadata, attribution, and tag pills.
- Don't hand-roll a card by writing `rounded-2xl bg-(--on-surface)/4 …` inline. Use the [`Card`](src/components/ui/card.tsx) primitive so the recipe stays consistent.
- Don't reach for a mono "eyebrow" label above a page or section headline by default. Eyebrows ("WRITING", "ABOUT US", etc.) above the H1 are a well-worn AI-website tell — they read as filler and add a fontsize without earning one. Let the headline carry the page. Section labels are allowed *between* major sections within a page (small mono SectionLabel above mid-page collections). The podcast hero is the documented exception (see *Podcast hero — special case*) — when in doubt, no eyebrow.

## Public brand kit

The brand page at `/brand` provides Intentface, Latch, and Intentface OS logos as transparent SVG and PNG files in original, white, and black variants. The complete Intentface logo is primary and preferred. Never provide or use a standalone wordmark. The standalone asterisk has its own card and includes its horizontal line, white on dark backgrounds and black on light backgrounds. It may only be used when Intentface is named elsewhere in the same context. Original-color downloads include separate dark- and light-background variants. `/brand/assets.json` lists direct asset URLs; `/brand.md` provides the readable guidelines; `/brand/DESIGN.md` downloads this complete source document. `/llms.txt` links to these materials.

The post composer at `/brand/ad-maker` frames uploaded screenshots or a short headline in the brand typography and atmosphere. It provides LinkedIn/X, Instagram square and portrait, and story presets, with PNG and content-paced seamless-loop MP4 export. Images stay in the browser. Drop one PNG, JPG, or WebP directly onto the preview to add or replace the image, or use the file chooser. The same 15 MB and 24-megapixel limits apply to both. Gradient studio at `/brand/gradient-studio` shares the site's V2 renderer with the composer. Exported artwork starts with broader light fields. The composer offers palette, individual color, composition, spread, and intensity controls. Its optional footer uses Intent Sans. The typeface page at `/brand/intent-sans` is the public specimen for Intent Sans: live axis playground, character set, number features, and the variable and static font downloads with their OFL license.

The brand kit previews Latch and Intentface OS with state buttons using the downloadable React components. The selected color variant applies to the preview; state selection previews React motion, while SVG and PNG downloads remain still artwork.

React / TSX downloads for Intentface, Latch, and Intentface OS are generated from the canonical components by `scripts/brand-react.cjs`. Each per-brand ZIP includes its required source files, embedded animation styles, and a usage guide. `/brand/intentface-react-logos.zip` contains all three; the full brand kit and individual logo kits also include the sources. Requires React 19+ and TypeScript, with no Next.js, Tailwind, or site stylesheet dependency. Components support original, white, and black variants, sizing, className, and accessible labels. Latch and Intentface OS retain their animation states with local reduced-motion guards. Inline SVG gradient and mask IDs are unique per instance. `/brand/assets.json` lists the ZIPs and individual source files.

The portable copywriting skill is at `/brand/skills/intentface-copy/SKILL.md`, with a ZIP at `/brand/intentface-copy-skill.zip`. It guides writing from supplied first-hand material and does not authorize publishing.

Brand editors use a neutral page background. Exported gradients omit the site background's edge ribbons. Post headlines align to the bottom. Landscape image posts use a large screenshot on the right and copy on the left; square and portrait posts place the screenshot above the copy. Image framing offers full-image fit or a deliberate edge crop. The footer is optional and empty by default. Both editors provide loop playback and desktop controls that can be floated, dragged, collapsed, or docked.


### Post formats and motion

The composer offers 1080×1350 portrait feed, 1080×1080 square, 1200×628 link card, 1280×720 landscape, 1080×1920 vertical, and 1080×1440 Instagram photo masters. These are output presets, not a guarantee against platform cropping. Feed margins are editorial. Vertical presets reserve conservative Reels, organic Stories, or centered-square bounds. Logo, headline and screenshot layout use the selected bounds; guides appear only in the editing preview.

Video effects are selectable: podcast glitch (default), signal tear, staggered rise, soft fade, or none. Podcast glitch decodes block glyphs into a headline with red/blue chromatic ghosts and brief scramble bursts. Signal tear adds displaced horizontal slices and wider color separation. Character positions follow the resolved headline so scrambling cannot change line breaks. Preview and MP4 share deterministic effects and content-paced duration: a five-second minimum, with two seconds reserved for transitions plus the larger of word count / 3 or non-space character count / 15, rounded up. Headline and footer both count. The gradient closes over the same duration. Fade and rise sample the transitions-dev texts-reveal recipe in canvas: a 600 ms cubic-bezier(0.22, 1, 0.36, 1) entrance from 3 px blur, 40 ms line stagger and 12 px rise for the rise effect, then a separate 200 ms ease fade-out in place. Distances scale from the 1080 px master. Animated text meets at zero opacity at the loop boundary; None keeps text visible. Selecting an effect starts playback unless reduced motion is requested. PNG exports keep all text visible. Safe-zone guides never enter exported pixels.
