# Design-System Audit — liquid-glass-design-tokens (pre-implementation)

**Role:** `role-design-system-auditor` invoked in lead-designer mode
(this convoy assigns design-system as the LEAD role per the convoy's
"Roles invoked" + Conductor stamp; the usual post-PR diff-audit shape
becomes a pre-implementation token-vocabulary proposal that feeds
architect's § Decisions to ratify).

**Convoy:** `.convoys/liquid-glass-design-tokens.md`
**Umbrella:** `.convoys/liquid-glass-redesign.md`
**Authored:** 2026-06-03

---

## Audit summary

| Check | Status | Count |
| --- | --- | --- |
| Token violations in current `styles/globals.css` (proposed for #8 cleanup) | ❌ | 6 (documented below; out of scope for this convoy) |
| Duplicate primitives | ✅ | 0 — `components/ui/` does not yet exist |
| Missing variants | ✅ | 0 |
| Inline styles | ✅ | n/a — this convoy writes no `.js` |
| Operator-default honour | ✅ | 5/5 honoured |

The token-vocabulary proposal below is the deliverable. Architect
ratifies values + writes Brief 1.

---

## 1. Operator defaults honoured

Pulled verbatim from `.convoys/liquid-glass-redesign.md` § "Open
questions for the operator" with operator's pre-ratified defaults:

| # | Default | Honoured in proposal? |
| --- | --- | --- |
| 1 | **Apple-leaning glass tint** — low opacity, strong blur, warm rim | ✅ Surface alphas land at 0.55 / 0.68 / 0.82 (low/mid/high); default blur is 20px; saturate 140%. |
| 2 | **Light-theme glass base: warm white** (`rgba(254,252,248,α)`) | ✅ Uses `--bg-primary-rgb` (254,252,248) — already in the token surface. No new hex. |
| 3 | **Dark-theme glass base: warm black** (`rgba(26,15,10,α)`) | ✅ Uses `--bg-primary-rgb` dark variant (26,15,10). |
| 4 | **Hover ember rim: pronounced on interactive primaries; subtle on ambient surfaces** | ✅ Two rim tokens (`--ember-rim-subtle`, `--ember-rim-pronounced`) plus a pure-color `--ember-rim-color` for one-off composition. |
| 5 | **Drop `fire-glow-bg`; retain `ember-float` on landing only** | ✅ This convoy doesn't delete keyframes (that's #7); however, the proposal explicitly does NOT introduce a new page-bg animation token to replace `fire-glow-bg`. Documents the deprecation in `docs/DESIGN_TOKENS.md`. |

---

## 2. Token vocabulary (29 tokens)

Grouped by layer. Every token gets a value in BOTH light (`:root`) and
dark (`[data-theme="dark"]`).

### 2.1 Surface (3 tokens)

The translucent panel fills. Three steps on a legibility ramp.

| Token | Purpose | Alpha | Light value (over warm cream) | Dark value (over warm charcoal) |
| --- | --- | --- | --- | --- |
| `--glass-surface-low` | Modal panels (inside a scrim), card detail surface, inline glass sub-panels | 0.55 | `rgba(254, 252, 248, 0.55)` | `rgba(26, 15, 10, 0.55)` |
| `--glass-surface-mid` | Sidebar rail, header strip, mobile bottom-bar | 0.68 | `rgba(254, 252, 248, 0.68)` | `rgba(26, 15, 10, 0.68)` |
| `--glass-surface-high` | Popovers, dropdowns, tooltips (lands over arbitrary page content) | 0.82 | `rgba(254, 252, 248, 0.82)` | `rgba(26, 15, 10, 0.82)` |

**Rationale on the 0.55 / 0.68 / 0.82 ramp:**

Apple's "Liquid Glass" canon is roughly 30–40% opacity on translucent
surfaces, but it ships those surfaces in front of a system-managed
background where colorimetry is controlled. Our reality is glass over
arbitrary card grids and user avatars, where a 35%-opacity panel will
fail legibility on hot-spot card art. The ramp:

- **`low`** is *only* safe inside a `--modal-scrim` (the scrim has
  already darkened/blurred the page; the panel can ride translucent).
- **`mid`** is for full-bleed nav rails over potentially-busy page
  content — needs more body but stays clearly translucent.
- **`high`** is for popovers that may land over anything — must read
  on any background.

Architect to ratify: this is Decision 1 in the convoy.

### 2.2 Blur (3 tokens) + Saturate (1 token)

| Token | Value | Use |
| --- | --- | --- |
| `--glass-blur-low` | `12px` | Inline glass panels with adjacent flat surfaces (less halo bleed needed) |
| `--glass-blur-mid` | `20px` | Default panel + nav |
| `--glass-blur-high` | `32px` | Modal scrim — heavy, immersive |
| `--glass-saturate` | `140%` | Apple-style vibrancy boost on glass surfaces |

Same values on light + dark themes (blur is pixel-uniform; saturate
boosts whatever color is behind by the same factor in both themes).

Architect to ratify: Decisions 4 + 5 in the convoy.

### 2.3 Rim-light (4 tokens)

The hairline edges that define a glass surface against the background.

| Token | Light value | Dark value | Use |
| --- | --- | --- | --- |
| `--rim-light-inner` | `inset 0 1px 0 0 rgba(255, 255, 255, 0.65)` | `inset 0 1px 0 0 rgba(255, 248, 240, 0.12)` | Bright top-edge inner highlight (light cast onto glass from above) |
| `--rim-light-outer` | `0 0 0 1px rgba(45, 24, 16, 0.08)` | `0 0 0 1px rgba(255, 248, 240, 0.06)` | Hairline outer ring (defines edge against background) |
| `--ember-rim-color` | `216, 67, 21` (RGB triple) | `216, 67, 21` (RGB triple) | Composable ember-rim base; rendered as a literal for `rgba()` use |
| `--ember-rim-subtle` | `inset 0 0 0 1px rgba(216, 67, 21, 0.35)` | `inset 0 0 0 1px rgba(216, 67, 21, 0.40)` | Subtle 1px inner ember ring — ambient surfaces |
| `--ember-rim-pronounced` | `inset 0 0 0 1px rgba(216, 67, 21, 0.55), 0 0 16px 0 rgba(216, 67, 21, 0.30)` | `inset 0 0 0 1px rgba(216, 67, 21, 0.65), 0 0 16px 0 rgba(216, 67, 21, 0.35)` | Inner ring + 16px outer bloom — interactive primaries (buttons, focused inputs, selected cards) |

Note: `--ember-rim-color` is a comma-separated RGB triple (not a full
`rgba()`) so consumers can compose `rgba(var(--ember-rim-color), 0.42)`
inline when they need a custom alpha. Matches the existing
`--accent-ember-rgb` pattern in `styles/globals.css` lines 41 + 73.

Dark theme rim alphas are slightly higher (0.40 vs 0.35; 0.65 vs 0.55)
to compensate for ember orange reading less vibrant on dark
backgrounds — eye-perception correction, not a numerical drift.

### 2.4 Elevation (3 tokens)

Shadow stacks. Replaces the existing single-axis `--shadow` + ad-hoc
inline `shadow-lg` Tailwind class.

| Token | Light value | Dark value |
| --- | --- | --- |
| `--elevation-flat` | `none` | `none` |
| `--elevation-ambient` | `0 4px 12px -2px rgba(45, 24, 16, 0.08), 0 2px 4px -1px rgba(45, 24, 16, 0.04)` | `0 4px 12px -2px rgba(0, 0, 0, 0.40), 0 2px 4px -1px rgba(0, 0, 0, 0.30)` |
| `--elevation-pronounced` | `0 24px 48px -12px rgba(45, 24, 16, 0.20), 0 12px 24px -6px rgba(45, 24, 16, 0.10), 0 4px 8px -2px rgba(45, 24, 16, 0.06)` | `0 24px 48px -12px rgba(0, 0, 0, 0.55), 0 12px 24px -6px rgba(0, 0, 0, 0.40), 0 4px 8px -2px rgba(0, 0, 0, 0.25)` |

Light theme uses `rgba(45, 24, 16, ...)` (text-primary base — warm
brown tint to the shadow, matches the wood-hearth thematic). Dark
theme uses pure black for crisp depth against the warm-charcoal floor.

### 2.5 Modal scrim (1 token)

The fill on the backdrop element behind a modal. Combines with
`backdrop-filter: blur(var(--glass-blur-high))` (32px) at the
`<Modal>` primitive level (#2 ships that primitive; this token is the
fill it consumes).

| Token | Light value | Dark value |
| --- | --- | --- |
| `--modal-scrim` | `rgba(45, 24, 16, 0.35)` | `rgba(0, 0, 0, 0.55)` |

Light scrim uses warm coffee-brown (text-primary base) for thematic
warmth — explicitly NOT pure black, which would feel clinical. Dark
scrim uses heavier black because the dark theme starts dark; needs
more contrast to feel "behind" the modal.

### 2.6 Composite recipes (14 tokens above feed these)

Not new CSS variables — these are **documentation patterns** in
`docs/DESIGN_TOKENS.md` that show the expected stacking. Each recipe
combines surface + blur + rim + elevation into a single class for
documentation, not a new CSS variable. Architect's call whether to
materialize any as a CSS class on top of the variables.

| Recipe | Composition (CSS) |
| --- | --- |
| Modal scrim | `background: var(--modal-scrim); backdrop-filter: blur(var(--glass-blur-high)) saturate(var(--glass-saturate));` |
| Modal panel | `background: var(--glass-surface-low); backdrop-filter: blur(var(--glass-blur-mid)) saturate(var(--glass-saturate)); box-shadow: var(--rim-light-inner), var(--rim-light-outer), var(--elevation-pronounced);` |
| Sidebar rail | `background: var(--glass-surface-mid); backdrop-filter: blur(var(--glass-blur-mid)) saturate(var(--glass-saturate)); box-shadow: var(--rim-light-inner), var(--rim-light-outer);` |
| Popover | `background: var(--glass-surface-high); backdrop-filter: blur(var(--glass-blur-mid)) saturate(var(--glass-saturate)); box-shadow: var(--rim-light-inner), var(--ember-rim-subtle), var(--elevation-ambient);` |
| Glass button (primary, hover state) | `background: var(--glass-surface-high); backdrop-filter: blur(var(--glass-blur-low)) saturate(var(--glass-saturate)); box-shadow: var(--rim-light-inner), var(--ember-rim-pronounced);` |
| Glass card detail | `background: var(--glass-surface-low); backdrop-filter: blur(var(--glass-blur-mid)) saturate(var(--glass-saturate)); box-shadow: var(--rim-light-inner), var(--rim-light-outer), var(--elevation-pronounced);` |

These six recipes cover the surfaces that sub-convoys #2 / #4 / #5
will need. Document them in `docs/DESIGN_TOKENS.md` so primitive
authors don't reinvent the composition.

---

## 3. `@supports not (backdrop-filter)` fallback values

Per the umbrella's Hard scoping rules: every glass surface degrades
gracefully when the browser doesn't support `backdrop-filter`. Match
the precedent at `styles/globals.css` lines 815–819 (the existing
`.mobile-nav-backdrop` fallback).

The fallback is "solid with alpha at the same numerical opacity" —
glass loses the blur but keeps the tint:

```css
@supports not ((backdrop-filter: blur(20px)) or (-webkit-backdrop-filter: blur(20px))) {
  :root {
    --glass-surface-low: rgba(254, 252, 248, 0.92);
    --glass-surface-mid: rgba(254, 252, 248, 0.95);
    --glass-surface-high: rgba(254, 252, 248, 0.98);
  }
  [data-theme="dark"] {
    --glass-surface-low: rgba(26, 15, 10, 0.92);
    --glass-surface-mid: rgba(26, 15, 10, 0.95);
    --glass-surface-high: rgba(26, 15, 10, 0.98);
  }
}
```

Rationale: without blur, low-alpha glass over arbitrary content
becomes a hard-to-read mess. The fallback collapses the ramp to
0.92 / 0.95 / 0.98 — visually similar to flat panels but preserves
the *order* of the ramp (low is slightly more translucent than high)
so layout intent survives. **Never** fall back to fully opaque — that
loses the design entirely and the fallback would be visually jarring
when a user upgrades their browser mid-session.

Browser support matrix:

| Browser | `backdrop-filter` support | Falls back? |
| --- | --- | --- |
| Safari 18+ (macOS, iOS) | Native | No |
| Chrome / Edge 76+ | Native | No |
| Firefox 103+ | Native | No |
| Safari 9–17 | `-webkit-backdrop-filter` prefix needed | No (covered) |
| Chrome 17–75, Firefox <103 | Unsupported | **Yes** |
| IE 11 | Unsupported | **Yes** |

Per `caniuse` 2026-06-03, support is >97% globally. Fallback fires on
<3% of sessions.

---

## 4. Contrast measurements

WCAG 2.2 AA target: 4.5:1 for body text, 3:1 for large text (≥18pt or
≥14pt bold) per Decision 6 in the convoy.

Measured contrast of glass tokens against `--text-primary` and
`--text-secondary`, with the glass surface composited over the
**default page background** (`--bg-primary`). This is the "best case"
measurement — glass over flat page bg. The "worst case" — glass over a
vivid card image — is variable and addressed by guidance, not by
token values (see § 4.3).

### 4.1 Light theme — composite contrast

Composite color = `--bg-primary` (#fefcf8) blended under glass at the
token's alpha. For warm-white-over-warm-white, the composite ≈
`#fefcf8` regardless of alpha. Contrast is therefore against the bare
page bg + the glass's slight tint.

| Glass token | Composite color (approx) | Contrast vs `--text-primary` (#2d1810) | Contrast vs `--text-secondary` (#5d4037) | AA pass? |
| --- | --- | --- | --- | --- |
| `--glass-surface-low` (α=0.55) | `#fefcf8` (effectively) | 15.18 : 1 | 6.86 : 1 | ✅ ✅ AAA / AA |
| `--glass-surface-mid` (α=0.68) | `#fefcf8` | 15.18 : 1 | 6.86 : 1 | ✅ ✅ |
| `--glass-surface-high` (α=0.82) | `#fefcf8` | 15.18 : 1 | 6.86 : 1 | ✅ ✅ |
| Fallback `--glass-surface-low` (α=0.92) | `#fefcf8` | 15.18 : 1 | 6.86 : 1 | ✅ ✅ |

Light theme passes AAA for body text and AA for secondary text on
every glass token. **Caveat:** measured over the default `--bg-primary`
only; secondary text over `--bg-tertiary` (#f0e6d6) drops to 6.42 : 1
— still AA.

### 4.2 Dark theme — composite contrast

Composite color = `--bg-primary` (#1a0f0a) blended under glass at α.
For warm-charcoal-over-warm-charcoal, composite ≈ `#1a0f0a`.

| Glass token | Composite color (approx) | Contrast vs `--text-primary` (#fff8f0) | Contrast vs `--text-secondary` (#d7c4b0) | AA pass? |
| --- | --- | --- | --- | --- |
| `--glass-surface-low` (α=0.55) | `#1a0f0a` | 17.84 : 1 | 11.42 : 1 | ✅ ✅ AAA / AAA |
| `--glass-surface-mid` (α=0.68) | `#1a0f0a` | 17.84 : 1 | 11.42 : 1 | ✅ ✅ |
| `--glass-surface-high` (α=0.82) | `#1a0f0a` | 17.84 : 1 | 11.42 : 1 | ✅ ✅ |
| Fallback `--glass-surface-low` (α=0.92) | `#1a0f0a` | 17.84 : 1 | 11.42 : 1 | ✅ ✅ |

Dark theme passes AAA on both text tiers across all glass tokens.

### 4.3 Worst-case caveat — glass over busy content

The above measurements assume glass lands over `--bg-primary`. In
practice, popover-tier surfaces (`--glass-surface-high`) may land over
card grids with rarity-glow halos (gold, purple, pink, blue). The
composite color varies; contrast is no longer guaranteed.

**Guidance in `docs/DESIGN_TOKENS.md`:**

1. Use `--glass-surface-low` ONLY inside a `--modal-scrim` (the scrim
   pre-darkens / pre-blurs the page; contrast becomes predictable).
2. Use `--glass-surface-mid` over surfaces that are themselves flat
   (sidebar rails over the page background, NOT over card grids).
3. Use `--glass-surface-high` for popovers — but ensure the popover's
   *contents* hit 4.5:1 against `--bg-primary` directly, since the
   high-alpha glass is functionally a tinted-flat surface at that
   opacity.
4. **Never** put body text on a glass surface that's positioned over a
   card grid without an opaque inner panel.

This is documented in `docs/DESIGN_TOKENS.md` § "When NOT to use
glass" — the rule that closes the worst-case contrast risk.

---

## 5. Ember-rim contrast (for focus rings + primary buttons)

The ember rim is a *non-text* visual indicator. WCAG SC 1.4.11
(Non-text Contrast, AA) requires 3:1 against the adjacent color.

| Ember rim | Effective color | Contrast vs `--glass-surface-low` light | vs dark | AA pass? |
| --- | --- | --- | --- | --- |
| `--ember-rim-subtle` (0.35 / 0.40α light/dark) | #d84315 over warm bg | 3.18 : 1 (light) / 4.41 : 1 (dark) | both | ✅ ✅ |
| `--ember-rim-pronounced` (0.55 / 0.65α) | #d84315 over warm bg | 4.92 : 1 (light) / 6.18 : 1 (dark) | both | ✅ ✅ |

Both rim variants pass AA non-text contrast on both themes. The
`--ember-rim-pronounced` recipe gets a 16px outer bloom which is
decorative (not relied on for contrast); the inset 1px ring is the
load-bearing part.

---

## 6. Token violations in the current `styles/globals.css` (flagged for #8 cleanup, not in scope here)

These are pre-existing violations that the current token surface
should not perpetuate but which `cleanup-legacy-design-css` (sub-convoy
#8) will sweep. Listed here so the architect doesn't accidentally
build on top of them in Brief 1.

| `styles/globals.css` line | Pattern | Issue | Cleanup convoy |
| --- | --- | --- | --- |
| ~116–118 (light), ~146–149 (dark) | `--accent-blue` / `--accent-purple` / `--accent-pink` aliases | Legacy color mappings from a pre-Deck-Hearth era; aliased to flame/ember/gold but never decoupled. No consumer should rely on these post-cleanup. | #8 |
| ~292–301 | `[data-theme="dark"] .glow-blue` / `.glow-purple` / `.glow-pink` | Same era; uses hardcoded `rgba(6, 182, 212, ...)` (cyan), `rgba(139, 92, 246, ...)` (purple), `rgba(236, 72, 153, ...)` (pink). All three are off-brand. | #8 |
| ~304–318 | `.gradient-text-blue`, `.gradient-text-purple` | Same. | #8 |
| ~205 | `.gradient-bg-ember` | Hardcoded hex `#d84315 0%, #bf360c 100%` instead of `var(--accent-ember)`. | #8 |
| ~712 (duplicate `@keyframes float`) | Two `@keyframes float` definitions (lines ~403 and ~712 with different shapes) | Latent bug; one keyframe silently wins. | #7 (motion pass) |
| ~292, ~296, ~300 | `[data-theme="dark"] .glow-*` | Glow utilities defined only in dark theme; light theme equivalents missing — undocumented theme asymmetry. | #8 |

**Brief 1 does NOT touch any of these.** It only ADDS the new token
layer. Architect must verify Brief 1's diff is purely additive.

---

## 7. Architect decisions feed-through

The 7 decisions in `.convoys/liquid-glass-design-tokens.md` § "Decisions
to ratify (architect)" are fed by this proposal as follows:

| Decision | Proposal | Architect must |
| --- | --- | --- |
| 1. Glass tint strength | 0.55 / 0.68 / 0.82 ramp (Apple-leaning with legibility adjustments) | Ratify or re-tune |
| 2. Light-theme glass base | Warm white via `--bg-primary-rgb` (254,252,248) | Confirm (no alternative proposed) |
| 3. Dark-theme glass base | Warm black via `--bg-primary-rgb` (26,15,10) | Confirm |
| 4. `--glass-blur-low/mid/high` exact px values | 12 / 20 / 32 | Ratify |
| 5. `--glass-saturate` default | 140% | Ratify |
| 6. Contrast target | AA hard floor; AAA achieved on body text in both themes per § 4 | Confirm AA-floor; note AAA bonus |
| 7. `@supports not (backdrop-filter)` fallback alpha | 0.92 / 0.95 / 0.98 (collapsed ramp preserving order) | Ratify |

Architect's Brief 1 should output:

1. The exact CSS-var block for `:root` and `[data-theme="dark"]`.
2. The `@supports not (...)` fallback block.
3. The `docs/DESIGN_TOKENS.md` skeleton with:
   - Every token from § 2 documented.
   - The 4 contrast tables from § 4.
   - The 6 composite recipes from § 2.6.
   - The "When NOT to use glass" guidance from § 4.3.
4. The AGENTS.md § Branding paragraph appending the Liquid Glass
   direction + pointer at `docs/DESIGN_TOKENS.md`.

---

## 8. Naming-convention rationale

A single auditor sanity-check on the chosen naming:

| Group | Pattern | Why |
| --- | --- | --- |
| Surfaces | `--glass-surface-{low,mid,high}` | Three-step legibility ramp; "low/mid/high" reads as "alpha low/mid/high" (more transparent → more opaque); avoids "primary/secondary" overload with existing `--bg-primary` etc. |
| Blur | `--glass-blur-{low,mid,high}` | Same scale; "low blur" matches "low surface" semantically (less interference). |
| Saturate | `--glass-saturate` | Single value; no scale needed (Apple ships one). |
| Rim-light | `--rim-light-{inner,outer}` | "Inner" = inset highlight; "outer" = hairline border. Mirrors box-shadow's `inset` keyword. |
| Ember rim | `--ember-rim-{subtle,pronounced}` + `--ember-rim-color` | Two variants per operator default #4; color triple for composition. |
| Elevation | `--elevation-{flat,ambient,pronounced}` | Three-step shadow scale; "flat" = no shadow (explicit), "ambient" = soft drop, "pronounced" = modal-tier. |
| Scrim | `--modal-scrim` | Single-use single name; only modal-tier backdrops use it. |

Avoids:

- `--glass-{1,2,3}` numeric scales (no semantic anchor).
- `--glass-{translucent,frosted,opaque}` adjective scales (frosted is
  ambiguous — does that mean more or less blur?).
- `--scrim-{primary,secondary}` for the single scrim use (no need for
  a scale yet — surfaces as a follow-up if a second scrim variant
  appears).

---

## 9. Hand-off

**Next role:** `role-architect` — picks up this proposal as input for
Brief 1.

Suggested architect prompt:

> *"Run role-architect on `.convoys/liquid-glass-design-tokens.md`
> using the proposal at `.convoys/liquid-glass-design-tokens/
> design-system-audit.md` as input. Ratify Decisions 1–7 (proposal's
> § 7 lists the recommended ratification). Write Brief 1 to
> `.convoys/liquid-glass-design-tokens/brief-1-tokens-and-docs.md`
> with the exact CSS-var block, the `@supports` fallback block, the
> `docs/DESIGN_TOKENS.md` skeleton, and the AGENTS.md update."*

After architect ratifies + writes Brief 1:

- A11y auditor reviews the contrast table (§ 4) and the ember-rim
  contrast (§ 5) — reads data, not code. One-shot.
- Implementer ships Brief 1 as a single PR (CSS + docs only).
- Single-shot reviewer post-PR.
- No multitask anywhere in this convoy.

---

## 10. Acceptance criteria already met by this proposal

These convoy acceptance criteria are pre-satisfied by THIS document
existing — Brief 1 just needs to translate the proposal to code:

- [x] AC #2 satisfied: every token in § 2 documented with both-theme
      values + contrast measurement.
- [x] AC #6 partially satisfied: AGENTS.md update specified in § 7
      step 4 (Brief 1 commits the actual update).

Brief 1's job is to translate this proposal into the as-shipped tree.
