# Zavvion Events — Design System

A single-stylesheet, token-driven design system for the Zavvion Events frontend. No
build step. No framework. One CSS file (`public/assets/css/zavvion-ui.css`) and one JS
file (`public/assets/js/zavvion-ui.js`) power every public page, the customer wallet,
the organiser console, the platform admin, and the mobile scanner.

This document is the reference for engineers extending the system — adding components,
tweaking themes, or auditing accessibility.

---

## 1. Design rationale

Zavvion Events sells real-world experiences: galas, concerts, festivals, conferences.
Two surfaces co-exist:

- **Public surfaces** (home, listings, event detail, checkout, customer wallet) are
  *cinematic and editorial*. Deep midnight backgrounds, generous spacing, a serif
  display face, restrained motion. The poster is the hero.

- **Console surfaces** (organiser, admin, scanner) are *dense and operational* — much
  closer to Linear or Stripe. Tight rows, mono-aligned numbers, panels with quiet
  borders, status pills doing the heavy lifting.

Both surfaces share a single token system. The same `--accent`, `--surface`,
`--c-success` cascade through every component. Themes swap colour values; geometry,
spacing, and typography stay constant.

---

## 2. Token system

All tokens are defined as CSS custom properties on `:root` in section 1 of
`zavvion-ui.css`. Theme overrides (section 2) only redefine *colour* tokens; everything
else is global.

### 2.1 Colour tokens

| Token | Role |
|---|---|
| `--bg-base` | Page background (deepest layer) |
| `--bg-soft` | Slight elevation under panels |
| `--surface` | Panel / card background |
| `--surface-2` | Nested surface (hover, sub-panel) |
| `--surface-3` | Deepest interactive surface (input, table row hover) |
| `--border` | Default 1px stroke |
| `--border-strong` | Emphasized stroke (active inputs, focused panels) |
| `--text` | Body text |
| `--text-muted` | Secondary text, helper copy |
| `--text-soft` | Tertiary text, captions, placeholders |
| `--accent` | Primary action / brand highlight |
| `--accent-2` | Secondary accent (warm, used sparingly) |
| `--accent-soft` | Tinted background for accent components |
| `--c-success` / `--c-success-soft` | Confirmation, validation pass |
| `--c-warn` / `--c-warn-soft` | Caution, queued, pending |
| `--c-danger` / `--c-danger-soft` | Errors, destructive |
| `--c-info` / `--c-info-soft` | Informational, neutral status |
| `--hero-grad` | Multi-stop gradient used in hero, organiser CTA, theme previews |

Always reference tokens — never hard-code hex values inside components. If a colour
isn't covered by a token, the right move is usually to add a token, not to inline a
value.

### 2.2 Typography

Three fonts, all loaded via `<link>` in every page head:

- **Fraunces** — display serif. Headlines, hero titles, ticket holder name, scan result
  text. Use the optical-size axis at large sizes (28px+).
- **Manrope** — body sans. All paragraphs, labels, buttons, table cells.
- **JetBrains Mono** — numerics, IDs, codes, timestamps, ticket tokens.

Type scale is named by pixel size for predictability:

```
--fz-12   --fz-13   --fz-14   --fz-15   --fz-16   --fz-18
--fz-20   --fz-24   --fz-30   --fz-36   --fz-48   --fz-60   --fz-72
```

Aliases are provided for ergonomics (`--fs-xs` → `--fz-12`, `--fs-sm` → `--fz-14`,
`--fs-md` → `--fz-16`, `--fs-lg` → `--fz-20`).

Line height is fixed by element role — body 1.55, h1/h2 1.1, h3 1.2, mono blocks 1.4.

### 2.3 Spacing

Spacing is a fixed scale, not a free-form numeric system. Always pick a token:

```
--sp-0   --sp-1   --sp-2   --sp-3   --sp-4   --sp-5   --sp-6
--sp-8   --sp-10  --sp-12  --sp-16  --sp-20  --sp-24  --sp-32
```

Numbers are pixels (e.g. `--sp-4` = 16px, `--sp-8` = 32px). The scale skips around
deliberately — there is no `--sp-7`. Use the next size up if you feel cramped.

### 2.4 Radii, shadows, motion

```
--r-2, --r-3, --r-4, --r-6, --r-8, --r-12, --r-16, --r-pill, --r-round
--shadow-xs, --shadow-sm, --shadow-md, --shadow-lg, --shadow-glow
--ease-standard, --ease-emphatic
--dur-fast (120ms), --dur-base (200ms), --dur-slow (320ms)
```

