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