# Pulsier Labs — design system v0.1

The identity system every Pulsier Labs product inherits: a pulse that reads as data, a page that reads as an instrument, and a voice with nothing to hide behind.

Tokens live in `src/app/globals.css`. This document is the rulebook; the CSS is the implementation. When they disagree, fix the CSS.

---

## 1. Principle

**One pulse, many instruments.**

A pulse is a small, regular, living signal against a still background. The system spends almost everything on stillness — near-black surfaces, quiet greys, generous space — so that a single accent and a single gesture carry all the emphasis.

Three states of the same idea recur throughout: **emit** (the ring), **travel** (the wave), **measure** (the bars). A product in the portfolio picks one and stays with it.

---

## 2. Color

Two surfaces only: **Ink** for the studio, **Paper** for documents and long reading.

| Token | Value | Use |
|---|---|---|
| `--ink-deep` | `oklch(0.11 0.012 250)` | Page ground |
| `--ink` | `oklch(0.16 0.012 250)` | Primary surface |
| `--ink-raise` | `oklch(0.22 0.012 250)` | Cards, wells |
| `--hairline` | `oklch(0.32 0.01 250)` | Rules, borders |
| `--signal` | `oklch(0.74 0.16 155)` | Accent, live state |
| `--drift` | `oklch(0.74 0.16 275)` | Second accent, spin-offs |
| `--mute` | `oklch(0.62 0.01 250)` | Labels, meta |
| `--paper` | `oklch(0.97 0.006 85)` | Documents, light surface |
| `--bright` | `oklch(0.95 0.006 85)` | Body text on Ink |

Available as Tailwind utilities: `bg-ink`, `text-signal`, `border-hairline`, and so on.

### Rules

- **Signal is never a background for text, and never fills more than 5% of a viewport.** It marks one thing. If two things on a screen are Signal green, one of them is wrong.
- Focus rings are the single exception — they use Signal without counting against the budget, because a focus ring is never decoration.
- **Drift** is reserved for spin-offs and second-order states. Do not reach for it to add variety.
- On Paper, Signal darkens to `oklch(0.52 0.16 155)` and passes at 15px and above for links and marks — never for body text.

Adding a color is a three-step edit, all required: declare it in `:root`, add its Paper counterpart in `.paper`, then expose it in `@theme inline` as `--color-<name>`. Skip step two and it breaks on the Paper surface; skip step three and Tailwind cannot see it.

---

## 3. Typography

**Archivo** for everything structural, **IBM Plex Mono** for labels and data. Both self-hosted via `next/font`.

| Role | Font | Size | Tracking |
|---|---|---|---|
| Display | Archivo 650 | 76–148px | −0.03em |
| Statement | Archivo 600 | 34–52px | −0.015em |
| Body | Archivo 400 | 17–20px / 1.6 | normal, max 68ch |
| Label & data | IBM Plex Mono 400–500 | 11–13px | 0.14–0.2em, uppercase |

### Rules

- Display type tightens as it grows. Large type at default tracking looks loose and amateurish.
- Body copy caps at 68 characters. Wider lines lose the reader between rows.
- Mono is for labels, data, section marks, and status — never body copy, never headlines.
- Weight does the work that color would otherwise do. Avoid `font-bold`; 650 is the ceiling.
- One `<h1>` per page. Headings descend without skipping levels.

> The font variable names must match between `layout.tsx` and `globals.css` — `--font-archivo` and `--font-plex-mono`. A mismatch silently falls back to a browser serif and nothing errors. This has already happened once.

---

## 4. Motion

Three durations, three curves. Nothing else.

| Name | Duration | Curve | For |
|---|---|---|---|
| **Emit** | 2600ms | `cubic-bezier(0.16, 1, 0.3, 1)` | Rings, reveals, page entrances |
| **Respond** | 180ms | `cubic-bezier(0.2, 0, 0, 1)` | Hover, focus, magnetic pull |
| **Carry** | 720ms | `cubic-bezier(0.7, 0, 0.2, 1)` | Section transitions, pinning |