All transitions use these duration + easing tokens. Motion is restrained on public
surfaces (fades, gentle translates) and effectively absent on console surfaces. We
never animate layout (width/height) — we animate `opacity`, `transform`, and `filter`.

### 2.5 Layout tokens

```
--container        max-width of the central column (1200px)
--container-tight  narrow content (760px) — used for long-form copy
--header-h         header height (64px), used for sticky offsets
--side-w           console sidebar width (256px)
```

---

## 3. Themes (16)

Themes are applied to the document via `data-theme="..."` on `<html>`. The active
choice is persisted in `localStorage` under the key `zv:theme`. Admins can broadcast a
site-wide default via `POST /api/v1/admin/themes/active` (see
`docs/frontend-handoff.md`).

The active admin-selectable catalog has **two classes** of theme. The former foundation
colour skins (`obsidian`, `midnight`, `aurora`, `ember`, `noir`, `velvet`, `forest`) are
retired and are not exposed in the catalog or Theme Studio.

**Botanical luxury skins (6)** — full palette + designed background image, all
harmonised. Applied via `[data-theme="<id>"]` block + `body::before` painting the
image as a fixed-to-viewport cover layer at z-index −1. Stage hero, disciplines,
invitation, and marquee bands are translucent so the image bleeds through the entire
page as one continuous textured surface.

| ID | Name | Image | Palette character |
|---|---|---|---|
| `gala` | Gala | emerald + ornate gold flowers | jewel-box opera, gala invitation |
| `verdant` | Verdant | emerald + gold pattern | Italian palazzo, marble palace |
| `onyx` | Onyx | black + bronze, smoky | noir gallery, atelier |
| `ivory` | Ivory | cream + gold (LIGHT theme) | daytime luxury, spa, members' club |
| `foundry` | Foundry | black + bronze leaves | premium suite, after hours |
| `ironwood` | Ironwood | black wood + sparse bronze | minimal, masculine, restrained |

**Cultural mask skins (10)** — Festival of Masks collection. Same architecture as
the botanical luxury skins (full palette swap + image bleed-through). Theatrical and
ceremonial register, ideal for opera, theatre, gala, festival, and cultural events.

| ID | Name | Image | Palette character |
|---|---|---|---|
| `tiki` | Tiki | Polynesian masks on emerald weave | tropical festival, luau |
| `serpent` | Serpent | Naga cobra mask on black + gold leaves | mystical, ceremonial |
| `hannya` | Hannya | Japanese Noh mask on cream (LIGHT theme) | daytime ceremony, tea house |
| `ritual` | Ritual | Sri Lankan dance mask, vivid orange + flame | dramatic, ceremonial |
| `cobra` | Cobra | Cobra mask on dark wood, bronze + restrained red | exotic, restrained |
| `elegy` | Elegy | Lamenting mask, deepest black + muted bronze | tragic theatre, intimate |
| `carnivale` | Carnivale | Venetian masks on emerald velvet | carnival, masquerade ball |
| `pavilion` | Pavilion | Black & gold mask on cream (LIGHT theme) | formal ball, daytime gala |
| `argento` | Argento | Silver and gold masks on dark wood | dual-metal, restrained |
| `doge` | Doge | Black & gold Venetian mask on emerald | royal Venetian, theatrical |

Skin images live in `public/assets/img/themes/<id>.jpg` (~240-400 KB each, 16 total).

### 3.1 Adding a new theme

For a future palette-only skin:

1. Append a new block in `zavvion-ui.css` section 2:

   ```css
   [data-theme="copper"] {
     --bg-base: #1a0f0a;
     --surface: #241710;
     --line: rgba(217,119,66,0.18);
     --text: #f5e8dc;
     --text-muted: #c8a78d;
     --accent: #d97742;
     --accent-2: #f0a868;
     /* ...complete the colour set... */
     --hero-grad: linear-gradient(180deg, #1a0f0a, #0e0805);
     color-scheme: dark;
   }
   ```

For a luxury skin (palette + image + bleed-through):

1. Save the image at `public/assets/img/themes/<id>.jpg`.
2. Add a `[data-theme="<id>"]` block in CSS section 35 (botanical) or 38 (cultural)
   with full palette tokens.
3. Add the matching `body::before { background-image: url('../img/themes/<id>.jpg'); }`.
4. Add the id to the unified bleed-through selectors in section 36 / 39 (.zv-stage,
   .zv-disciplines, .zv-invitation, .zv-marquee).
5. Add `--skin-stage-tint` and `--skin-stage-tint-2` for the stage radial wash.

