# Evacor Design System — v2.1.0

One system for every product in the Evacor family. Every app uses the same shell, the same components and the same tokens — only the product name, monogram, nav items and content change.

## What's in this package

| Path | What it is |
|---|---|
| `README.md` | This file. Setup, rules, tokens at a glance. |
| `PROMPT.md` | A ready-to-paste prompt for building an Evacor screen with an AI tool. |
| `VISUALS.md` | The visual language in words + an index of every screenshot. |
| `CLAUDE.md` | Standing instructions for Claude Code working in an Evacor repo (vendored into every app). |
| `CHANGELOG.md` | What changed per version; copied into each app's update PR. |
| `apps.json` | The apps that receive the design system and where their `tokens.css` lives. |
| `scripts/` | `check`, `build`, `release`, `sync-apps`. |
| `tokens.css` | All CSS variables for Brand / Dark / Light + component recipes (`.ev-*`). |
| `tokens.json` | The same values for JS, native or Figma token plugins. |
| `screenshots/` | Every section of the handbook, in all three themes. |
| `reference/` | The interactive handbook source. Live at https://system.evacor.io, or `npm run dev` locally. |
| `reference/assets/evacor-mark.svg` | The Evacor mark, `fill: currentColor`. The one logo every product uses. |
| `reference/assets/evacor-logo.svg` | The same mark in its three colour modes, for brand work. |

## How updates reach the apps

This repo is the only place the design system is edited. Each app carries a vendored copy of `tokens.css` (at the path its layout already imports) and a read-only `design-system/evacor/` folder with this handbook, and never edits them.

1. Change the system here (with Claude Code: it follows `CLAUDE.md`).
2. Note it in `CHANGELOG.md`, run `npm run check`, commit.
3. `npm run release -- patch|minor|major` then `git push --follow-tags`.
4. The **Sync apps** workflow opens one pull request per app listed in `apps.json`, titled "Design system vX.Y.Z", with the changelog in the body. Each PR gets a Vercel preview.
5. Review the preview, merge. Merging is the production deploy for that app. Nothing changes in any app until its PR is merged, and each app can be rolled back on its own.

Locally, `npm run sync` does the same as the workflow (needs `gh auth login` as bsqagency); `npm run sync:dry` shows what would change without pushing.

The handbook itself is deployed from `main` to https://system.evacor.io on every push.

## Setup

```html
<html data-theme="brand">   <!-- "brand" | "dark" | "light" -->
<head>
  <link rel="stylesheet" href="tokens.css">
</head>
```

Font: `'Inter Display', 'Inter', sans-serif`. Weights **400 and 500 only**.

Theme toggle cycles **Brand → Dark → Light**. The only theme control is the **Switch theme** row in the profile menu (and the icon button on the workspace card). Never a Light / Dark / Brand text control, on any screen size. The choice is remembered per product (`<app id>-theme`).

## Themes

- **Brand** — Dark tokens, near-black page (`#0A0C0B`) with a soft deep-green glow at the top. Every container is transparent glass (`rgba(255,255,255,.02–.07)` + `backdrop-filter: var(--glass)`, 14px) so the glow shows through. Borders stay 1px silver.
- **Dark** — Solid near-black surfaces, silver rim borders.
- **Light** — Warm grey page (`#F7F8F7`), white cards, single 1px borders (`#ECEEED`).

## Core rules

- Use `var(--token)`. Never a literal hex in product code.
- Radius: **3px** on every square shape. `999px` only for avatars, dots, pills, radio and the active nav bar.
- **No drop shadows.** Depth comes from borders and surface fills.
- **Brand accent** `--accent` (`#7AE7CF`): at most one accent element per screen, and only on the main commit action (`.ev-btn.ev-btn--accent`). Never on links, focus rings, status, charts, surfaces or a second button. Screens without a commit action (settings that autosave, dialogs) have no accent. The Evacor mark in Brand theme is the one documented exception and does not count.
- **The logo** is the shared Evacor mark (`reference/assets/evacor-mark.svg`, 24px tall, colour `--logo`: dark on Light, light on Dark, accent on Brand) followed by the product wordmark: the product name in uppercase, 15px / 500 / .14em. Never a monogram tile, never a product-specific mark.
- **Indicator colours** are for status and category only — never surfaces, never actions. Actions are monochrome (`--btn-bg` / `--btn-fg`) or the accent.
- Motion: animate `transform`, `opacity` and `filter` only (colour crossfades for hover, press and theme). `--ease: cubic-bezier(.22,1,.36,1)`, `--spring: cubic-bezier(.34,1.3,.64,1)`, `--ease-in` for exits, durations 160 / 240 / 380ms. Buttons press to `scale(.97)` on `--ease` and release on `--spring`. Everything that opens also closes: menus exit on `--dur-1`, dialogs and scrims on `--dur-2`. Knobs, ticks and indicators travel with `transform`, never with `width`, `padding` or `justify-content`. Theme changes are one full-page crossfade (`document.startViewTransition`). Respect `prefers-reduced-motion`.
- Scrollbars: 5px, thumb only, `--bd-active`.
- Navigation labels are Title Case.

