# 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 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).

---

## 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

The palette is a warm neutral core — **beige** and near-**black** — with a
single **orange** accent. Blue is the interactive/link color. The decorative
brand colors (red, purple, green, blue) are for illustration only; data
visualisation uses a separate, colorblind-checked series palette (below).

Every color is defined as an RGB triple custom property
(`--color-<name>-rgb`) so it can be used at any opacity via
`rgb(var(--color-x-rgb) / <alpha>)`. Hex equivalents are given here for
convenience.

### Neutrals (white → black)

| Token                | Hex       | RGB         |
| -------------------- | --------- | ----------- |
| `--color-white`      | `#F8F8F8` | 248 248 248 |
| `--color-white-off`  | `#F0F0F0` | 240 240 240 |
| `--color-grey-light` | `#848484` | 132 132 132 |
| `--color-grey-blue`  | `#868F97` | 134 143 151 |
| `--color-grey`       | `#6C6C6C` | 108 108 108 |
| `--color-grey-mid`   | `#646464` | 100 100 100 |
| `--color-grey-dark`  | `#282828` | 40 40 40    |
| `--color-black-off`  | `#141414` | 20 20 20    |
| `--color-black`      | `#0C0C0C` | 12 12 12    |

### Brand & accent

| Token                | Hex       | Role                                               |
| -------------------- | --------- | -------------------------------------------------- |
| `--color-beige`      | `#EEECE9` | Primary light background                           |
| `--color-beige-off`  | `#E4E1D9` | Muted / action surface (light)                     |
| `--color-orange`     | `#D4821E` | **Accent** — primary CTAs, focus rings, highlights |
| `--color-blue`       | `#005492` | Interactive / links (light)                        |
| `--color-blue-light` | `#8EB9F0` | Interactive / links (dark)                         |
| `--color-red`        | `#F05D5D` | Decorative / error                                 |
| `--color-purple`     | `#AD64BB` | Decorative                                         |
| `--color-green`      | `#7DB47A` | Decorative / success                               |

Orange is the one accent that carries the brand — use it deliberately
(one clear call to action per view), not as a fill.

### Semantic theme tokens

The UI is themed through a small set of semantic tokens that remap between
light and dark (via `prefers-color-scheme`). Build with these, not raw
palette values.

| Token                        | Light      | Dark       |
| ---------------------------- | ---------- | ---------- |
| `--theme-foreground`         | black      | white      |
| `--theme-background-main`    | beige      | black      |
| `--theme-background-alt`     | white      | black-off  |
| `--theme-background-actions` | beige-off  | grey-dark  |
| `--theme-border`             | grey-light | grey       |
| `--theme-foreground-muted`   | grey-mid   | grey-light |
| `--theme-accent`             | orange     | orange     |
| `--theme-error`              | `#B3261E`  | red        |

Secondary text (placeholders, captions, status lines) uses
`--theme-foreground-muted`, never `--theme-border`: the border grey only
reaches about 3.5:1 against the backgrounds, below WCAG AA for text.

Error text uses `--theme-error`. Brand red (`#F05D5D`) is about 3:1 on the
light backgrounds, so for text it only works in dark mode; in light mode
use `#B3261E` (about 6:1).

Note: the standalone studio splash page currently uses `#0F0F0F` for its
near-black; the canonical system value is `#0C0C0C` — prefer the token.

### 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

One typeface: **Inter** (the `InterVariable` woff2, weights 100–900, with a
matching italic). System-font fallbacks keep the first paint honest.

```css
--font-inter:
	'InterVariable', system-ui, -apple-system, BlinkMacSystemFont, Segoe UI,
	Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji,
	Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
--font-mono:
	ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono', 'Roboto Mono',
	'Oxygen Mono', 'Ubuntu Monospace', 'Source Code Pro', 'Fira Mono',
	'Droid Sans Mono', 'Courier New', monospace;
```

Headings are heavy and tight: **weight 800**, `letter-spacing: -0.015em`,
`line-height: 1.2`. Body is **weight 400** at `line-height: 1.6`. Labels run
300–400. Sizes below list mobile → `min-width: 768px` where they scale up.

| Style     | Size (mobile → ≥768px) | Weight | Line height |
| --------- | ---------------------- | ------ | ----------- |
| heading-1 | 48 → 62 px             | 800    | 1.2         |
| heading-2 | 40 → 48 px             | 800    | 1.2         |
| heading-3 | 36 → 42 px             | 800    | 1.2         |
| heading-4 | 26 → 32 px             | 800    | 1.2         |
| body-1    | 22 px                  | 400    | 1.6         |
| body-2    | 18 px                  | 400    | 1.6         |
| body-3    | 16 px                  | 400    | 1.6         |
| label-1   | 14 → 16 px             | 400    | 1.4         |
| label-2   | 12 → 14 px             | 400    | 1.4         |
| label-3   | 12 px                  | 300    | 1.4         |

---

## 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:** floating surfaces (a panel over a page, a bar pinned to the
viewport) get one soft, low shadow instead of heavier borders:
`--shadow-float: 0 18px 40px -16px rgb(var(--color-black-rgb) / 0.3), 0 2px 6px rgb(var(--color-black-rgb) / 0.05)`,
stronger in dark mode (`0 18px 40px -14px rgb(0 0 0 / 0.7), 0 2px 6px rgb(0 0 0 / 0.3)`)
where a light shadow disappears.

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** — pill-shaped (`border-radius: 5em`), `padding: 0.4em 1.2em`,
`label-1` type, 1px border at `rgba(border, 0.2)`. Three variants:

- `primary` — white text on the orange accent. One per view: in a toolbar
  that is the submit, and actions like Save or Publish next to it stay
  `secondary`, not filled with the foreground color.
- `secondary` — foreground text on the background color.
- `transparent` — foreground text, no fill.

Disabled: `opacity: 0.5`, `cursor: not-allowed`. Loading:
`cursor: progress`. Buttons have a subtle per-character "roll" hover
animation — decorative, and disabled under `prefers-reduced-motion: reduce`.

**Icon buttons** — round (`border-radius: 50%`), about 34 px, muted until
hovered (`--theme-background-actions` fill, foreground color). A primary
icon button is a white icon on an orange circle. The accessible name is a
visually-hidden label inside the button, plus a `title` for the tooltip.

**Cards** — rounded (medium/large radius), on `--theme-background-alt`, often
introduced by a small `subTitle` label above a heading. Feature cards carry a
single leading emoji.

**Surfaces** — float rounded panels over the main background rather than using
flush, hard-bordered blocks. Borders are low-contrast
(`rgba(--theme-border, 0.2–0.35)`).

**Focus** — `:focus-visible` gets a 2 px orange outline 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-float`.
- 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 beige/black backgrounds — the
  neutral scale and blue link colors 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 `InterVariable.woff2` + `InterVariable-Italic.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 Inter only shows when the host site loads it; the `--font-inter` 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._