Then register in `zavvion-ui.js` MOCK.themes and admin's `LEAF_OVERLAYS` map.

### 3.2 Switching themes at runtime

```js
Zavvion.Theme.apply('gala');       // sets data-theme + persists to localStorage
Zavvion.Theme.current();           // returns active id
Zavvion.Theme.list();              // returns all theme metadata
```

The Theme Studio also calls `api.setTheme(id)` to broadcast the change globally for
admins.

---

## 4. Components

All components are prefixed `.zv-` and live in sections 3–29 of the stylesheet. The
list below is the canonical reference; if you need something that isn't here, prefer
extending an existing component over inventing a new one.

### 4.1 Layout primitives

- `.zv-container` — centered max-width column
- `.zv-section` — vertical rhythm wrapper (top/bottom padding)
- `.zv-grid--2` / `.zv-grid--3` — responsive grids that collapse to 1 column under 720px
- `.zv-grid--checkout` — 1fr + 380px, collapses under 980px
- `.zv-shell` / `.zv-side` / `.zv-main` — console layout (sidebar + main)

### 4.2 Surfaces

- `.zv-panel` — primary container (border, radius, padding)
- `.zv-panel__head` — flex row inside panel head with title left, actions right
- `.zv-card` — lighter surface for grids of items (events, products, themes)
- `.zv-stat` — single KPI (label + value + delta)
- `.zv-stat-grid` — 4-up responsive stat row

### 4.3 Buttons

- `.zv-btn` — base
- `.zv-btn--primary` — filled accent
- `.zv-btn--ghost` — transparent with border
- `.zv-btn--outline` — outline only
- `.zv-btn--danger` — destructive
- `.zv-btn--sm` / `.zv-btn--lg` — sizing modifiers
- `.zv-btn--block` — full-width

Always include an `aria-label` if the button has only an icon child.

### 4.4 Form controls

- `.zv-field` — label + input wrapper
- `.zv-input`, `.zv-select`, `.zv-textarea` — base controls
- `.zv-checkbox`, `.zv-radio` — custom styled, fall back to native focus ring
- `.zv-stepper` — −/qty/+ for ticket quantity

### 4.5 Status indicators

- `.zv-pill` + variants `--ok`, `--warn`, `--danger`, `--info`, `--muted`
- `.zv-badge` — solid colour chip used in card overlays
- `.zv-result` + variants `--ok`, `--warn`, `--bad` — large success/failure states

### 4.6 Navigation

- `.zv-header` / `.zv-header__inner` — public site header
- `.zv-nav` — primary nav row
- `.zv-side__group` / `.zv-side__heading` / `.zv-side__link` — console sidebar
- `.zv-tabs` / `.zv-tab` — horizontal hash-routed tabs
- `.zv-pillbar` — segmented filter control
- `.zv-stepper-bar` — checkout progress (Tickets → Details → Payment → Confirm)

### 4.7 Tables and data

- `.zv-table` — semantic `<table>` with hover rows and aligned mono numerics
- `.zv-feed` / `.zv-feed__row` / `.zv-feed__dot` — activity timelines

### 4.8 Tickets and seats

- `.zv-ticket` + slots `__main`, `__title`, `__meta`, `__seat`, `__token`, `__qr`
- `.zv-ticket--past` — dimmed state
- `.zv-seatmap` / `.zv-seat` + states `--free`, `--taken`, `--selected`, `--vip`
- `.zv-summary` + `__body`, `__lines`, `__totals`, `__grand`
- `.zv-hold-bar` — countdown banner during seat hold

### 4.9 Scanner

- `.zv-scan-stage` — large camera viewport
- `.zv-scan-big` — Fraunces display result
- `.zv-scan-history` — recent scans list
- `.zv-scan-result--ok` / `--bad` / `--warn` — with `__icon` slot

### 4.10 Toasts and modals

- `Zavvion.toast(message, kind, timeout)` — kind ∈ `info` | `success` | `error`
- `Zavvion.modal({title, body, actions})` — actions take the shape
  `{label, cls, onClick(close)}` where `cls` is the full button class string and
  `onClick` receives the close function.

---

## 5. Accessibility

The system is built to meet **WCAG 2.1 AA** as the floor.

- **Contrast**. Every theme is hand-tuned to keep `--text` against `--bg-base` and
  `--surface` above 7:1. `--text-muted` against `--surface` is held above 4.5:1.
  `--text-soft` is reserved for non-essential captions and stays above 3:1.