## Indicator colours

| Name | Light stop (-a) | Base (-b) |
|---|---|---|
| Mint | `#9AE0BD` | `#60CD96` |
| Sky Blue | `#8CCDED` | `#4AB0E3` |
| Periwinkle | `#9696E3` | `#5A5AD3` |
| Crimson | `#D94A4F` | `#B9272C` |
| Coral | `#F4B985` | `#EE913F` |
| Slate (neutral) | `#B7BCC2` | `#8A9099` |

Seven status treatments, all built from these two stops: Glass pill (default), Outline, Dot in pill, Dot + label, Left bar, Tint, Dot only.

## Status → colour
| Status | Token |
|---|---|
| Open, Live | `--ind-sky` |
| Approved | `--ind-mint` |
| Paid | `--ind-periwinkle` |
| Review | `--ind-coral` |
| Overdue | `--ind-crimson` |
| Pending, Draft | `--ind-slate` |

Glass pill: text `-b`, background `linear-gradient(180deg, -a @10%, -b @6%)`, border `-b @12%`, 10px / 500, padding 5×10.


## App shell (same in every app)

- **Rail** 300px: brand block (the Evacor mark, 24px, `--logo` + the product wordmark, uppercase 15px / 500 / .14em) → grouped nav (Title Case labels, 3px active bar) → workspace card at the bottom (avatar, name, role, Settings / Theme / Sign out).
- **Header** 56px, sticky, joined to the rail: page title (no breadcrumb) → search with Ctrl K → one icon tray: Share, Notifications, App switcher (waffle), Profile avatar.
- **Profile menu** opens from the avatar: Profile, Switch theme, Sign out. This is the only place the theme is switched from the header; there is no Light / Dark / Brand control in any header, including the compact one.
- **Main column**: hero card (greeting "Good morning, <Name>" + grouped KPI strip + max 2 actions; the primary is the screen's one accent) → content panels.
- **Below 1200px** the rail becomes an off-canvas drawer (opens on `--spring`, closes on `--ease-in`, scrim fades, page scroll locks) and the header becomes a 56px compact bar: menu, mark + wordmark, search icon (expands into the bar), avatar with the same profile menu. Nothing else is added or removed.

## Layout
Rail 300px · header 56px · panel gap 16px · grid gap 12px · panel padding 20–28px · touch target 44px.
Breakpoints: phone 720, tablet 1200, laptop 1400. Below tablet the rail becomes an off-canvas drawer and the header the compact bar. On phones: panel padding 18/16, hero KPIs 2×2, tab rows scroll sideways, tables scroll inside their panel (never the page), inputs are 16px, every icon button has a 44px hit area, the Ctrl K chip is hidden.

## Layout rule: equal-height rows

Cards that sit side by side in one row always share the row's height.

- Row containers use `display:grid` (or flex) with `align-items:stretch` — never `start`.
- Each card is `display:flex; flex-direction:column; height:100%`.
- Short content stays at the top; empty space falls to the bottom of the card. Pin footers/actions with `margin-top:auto`.
- Empty states fill the card: centre the empty message vertically when the card is taller than its content.


## Layout rule: full-height screens

Every screen fills the viewport — no dead space below the content.

- The main column is `display:flex; flex-direction:column; min-height:calc(100dvh - 56px)`.
- The last content panel (table, board, list, empty state) takes the rest: `flex:1; min-height:0`.
- Inside that panel, the body (rows, kanban columns, empty state) is also `flex:1` so it stretches to the bottom.
- Empty states are centred vertically inside the stretched panel.
- Long content scrolls inside the panel body; the page itself never ends mid-screen.
