# Obelism — Design & Brand Guide

> A machine-readable reference for the Obelism brand: who we are, how we
> write, and the exact design tokens (color, type, space, motion) our
> products are built on. Point an LLM or coding agent at this file and it
> should be able to produce work that looks and sounds like Obelism without
> guessing.

Obelism is a software studio (`obelism.studio`). We build
tools that help out — software focused on what people are actually trying to
get done, with a flow that enhances the task instead of complicating it.
Driven by perfection.

This guide describes the shared design system that all Obelism surfaces use —
the studio site and products like **Obelism Improve** (A/B testing & feature
flags, `improve.obelism.studio`). If you're building anything under the
Obelism name, start here.

- **Studio:** Obelism — software studio
- **Contact:** improve@obelism.studio
- **Products:** Obelism Improve — `improve.obelism.studio`
- **Wordmark:** the product's name in Instrument Serif, closed by the mark in
  accent on the baseline ("Obelism Content"). In an interface it is live text
  (`Wordmark`); outlined SVGs for print, email and slides are in
  `design/identity/logo`.
- **Mark glyph:** `⁜` (the obelus-derived mark; the studio splash uses it
  as its logo). The glyph is small for its font size: at text sizes set it at
  22 px or more, or use an SVG of the mark.

The companion files:

- [`/base.css`](/base.css) — a starter stylesheet with all the tokens, reset,
  theme, and type scale below. Copy it into a project and adapt.
- [`/components.md`](/components.md) — how we build components on top of those
  tokens (HTML-first, token-driven, the motion/animation style).
- [`/design/screens.md`](/design/screens.md) — how screens are put together:
  the templates, which element to pick, where actions go, and the checks
  before shipping one.

---

## Using this system across projects

There is no npm package and no build step to adopt — this is a **fork-and-adapt
base**, not a locked dependency. That keeps every project free to diverge where
it needs to while still speaking the same language.

1. **Take `/base.css`** into the project (copy the file, or vendor it). It
   defines every token this guide describes.