- **Focus**. Every interactive element has a visible focus ring using
  `outline: 2px solid var(--accent); outline-offset: 2px`. We never rely on colour
  alone — focus also shifts shadow.
- **Keyboard**. Tab order follows DOM order. Modals trap focus on open and restore
  focus on close. The console sidebar is fully keyboard-navigable — links, no
  divs-as-buttons.
- **Hit targets**. Minimum 40×40px on touch surfaces, 44×44px on the scanner.
- **Motion**. Anything decorative is gated behind `@media (prefers-reduced-motion:
  reduce)`. The scanline animation, hero parallax fades, and toast slide-ins all stop
  when this query matches.
- **Screen readers**. Status pills carry `aria-label` describing the state in words,
  not colour. The scanner result region uses `aria-live="polite"` so SR users hear
  scan outcomes.
- **Skip link**. `.zv-skip` is the first focusable element on every page, jumping to
  the main `<main id="main">` landmark.
- **Forms**. Every input has a `<label>` (visible or `.zv-visually-hidden`). Errors are
  announced in-line via `aria-describedby`, never by colour change alone.

---

## 6. Responsive breakpoints

There are four named breakpoints. They are kept simple deliberately:

```
default          mobile-first, ~360px and up
@media (min-width: 720px)   tablet
@media (min-width: 980px)   small desktop, multi-column appears
@media (min-width: 1200px)  full-width container
```

Console layouts collapse to single-column under 980px and the sidebar becomes a
drawer (toggled via `[data-zv-toggle="sidebar"]` and the `.is-open` class on
`.zv-side` plus `.zv-overlay`).

The checkout summary becomes a sticky bottom-sheet under 980px — the
`.zv-summary__handle` element is the visible drag affordance.

The scanner is the only screen that actively *prefers* mobile and stays single-column
even on desktop, with the camera viewport capped to a sensible reading size.

---

## 7. Motion principles

- **Quiet by default.** Console interfaces transition in 120ms or not at all. We never
  delay a click result.
- **Editorial moments.** Hero entrances, theme switches, and successful scan
  confirmations get 320ms eased motion to feel intentional.
- **Single property at a time.** `transform` and `opacity` only. No animating width,
  height, top, left, margins, padding.
- **Loops sparingly.** Only the scanner scanline and the loading skeletons loop. The
  scanner stops the loop on a result. Skeletons stop the loop when content arrives.

---

## 8. Naming conventions

- All custom classes are prefixed `.zv-`. There are no exceptions — this prevents
  collisions when the frontend is mounted alongside admin tools, vendor scripts, or
  legacy templates.
- BEM-flavoured: block `zv-panel`, element `zv-panel__head`, modifier
  `zv-panel--accent`. Modifiers always go last.
- States use `is-` prefix: `is-active`, `is-open`, `is-disabled`, `is-loading`. These
  are added/removed by JS and styled via `.zv-foo.is-open`.
- Data attributes drive behaviour: `[data-route]` for hash routing,
  `[data-zv-toggle]` for UI toggles, `[data-route-panel]` for panel visibility,
  `[data-theme]` for theming. JS hooks always go on `data-*`, never on classes.
- IDs are reserved for unique anchors (`#main`, `#zv-checkout-body`,
  `#zv-result`). Never style by ID.

---

## 9. File structure recap

```
public/assets/css/zavvion-ui.css   single stylesheet, ~1250 lines, 30 sections
public/assets/js/zavvion-ui.js     single IIFE, ~620 lines, exposes window.api + window.Zavvion
public/assets/img/                 placeholder dir (posters generated as inline SVG by JS)
```

There is no build step. Edit the CSS, save, refresh. Edit the JS, save, refresh. The
constraint is deliberate — it keeps the surface area small, the diffs readable, and
the deploy story trivial (`scp` or `git pull` and you're done).

---

## 10. Extending the system

When you need something that isn't here:

1. **Check tokens first.** Nine times out of ten the colour or spacing already exists.
2. **Look for a sibling component.** A new "promo" panel is probably a `.zv-panel` with
   a modifier, not a new component.
3. **Add a token before adding a value.** If the new component needs a colour that
   isn't in the system, add the token to `:root` and to every theme. Don't inline
   hex.
4. **Mirror the BEM structure.** Block, elements with `__`, modifiers with `--`,
   states with `is-`.
5. **Document in this file.** A new component is not done until it appears in section
   4 above.

For larger expansions (a new console page, a new public surface), copy the closest
existing page as a starting point and prune what you don't need. The pages are
intentionally self-contained — each one is a working artifact you can read top to
bottom.
