deckhearth/.convoys/liquid-glass-design-tokens/design-system-audit.md
varutasu 334612ad79
feat(design-system): Liquid Glass redesign portfolio — foundation + primitives + Layout (#95)
* feat(design-system): Liquid Glass redesign portfolio — foundation + primitive kit + Layout shell

Operator-requested epic to migrate the UI from the current "warm panel + side-highlight + heavy gradient" visual language to a Liquid Glass aesthetic that retains Deck Hearth's fireplace warmth as accent / gradient / motion (not as panel fill). This squash carries the full 8-convoy portfolio drive-through; 5 sub-convoys reach merged state, 3 land architecture-only and queue impl for follow-up turns gated on dedicated visual-diff baseline re-seeds.

Sub-convoy #1 (liquid-glass-design-tokens) — MERGED. 29 CSS custom properties: glass-surface {low,mid,high} alpha ramp + blur/saturate + rim-light (inner/outer) + ember-rim (subtle/pronounced; RGB triple) + 3-tier elevation + modal-scrim, both light + dark themes with eye-perception-corrected alphas; @supports not (backdrop-filter) fallback collapsing surfaces toward solid (preserves ramp ordering). Authored docs/DESIGN_TOKENS.md (270 LOC reference with WCAG AA contrast tables, composite recipes, when-NOT-to-use-glass guidance, per-card grid GPU budget). AGENTS.md gains a § Visual language section as the new agent-contract surface.

Sub-convoy #2 (liquid-glass-modal-and-surface-primitive) — Brief 1 MERGED. Adds <GlassSurface> (forwardRef composable; tint / rim / elevation / blur props) and <Modal> primitive (focus-trap, ESC + backdrop close, body-scroll lock, ARIA dialog shape, built-in close button) consuming the token surface. lib/use-focus-trap.js — homegrown hook (~60 LOC, no dep). 10 new vitest cases covering open/close render, ARIA, ESC + closeOnEsc gate, backdrop gate, hideCloseButton, body-scroll lock + restore. 4 reference modal migrations as proof-of-pattern: ShareModal, CollectionDeleteModal, CollectionsCreateModal, CardDetailQuantityModal. Brief 2 (11 remaining modals) queued; CI grandfather list locks the pattern in.

Sub-convoy #3 (liquid-glass-form-primitives) — Brief 1 MERGED. Adds <Button> (primary ember-gradient with ember-rim-pronounced; secondary glass-mid; danger; ghost), <Input> (glass-high with ember focus ring + label + helperText + error + aria-invalid + describedby wiring + leadingIcon decorative + trailingAction interactive), <SearchBar> (composes Input with leading search icon + conditional clear button). 10 new vitest cases. pages/login.js + pages/signup.js fully migrated — 2 submit buttons + 7 inputs total; existing test/pages/login.test.js assertion ("Sign in to Deck Hearth" button text) preserved. Brief 2 (profile/settings + deck-builder + scanner + card-editor + collection-cluster modal forms) queued.

Sub-convoy #4 (liquid-glass-layout-shell) — MERGED. 6 shell surfaces glass-migrated: desktop sidebar rail (glass-mid + rim + ambient elevation), mobile drawer (glass-mid + pronounced elevation), mobile overlay scrim (modal-scrim + blur-high — visually consistent with <Modal>), search header strip (glass-mid + rim), UserProfileDropdown popover (glass-high + ember-rim-subtle + ambient — matches popover recipe), MobileNavigation bottom bar (replaces legacy mobile-nav-backdrop class). The 5 Layout regression-lock tests (logged-out CTA, no maintainer-email default, "Sign in" link present, supplied email renders, no "Guest" placeholder) all still pass — every edit preserved the documented contract.

Sub-convoy #5 (liquid-glass-card-surfaces) — ARCHITECTURE RATIFIED; implementation queued. Pixel-sensitive (rarity-glow reconciliation) so wants a dedicated visual-diff baseline re-seed PR. Pre-blocked on a fix-card3d-state convoy (Card3D has pre-existing state-management bug: state setters used without useState declarations).

Sub-convoy #6 (liquid-glass-public-and-auth) — ARCHITECTURE RATIFIED; partial impl shipped via #3 (login + signup form primitives migrated). Landing page editorial + public collection/deck views + login/signup outer-wrapper sweep queued.

Sub-convoy #7 (motion-system-pass) — MERGED. 8 motion tokens (5-tier duration taxonomy: instant/quick/default/slow/deliberate; 3 easings: ease-out default, spring for delight, linear for progress) added to the token surface. prefers-reduced-motion upgraded from a narrow nav-item rule to a site-wide universal sweep collapsing animation-duration + transition-duration to 0.01ms (preserves end states, no flicker); .motion-essential class is the opt-in escape hatch for state-meaningful animation (loading spinners, scan reticles). Authored docs/MOTION_SYSTEM.md with WCAG SC 2.3.3 contract, composition recipes, audit of existing keyframes, and adding-new-animation checklist.

Sub-convoy #8 (cleanup-legacy-design-css) — Brief 1 MERGED. Two new CI jobs in .github/workflows/ci.yml: (1) forbidden-modal-shell-without-primitive (BLOCKING) — fails build if any new file outside the 9 grandfathered legacy modals uses the fixed inset-0 bg-black bg-opacity- shell pattern; locks in the discipline that every modal must compose <Modal> from components/ui. (2) forbidden-deprecated-color-aliases (WARN-only) — audits pre-Deck-Hearth blue/purple/pink aliases (gradient-text-purple/pink/blue, glow-purple/pink/blue, gradient-bg-purple/blue/pink) as a baseline; graduates to FAIL after #8 Brief 2 sweeps consumers. .cursor/rules/ui-and-theming.mdc updated to document the components/ui/ primitive kit and point at the new canonical reference modals.

Verification: lint 0 errors (2 pre-existing warnings in unrelated CardEditorForm.js + CollectionsPageView.js — out of scope); vitest 104/104 passing (was 84 — +20 from new primitive tests: 10 Modal + 10 ui-primitives); ci.yml valid YAML; both new CI gates locally exercised and pass on the current tree.

Operator follow-ups documented in .convoys/ship-readiness.md § "Design-system redesign portfolio":
- Re-seed Linux visual-diff baselines via Docker workflow (AGENTS.md § 6) after this merges.
- preview-smoke.yml runs against the preview; auth + scanner specs touch the migrated surfaces.
- Vercel promote to production once smoke + visual gates pass.
- Queued follow-up implementer turns: #2 Brief 2 (11 modals), #3 Brief 2 (other forms), #5 Brief 1 (cards, after fix-card3d-state), #6 Brief 1 (landing editorial), #8 Brief 2 (legacy CSS deletion + WARN→FAIL graduation).

The user-visible promise — "modern fireplace aesthetic; modals blur the page behind them; reusable components" — is delivered TODAY by the merged work.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(use-focus-trap): preserve named useFocusTrap export for ScannerPageView

The portfolio squash inadvertently overwrote the pre-existing
lib/use-focus-trap.js (named `export function useFocusTrap(active)`
returning a ref — used by ScannerPageView, line 21) with a default-
only export shaped for the new `<Modal>` primitive. Vercel build
failed: "Export useFocusTrap doesn't exist in target module".

Fix: the file now exports BOTH —
- `useFocusTrap(active)` (named, original) — returns a ref;
  pre-Liquid-Glass call sites (ScannerPageView) keep working.
- `useFocusTrapContainer({ active, containerRef, ... })` (default,
  new) — takes a caller-owned ref so panel refs can forward through
  forwardRef chains (Modal.js consumes this shape).

Both hooks are commented to document which to use when. Modal.js
imports default already, so no change needed there.

Verified: npm run build passes (was failing in CI); lint 0 errors;
vitest 104/104 still green.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 20:12:33 -05:00

412 lines
21 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 3040% 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 815819 (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 917 | `-webkit-backdrop-filter` prefix needed | No (covered) |
| Chrome 1775, 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 |
| --- | --- | --- | --- |
| ~116118 (light), ~146149 (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 |
| ~292301 | `[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 |
| ~304318 | `.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 17 (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.