2. **Keep the token names.** Change _values_ freely (a project can retint,
   add tokens, drop what it doesn't use), but keep names like
   `--color-orange-rgb`, `--theme-accent-rgb`, `--border-radius-pill`,
   `--ease-roll`. Portable names mean this guide still documents your project
   and an agent can move between repos without relearning.
3. **Build with the semantic `--theme-*` tokens**, not raw palette values, so
   components theme themselves in light and dark for free.
4. **Follow `/components.md`** for component shape and motion.
5. **When a project improves a pattern**, fold it back here so the next project
   inherits it. This trio (`design.md` + `base.css` + `components.md`) is the
   shared source of truth; the product repos are downstream.

Canonical token source in code today: `@obelism/ui` →
`packages/ui/src/foundation/**`. `base.css` is the distilled, dependency-free
version of that.

---

## Principles

Four ideas drive every decision. When a design or a sentence is in doubt,
these break the tie.

1. **Help, don't complicate.** Our goal is to help, not make things more
   complicated. Cut steps, cut jargon, cut chrome. If a feature doesn't move
   the user toward what they came to do, it's a candidate for removal.
2. **Focus on the task.** Design the flow around what the user needs to
   accomplish. One product, one job, done well beats a suite that does
   everything adequately.
3. **Driven by perfection.** Craft and attention to detail are the point.
   Software that isn't made with care gets in the way of its own purpose.
4. **Show the numbers, keep it light.** Back claims with data and clear
   visualisations, and ship the smallest, fastest thing that does it
   (the Improve client SDK is 2.1 kB min+gz — that's the bar).

---

## Voice & tone

Obelism sounds like a sharp, friendly engineer explaining something they
genuinely care about — plain-spoken, confident, a little playful, never
corporate.

**Do**

- Write short, direct sentences. Lead with the benefit, then the mechanism.
- Be warm and human. Contractions are good. A wink is fine
  ("But wait, there is more", "But what about …", "We know it's important").
- Be concrete and honest about limits ("currently in closed beta",
  "there is a no-code plugin in beta").
- Use plain verbs: _help, ship, test, track, release, measure._
- Address the reader as "you"; refer to the studio as "we".

**Don't**

- Reach for hype or buzzwords ("revolutionary", "synergy", "next-gen",
  "10x"). Let the work be the claim.
- Pad sentences with filler or hedging. If a word can go, it goes.
- Over-explain. Trust the reader.
- Be cold or bureaucratic.

**Reference copy** (the house style, verbatim from shipping surfaces)

- Hero: _"Improve your platform today."_ /
  _"Obelism Improve provides streamlined AB testing and feature flags.
  Giving you the tools to effortlessly upgrade your platform based on
  numbers."_
- Value: _"Improve gives you the toolkit focussed on optimizing your website.
  AB-tests to get hard data on what works for your customers and Feature flags
  for no sweat continuous delivery of major features."_
- Studio: _"Software is eating the world, but not all software is created
  equal. When it's not crafted with care and attention it can be cumbersome
  and get in the way of its purpose."_

**Mechanics**

- Sentence case for headings and buttons ("Start testing", not "Start
  Testing"). The one exception is the wordmark and product names
  ("Obelism Improve").
- Emoji are used sparingly, one per feature card as a friendly marker
  (🕺 📡 🚠 🔒 📊 🪶). Don't scatter them through prose.
- Numbers and units stay tight and specific ("2.1 kB", "three environments").

---

## Color

A white workspace floats on warm **paper**; text is **ink**. **Orange** is
Obelism itself: the mark, AI actions, send, selection rings and counts. The
primary action is ink, not orange. Five pastel **tints** sort the product by
place. Data visualisation uses a separate, colorblind-checked series palette
(below).

Every color is an RGB triple custom property (`--x-rgb`) so it can be used at
any opacity via `rgb(var(--x-rgb) / <alpha>)`. Build with the semantic
`--theme-*` tokens: they remap between light and dark
(`prefers-color-scheme`).

### Surfaces, text and lines

| Token                           | Light       | Dark        | Use                                                       |
| ------------------------------- | ----------- | ----------- | --------------------------------------------------------- |
| `--theme-background-main-rgb`   | 242 240 236 | 17 16 15    | Paper: behind everything                                  |
| `--theme-background-panel-rgb`  | 255 255 255 | 21 20 18    | The workspace panel                                       |
| `--theme-background-alt-rgb`    | 255 255 255 | 27 26 24    | Cards, tables, dialogs and menus on the panel             |
| `--theme-background-subtle-rgb` | 250 249 247 | 33 31 29    | Table heads, drawers, quiet blocks in a card              |
| `--theme-background-wash`       | ink at 6%   | ink at 6%   | A step above any surface: placeholders, quiet chips       |
| `--theme-background-actions`    | 232 229 223 | 45 43 40    | Behind controls: segmented tracks, search pills           |
| `--theme-foreground-rgb`        | 23 22 20    | 242 240 236 | Ink: text and the primary button                          |
| `--theme-foreground-soft-rgb`   | 68 64 60    | 214 210 204 | Navigation, secondary text                                |
| `--theme-foreground-muted-rgb`  | 105 100 94  | 160 154 146 | Captions, meta, placeholders (4.5:1 everywhere)           |
| `--theme-hairline-rgb`          | 236 233 228 | 44 42 39    | Dividers and card outlines (decoration only)              |
| `--theme-border-soft-rgb`       | 220 216 209 | 64 61 57    | Outlines of labelled controls                             |
| `--theme-border-rgb`            | 138 132 124 | 112 108 102 | Boundaries that must be seen: inputs (3:1)                |
| `--theme-accent-rgb`            | 212 130 30  | 212 130 30  | Obelism orange                                            |
| `--theme-on-accent-rgb`         | 23 22 20    | 23 22 20    | Text and icons on orange (6:1; never white, 3:1)          |
| `--theme-focus-rgb`             | 176 98 0    | 212 130 30  | The focus ring (3:1)                                      |
| `--theme-link-rgb`              | 0 84 146    | 142 185 240 | Links in running text                                     |
| `--theme-code-rgb`              | 31 29 27    | 31 29 27    | Code blocks (text 242 240 236; `--code-*-rgb` for syntax) |

### Feedback

Each is at least 4.5:1 on every surface, so it can be text. Never the only
signal: pair it with a word or an icon.

| Token                  | Light     | Dark        |
| ---------------------- | --------- | ----------- |
| `--theme-positive-rgb` | 30 112 45 | 125 190 122 |
| `--theme-negative-rgb` | 184 46 46 | 240 110 110 |
| `--theme-warning-rgb`  | 150 88 0  | 230 160 60  |
| `--theme-info-rgb`     | 0 84 146  | 142 185 240 |

### Tints

A tint means a place: coral (home, changes, AI, watch out), butter (canvas,
drafts, loading), lilac (content model, structure), sky (content, promote,
info), mint (media, environments). Each has a fill (tiles, chips), a wash
(large panels) and an ink for text and icons on either. Tints replace emoji:
the interface has none.

| Tint   | Fill (`--tint-*-rgb`) | Wash (`-wash-rgb`) | Ink (`-ink-rgb`) |
| ------ | --------------------- | ------------------ | ---------------- |
| coral  | 251 217 203           | 255 243 236        | 122 46 20        |
| butter | 248 233 176           | 253 248 228        | 107 78 7         |
| lilac  | 235 221 245           | 247 241 251        | 91 42 134        |
| sky    | 215 230 250           | 238 244 252        | 30 58 138        |
| mint   | 211 236 218           | 237 247 240        | 20 83 45         |

Dark mode inverts them (deep fills, light inks); the values are in
`/base.css`.

The named `--color-*` palette in `/base.css` (white, greys, black, beige,
orange, blue, red, purple, green) stays for illustration only: status takes
the feedback tokens, places take the tints.

### Environments

Dots and the live bar only: `--env-develop-rgb` (22 163 74),
`--env-staging-rgb` (217 119 6), `--env-live-rgb` (220 38 38), and
`--env-live-strong-rgb` (153 27 27) behind the white text of the bar shown
while editing live. "production" is called "live" in the interface.

### Data-viz series palette

For charts only. A fixed-order categorical palette, validated for
colorblind-safe adjacent separation and contrast on our actual backgrounds.
Assign in slot order — never cycle or reassign per filter.

| Slot | Light     | Dark      |
| ---- | --------- | --------- |
| 1    | `#2A78D6` | `#3987E5` |
| 2    | `#008300` | `#008300` |
| 3    | `#E87BA4` | `#D55181` |
| 4    | `#EDA100` | `#C98500` |
| 5    | `#1BAF7A` | `#199E70` |
| 6    | `#EB6834` | `#D95926` |
| 7    | `#4A3AA7` | `#9085E9` |
| 8    | `#E34948` | `#E66767` |

---

## Typography

Three typefaces, all SIL Open Font License and self-hosted: **Geist** for the
interface, **Geist Serif** for titles and **Geist Mono** for numbers, paths,
IDs and code. Geist Serif is for page titles, section and dialog titles and
large quotes, never for labels, buttons, tables or forms. System-font
fallbacks keep the first paint honest.

```css
--font-base:
	'Geist', ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
	Segoe UI, Roboto, Helvetica Neue, Arial, sans-serif, Apple Color Emoji,
	Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
--font-display: 'Geist Serif', 'Iowan Old Style', Georgia, serif;
--font-mono:
	'Geist Mono', ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono',
	'Roboto Mono', 'Source Code Pro', 'Courier New', monospace;
```

Headings are Geist Serif at **weight 600**, tight: the release draws one
upright weight, so that 600 is the browser's synthesized bold. Body is Geist
**400**; the interface default is 14 px. Sizes below list mobile →
`min-width: 768px` where they scale up.

| Style     | Family      | Size (mobile → ≥768px) | Weight | Line height | Tracking |
| --------- | ----------- | ---------------------- | ------ | ----------- | -------- |
| heading-1 | Geist Serif | 44 → 56 px             | 600    | 1.08        | −0.02em  |
| heading-2 | Geist Serif | 34 → 40 px             | 600    | 1.1         | −0.02em  |
| heading-3 | Geist Serif | 26 → 28 px             | 600    | 1.2         | −0.01em  |
| heading-4 | Geist Serif | 20 → 22 px             | 600    | 1.27        | 0        |
| body-1    | Geist       | 20 px                  | 400    | 1.5         | 0        |
| body-2    | Geist       | 16 px                  | 400    | 1.6         | 0        |
| body-3    | Geist       | 15 px                  | 400    | 1.73        | 0        |
| label-1   | Geist       | 14 px                  | 400    | 1.43        | 0        |
| label-2   | Geist       | 13 px                  | 400    | 1.46        | 0        |
| label-3   | Geist       | 12 px                  | 400    | 1.33        | 0        |
| eyebrow   | Geist       | 11 px, uppercase       | 600    | 1.45        | +0.08em  |
| numeric   | Geist Mono  | 32 → 36 px             | 500    | 1.1         | −0.02em  |
| code      | Geist Mono  | 13 px                  | 400    | 1.7         | 0        |

---

## Space, layout & radius

- **Content max-width:** `1260px`.
- **Grid:** 6 columns / 24 px gutter on portrait, 12 columns / 48 px gutter on
  landscape (`--grid-columns`, `--grid-gutter`).
- **Radii:** things are soft and rounded.

| Token                     | Value | Use                      |
| ------------------------- | ----- | ------------------------ |
| `--border-radius-small`   | 4 px  | Small chips              |
| `--border-radius-default` | 12 px | Inputs, default surfaces |
| `--border-radius-medium`  | 20 px | Cards                    |
| `--border-radius-large`   | 30 px | Large panels             |
| pill                      | `5em` | Buttons (fully rounded)  |

**Elevation:** resting surfaces get `--shadow-small`; lifted ones (a menu, a
popover, the prompt bar) `--shadow-medium`; floating ones (a dialog, a sheet)
`--shadow-large`. The lifted and floating shadows carry a hairline ring, so a
surface reads on paper and on white, and get more weight in dark mode.

Spacing scale is em/rem-based and generous — favor breathing room over
density. Prefer relative units (`em`, `ch`, `rem`) so type and spacing scale
together.

---

## Components

Conventions distilled from the shipping UI. Reuse these shapes rather than
inventing new ones.

**Buttons** — pills (`--border-radius-pill`), `label-1` type, at 32, 40
or 48 px high. Ink is the action, orange is Obelism:

- `primary` — ink: `--theme-foreground-rgb` fill, paper text. The main
  action of a view (Save, Publish, Start a test).
- `secondary` — white with a soft outline (`--theme-border-soft-rgb`),
  ink text. An off secondary keeps its outline, so it never reads as an
  off primary.
- `transparent` — ink text, no fill.
- `accent` — Obelism orange, for AI actions only ("Ask Obelism", send), one
  per view. Text and icons on it are ink (`--theme-on-accent-rgb`, 6:1, in
  both themes); white on orange is 3:1 and not allowed.
- `danger` — deletes or revokes, inside a confirm.
- `dashed` — a dashed outline: a place to add to a list ("Add a field").

Disabled: the actions surface with muted text, `cursor: not-allowed`.
Loading: a small ring before the label, `aria-busy`, `cursor: progress`.
Buttons have a subtle per-character "roll" hover animation — decorative,
and disabled under `prefers-reduced-motion: reduce`.

**Icon buttons** — round, a 16 px line icon, ghost until hovered (a paper
fill). Their hit area stays 44 px (`--size-target`) whatever they look
like. The accessible name is the button's label, also its tooltip. Send is
the one filled icon button: an ink arrow on an orange circle.

**Icons** — line icons on a 16 px grid, 1.5 stroke, round caps,
`currentColor`. No emoji in the interface: a place gets its line icon on a
tint tile instead.

**Tints** — a tint means a place, and a content type keeps its tint
wherever it shows: coral for home, changes, AI and watch-outs; butter for
the canvas, drafts and loading; lilac for the content model and structure;
sky for content, promote and info; mint for media and environments. Use the
fill (`--tint-*-rgb`) for icon tiles, chips and stat cards, the wash
(`--tint-*-wash-rgb`) for large panels and notes, and the ink
(`--tint-*-ink-rgb`) for text and icons on either. Tints sort places; they
never signal status, which takes the feedback tokens.

**Marks** — Obelism is named after the marks scribes put in the margins of
manuscripts, and the product uses the real set (Unicode U+2058, U+2E13,
U+2E16 …), drawn as SVGs on `currentColor`, never typed as characters. The
logo is one of them: `⁜`, U+205C, the dotted cross.

- Status marks: `obelism` (anything Obelism, or its AI, proposes until
  someone keeps it), `note` (four dots: notes, info), `watch-out` (the
  dotted obelus: a risky move or a field that won't look right; it warns,
  never blocks), `changed` (the dotted diple: changed since it was
  published, "Updated in" in the docs), `draft` (two dots: not on the site
  yet).
- Structure marks: `group` (a group of fields), `insert` (where a new block
  goes), `split` and `merge`, `links-to` and `used-in` (references),
  `separator` (breadcrumbs and paths), `working` (five dots: the spinner).

A mark sits in the margin, beside what it flags (left of a row, a note or a
field label), never inside a sentence, and always with a word: Note, Watch
out, Changed, Draft. One meaning each; marks may change colour, never
shape. Errors are not marks: anything that failed or blocks takes the
negative token and an error icon.

**Cards** — white (`--theme-background-alt`), a 20 px radius and a hairline
ring, a 32 px icon tile on its tint above a `heading-4` title and a soft
line. A linked card firms its ring and lifts a little on hover.

**Surfaces** — a white workspace panel floats on paper, and cards float in
it, rather than flush, hard-bordered blocks. Their outlines are the hairline
(`--theme-hairline-rgb`), decoration only; a boundary that must be seen (an
input, a checkbox) takes `--theme-border-rgb` (3:1). Dark mode lifts each
surface above the one it sits on (paper, the panel, a card, then its table
head and quiet blocks), so a card's fill separates it from the panel and not
just its ring.

**Focus** — `:focus-visible` gets a 2 px outline in `--theme-focus-rgb` (a
darker orange in light mode, 3:1) with a 2 px offset.
A field without its own border (text inside a card) moves focus to the card:
`:focus-within` sets the border to the accent at 0.6 and adds a 3 px ring of
the accent at 0.18.

**Prompt composer** (the reference for any "type what you want" box, e.g.
the Content SDK's prompt box):

- Closed, it is a single floating pill (56 px high, up to 560 px wide) that
  reads like an empty field: muted 16 px text on the left, the orange `⁜` on
  the right, `--shadow-medium`.
- Open, it is a card (`--border-radius-medium`) with the text area on top
  (16 px, line-height 1.6, no border of its own) and a toolbar below. Left:
  what the prompt acts on and Undo, as pill chips with a 16 px icon. Right:
  close, a thin divider, the round orange send button.
- Progress, results and errors are one status line above the text, with a
  small orange spinner while working.
- Secondary actions (Save, Publish, sign out) go in a footer row on a light
  `--theme-background-actions` tint, separated by a hairline border.

---

## Motion

Motion is purposeful and quick, never showy. Most of it is never consciously
noticed, and that is the point: a press that gives, a list that settles in, an
accordion that opens instead of jumping. [`/components.md`](/components.md#motion-style)
has the recipes; these are the rules.

**Should it animate at all?** Decide by how often someone sees it.

| How often                                           | Motion                                         |
| --------------------------------------------------- | ---------------------------------------------- |
| Constantly (keyboard shortcuts, dashboard toggles)  | None. Keyboard-initiated actions never animate |
| Often (hover, list navigation, presses)             | Tiny: a 3 px nudge, a 0.98 press               |
| Occasionally (panels, accordions, a form's success) | A short enter, a quicker exit                  |
| Rarely, or on marketing pages (first scroll, demos) | Can explain and delight: staggers, a CRT boot  |

Every animation answers "why does this move?": feedback (the press), state
(the flag going on), spatial continuity (a panel rising from where it sits),
or explanation (a terminal replaying a session). "It looks nice" only counts
on marketing surfaces people see once.

**Easing** — never the built-in `ease-in`, and prefer the house curves to the
weak CSS keywords:

| Token           | Curve                             | For                                                                                               |
| --------------- | --------------------------------- | ------------------------------------------------------------------------------------------------- |
| `--ease-out`    | `cubic-bezier(0.23, 1, 0.32, 1)`  | Anything entering or responding: most of the motion is in the first third, so it reads as instant |
| `--ease-in-out` | `cubic-bezier(0.77, 0, 0.175, 1)` | Something moving or morphing on screen                                                            |
| `--ease-roll`   | `cubic-bezier(0.65, 0, 0.35, 1)`  | The button's character roll                                                                       |
| `linear`        | —                                 | Only when tied to scroll position or a constant loop                                              |

Plain `ease-out` is fine for fades and small presses.

**Duration** — UI stays under 300 ms; exits are quicker than entrances.

| What                             | Duration                  |
| -------------------------------- | ------------------------- |
| Press feedback (`scale: 0.98`)   | 160 ms                    |
| Hover nudge, fade-out            | 200 ms                    |
| Crossfade, panel enter           | 250 ms                    |
| Entrance rise, list stagger item | 300 ms                    |
| Disclosure (height + fade)       | open 280 ms, close 200 ms |
| Marketing demo (terminal boot)   | up to ~650 ms             |

**Distances are small.** Things rise 8 px (a message, a panel) to 20 px (a
card in a list), never slide in from off screen. Nothing appears from
`scale(0)`; start at 0.95–0.98 with `opacity: 0`. Staggers are 50 ms apart and
capped (after ~8 items the rest arrive together), and never block input.

**Scroll** — parallax is CSS scroll-driven (`animation-timeline: view()` or a
named `view-timeline`), linear, and subtle: about 3% of the scroll speed
(±20–30 px over a section's time in view). Behind `@supports
(animation-timeline: view())`, so unsupported browsers get a static page.
Scroll-triggered entrances play **once**, when the element's top is about 40%
up the viewport, and only for content that started below the fold.

**What we build with, in order of preference:** CSS transitions (they retarget
when interrupted), `@starting-style` for entering elements (no `mounted`
state in JS), native elements (`<details>` with `::details-content` and
`interpolate-size`), keyframes for one-shot or looping sequences, and
JavaScript only to decide _when_ (an `IntersectionObserver` flipping a
`data-*` attribute). Animate `transform`, `translate`, `scale` and `opacity`;
height only through the native disclosure. Never `transition: all`.

**Progressive, always.** Server-rendered output is the finished state. Never
hide something on hydration that is already on screen; without JavaScript or
with an unsupported feature, everything is simply there.

**Hover** motion sits behind `@media (hover: hover) and (pointer: fine)`, so a
tap on a phone doesn't trigger it.

**Reduced motion** means fewer and gentler, not none: drop movement
(translate, scale, parallax, staggers, the boot sequence) and keep the
opacity fades that explain a change. Essential feedback stays (a spinner
while something works). Gate with `@media (prefers-reduced-motion:
no-preference)` around the motion, or a `reduce` branch that strips the
movement.

---

## Accessibility

- Full light and dark support via `prefers-color-scheme`; `color-scheme` is
  set so form controls and scrollbars match.
- Meet WCAG AA contrast against the actual paper, white and dark
  backgrounds: text 4.5:1, control boundaries and focus rings 3:1. The
  theme tokens are chosen for this.
- Preserve accessible names when text is decorative (e.g. the button roll
  keeps a visually-hidden real label).
- Data-viz colors are colorblind-checked; never rely on color alone to convey
  meaning — pair with labels, shapes, or direct values.

---

## Assets & code style

- **Fonts:** self-hosted Geist and Geist Mono (variable) and Geist Serif
  (one weight) as `woff2`, `font-display: swap`.
- **Icons/illustration:** inline SVG, `fill: currentColor` so marks inherit
  the theme foreground. UI icons are line icons (Lucide): 16 px,
  `stroke: currentColor`, `stroke-width: 1.75`, round caps and joins,
  `fill: none`.
- **Embedded UI** (widgets an SDK puts on a customer's site): render into a
  shadow root with `:host { all: initial; }` so neither side's CSS leaks,
  and define the tokens on the widget's root element under the same names
  (no `base.css` there). Fonts can't be loaded from inside a shadow root,
  so Geist only shows when the host site loads it; the `--font-base` stack
  falls back to the system font. Pin the widget with `position: fixed` and
  the maximum `z-index`, and follow `prefers-color-scheme`.
- **Code formatting** (oxfmt): **tabs**, single quotes, no semicolons,
  trailing commas everywhere. Match this in any file you touch.

---

_This file is the source of truth for the Obelism look and voice. Keep it in
sync when the design tokens or brand direction change._