Exposed as `ease-emit`, `ease-respond`, `ease-carry`.

### The logo cycle

One 4s cycle, three consequences **in order**: the arcs fire outward, the apex lands and `pulsier` swells, then the charge leaves through `labs`. Nothing moves at the same time as anything else. Simultaneous motion is the failure mode — it reads as a loading screen rather than a signal.

### Rules

- Never animate `width`, `height`, `top`, or `left`. Transform and opacity only.
- One moving accent per viewport.
- `prefers-reduced-motion: reduce` is honoured globally. Canvas fields draw a single static frame and stop; they become texture, not animation. Any new motion must survive that rule and must never be the only way information is conveyed.

---

## 5. The fields

The signature backgrounds. Each is one dpr-corrected canvas, paused while off-screen, with the pointer position smoothed rather than sampled raw.

- **PulseField** — a lattice of points that leans toward the pointer and carries a ring outward from every click. Hero and closing sections.
- **ScopeField** — a single trace that bends toward the cursor and snaps back. Section dividers and backdrops for data. **Never behind body copy.**
- **ResolveField** — noise that resolves into language near the cursor. The house metaphor, literally: unstructured input becoming a readable answer wherever attention lands.

All three take a `tint` and share `useStage` (`src/components/fields/use-stage.ts`). Points only take on Signal where the pointer is actually pulling them — elsewhere they stay `--mute`, which is what keeps the accent budget intact.

---

## 6. The marks

**The wordmark** — `pulsier` in Bright, `labs` in Mute. Animated form adds the breath and the wave; static form is for print, favicons, embeds, and email.

**The emitter** — a dot with arcs radiating from it. Three arcs at 62px, two at 22px, and below 20px use the ring-only `PulseDot`, where arcs would collapse into a smudge.

### Rules

- Clearspace equals one dot diameter on all sides.
- Minimum width 88px for the wordmark.
- Never recolor the dot. Never outline the wordmark.
- Below 24px the wave stops and only the breath remains, so the mark stays legible in a nav bar.

---

## 7. Components

- Radius base is `0.25rem` — the system is square-ish and instrument-like. Pills (`rounded-full`) are reserved for actions and status chips.
- Cards are `--ink-raise` on `--ink`, with a `--hairline` border at 60–80% opacity. Hover lifts the border toward Signal, not the background.
- Status chips: **Live** is Signal with a beating dot; **In the works** is a plain hairline outline; **Research** is a dashed outline; **Spin-off** uses Drift.
- Generate shadcn/ui components with the CLI and treat `src/components/ui/` as generated code. The token bridge in `globals.css` keeps their output on-brand.

---

## 8. Voice

| Rule | Do | Not |
|---|---|---|
| Say the mechanism. | "It answers from your programme only — it never invents a room." | "Powered by cutting-edge AI to delight your users." |
| Own the limit. | "Say plainly which questions your content could not answer." | "Industry-leading accuracy." |
| One idea per line. | "Point it at your content. Share one link." | "A holistic, end-to-end platform for the modern enterprise." |
| Specifics, not adjectives. | "Name what came in, what was missed, and how long the fix took." | "Massive engagement across the board." |

> **On numbers.** The voice rewards specifics — which makes it dangerous. Any figure published on this site has to be one the studio can stand behind and show its working for. Until then, name the mechanism instead of quantifying it. Illustrative metrics in a design file are not a licence to publish them.

---

## 9. Accessibility

Not a polish pass:

- Contrast 4.5:1 for body text, 3:1 for large text and UI boundaries. `--mute` on `--ink-deep` passes for labels; `--mute` on `--ink-raise` is marginal — don't stack them for anything a reader must actually read.
- Focus is never removed, only restyled. The global `:focus-visible` rule uses a Signal outline with 3px offset.
- Canvas fields are `aria-hidden` and purely decorative. Nothing in them is content.
- Decorative marks and icons beside text labels get `aria-hidden`.
- Touch targets at least 44×44px.
- Real semantics: `<button>` for actions, `<a>` for navigation.
