* 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>
506 lines
23 KiB
Markdown
506 lines
23 KiB
Markdown
---
|
||
convoy: liquid-glass-design-tokens
|
||
brief_number: 1
|
||
depends_on: []
|
||
files:
|
||
- styles/globals.css
|
||
- docs/DESIGN_TOKENS.md
|
||
- AGENTS.md
|
||
---
|
||
|
||
# Brief 1: Tokens and Docs
|
||
|
||
## Goal (1 sentence)
|
||
|
||
Add the canonical Liquid Glass CSS-variable layer to `styles/globals.css`
|
||
(both themes + `@supports` fallback), create `docs/DESIGN_TOKENS.md` as
|
||
the reference document, and append a `## Visual language` section to
|
||
`AGENTS.md` — touching zero `.js` files and shipping zero visual diff.
|
||
|
||
## Files in scope (do not edit anything else)
|
||
|
||
- `styles/globals.css` — modified. Adds **one** new well-delimited block
|
||
(29 new CSS variables) and **one** `@supports not (...)` fallback
|
||
block. Pre-existing rules untouched.
|
||
- `docs/DESIGN_TOKENS.md` — new. Reference doc per § "docs/DESIGN_TOKENS.md
|
||
skeleton" below.
|
||
- `AGENTS.md` — modified. Insert a new `## Visual language` section
|
||
between the existing `## Product vocabulary` (ends line ~27) and
|
||
`## 1. Project overview` (line 29).
|
||
|
||
**Out of scope:** any file in `components/**`, `pages/**`, `lib/**`,
|
||
`test/**`. Any change to existing tokens / utility classes / keyframes
|
||
in `styles/globals.css`. Any deletion. See § "Anti-scope" below.
|
||
|
||
## Conventions to follow
|
||
|
||
- **Theme-token discipline** (`.cursor/rules/ui-and-theming.mdc`): no
|
||
hardcoded hex in `.js` files — N/A here (no `.js` touched), but the
|
||
new tokens themselves embed hex inside `rgba()` triples; that is the
|
||
canonical pattern, see `styles/globals.css` lines 25–35 for precedent.
|
||
- **Theme symmetry** (`.cursor/rules/ui-and-theming.mdc`): every new
|
||
variable that's color-dependent MUST get a value in BOTH `:root` and
|
||
`[data-theme="dark"]`. Theme-independent values (blur px, saturate
|
||
percentage) live only in `:root` and are inherited.
|
||
- **No-go zones** (`.cursor/rules/no-go-zones.mdc`): `styles/globals.css`
|
||
is NOT a no-go zone; safe to edit. `AGENTS.md` is the canonical agent
|
||
contract — edits go through the documented sections.
|
||
- **JavaScript-only repo** (`AGENTS.md` Gotcha #9): do not introduce
|
||
any `.ts` / `.tsx` file. N/A here (no `.js` either).
|
||
|
||
## Verbatim CSS to add to `styles/globals.css`
|
||
|
||
**Insertion point**: append AFTER the existing
|
||
`[data-theme="dark"] { ... }` block that ends at line 74 (the block
|
||
that defines `--bg-primary-dark` through `--accent-ember-rgb`) and
|
||
BEFORE the existing `/* Apply theme colors */` block at line 76. This
|
||
keeps the token-definition section contiguous.
|
||
|
||
Add a clear comment fence so the block is greppable + recognizable:
|
||
|
||
```css
|
||
/* ============================================================
|
||
Liquid Glass tokens — added by liquid-glass-design-tokens convoy
|
||
(2026-06-03). See docs/DESIGN_TOKENS.md for the full reference,
|
||
contrast tables, composite recipes, and "When NOT to use glass"
|
||
guidance.
|
||
============================================================ */
|
||
|
||
:root {
|
||
/* Glass surfaces (light) — 3-step legibility ramp.
|
||
low: modal panels inside a scrim, card detail, inline sub-panels.
|
||
mid: sidebar rail, header strip, mobile bottom-bar.
|
||
high: popovers, dropdowns, tooltips (can land over anything). */
|
||
--glass-surface-low: rgba(254, 252, 248, 0.55);
|
||
--glass-surface-mid: rgba(254, 252, 248, 0.68);
|
||
--glass-surface-high: rgba(254, 252, 248, 0.82);
|
||
|
||
/* Glass blur + saturate (theme-independent; inherited by dark). */
|
||
--glass-blur-low: 12px;
|
||
--glass-blur-mid: 20px;
|
||
--glass-blur-high: 32px;
|
||
--glass-saturate: 140%;
|
||
|
||
/* Rim-light (light) — inner highlight + outer hairline. */
|
||
--rim-light-inner: inset 0 1px 0 0 rgba(255, 255, 255, 0.65);
|
||
--rim-light-outer: 0 0 0 1px rgba(45, 24, 16, 0.08);
|
||
|
||
/* Ember rim — composable RGB triple + two preset variants.
|
||
Triple is theme-independent (ember orange #d84315);
|
||
variants differ per theme for eye-perception correction. */
|
||
--ember-rim-color: 216, 67, 21;
|
||
--ember-rim-subtle: inset 0 0 0 1px rgba(216, 67, 21, 0.35);
|
||
--ember-rim-pronounced:
|
||
inset 0 0 0 1px rgba(216, 67, 21, 0.55),
|
||
0 0 16px 0 rgba(216, 67, 21, 0.30);
|
||
|
||
/* Elevation (light) — warm-brown-tinted shadows. */
|
||
--elevation-flat: none;
|
||
--elevation-ambient:
|
||
0 4px 12px -2px rgba(45, 24, 16, 0.08),
|
||
0 2px 4px -1px rgba(45, 24, 16, 0.04);
|
||
--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);
|
||
|
||
/* Modal scrim (light) — warm coffee-brown, NOT pure black. */
|
||
--modal-scrim: rgba(45, 24, 16, 0.35);
|
||
}
|
||
|
||
[data-theme="dark"] {
|
||
/* Glass surfaces (dark) — same alpha ramp over warm-charcoal base. */
|
||
--glass-surface-low: rgba(26, 15, 10, 0.55);
|
||
--glass-surface-mid: rgba(26, 15, 10, 0.68);
|
||
--glass-surface-high: rgba(26, 15, 10, 0.82);
|
||
|
||
/* Rim-light (dark) — softer warm-white inner + faint outer. */
|
||
--rim-light-inner: inset 0 1px 0 0 rgba(255, 248, 240, 0.12);
|
||
--rim-light-outer: 0 0 0 1px rgba(255, 248, 240, 0.06);
|
||
|
||
/* Ember rim (dark) — alpha bumped to compensate for ember orange
|
||
reading less vibrant on dark backgrounds (eye-perception correction,
|
||
not a numerical drift). RGB triple inherits from :root. */
|
||
--ember-rim-subtle: inset 0 0 0 1px rgba(216, 67, 21, 0.40);
|
||
--ember-rim-pronounced:
|
||
inset 0 0 0 1px rgba(216, 67, 21, 0.65),
|
||
0 0 16px 0 rgba(216, 67, 21, 0.35);
|
||
|
||
/* Elevation (dark) — pure-black shadows for crisp depth against
|
||
the warm-charcoal floor. */
|
||
--elevation-ambient:
|
||
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(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);
|
||
|
||
/* Modal scrim (dark) — heavier black; dark theme starts dark so
|
||
needs more contrast to feel "behind" the modal. */
|
||
--modal-scrim: rgba(0, 0, 0, 0.55);
|
||
}
|
||
|
||
/* Fallback for browsers without backdrop-filter support (<3% of
|
||
sessions per caniuse 2026-06-03). Collapses the alpha ramp toward
|
||
solid so glass surfaces remain legible without the blur layer.
|
||
Never goes fully opaque — preserves the design's tinted-surface
|
||
intent and the ramp ordering. The @supports negation guards both
|
||
the unprefixed property AND -webkit-backdrop-filter (Safari 9-17
|
||
needed the prefix). */
|
||
@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);
|
||
}
|
||
}
|
||
```
|
||
|
||
**Sanity check** before committing: the new block is purely **additive**.
|
||
No existing line in `styles/globals.css` should be deleted or modified.
|
||
`git diff styles/globals.css` should show only insertions in this block's
|
||
region.
|
||
|
||
## `docs/DESIGN_TOKENS.md` — full content
|
||
|
||
Create this file at `docs/DESIGN_TOKENS.md` (the `docs/` directory
|
||
already exists per repo layout). Use the exact content below:
|
||
|
||
````markdown
|
||
# Design tokens — Deck Hearth
|
||
|
||
Reference for the design-token surface. The canonical product brand is
|
||
**Deck Hearth** (post-`pick-a-name` PR #21, 2026-05-24); the visual
|
||
direction is **Liquid Glass** (post-`liquid-glass-design-tokens` PR
|
||
TBD, 2026-06-03 — see `.convoys/liquid-glass-redesign.md`).
|
||
|
||
Every color token is defined in `styles/globals.css` and consumed via
|
||
`var(--token-name)`. **Do not** hardcode hex in `.js` files; the post-
|
||
`cleanup-legacy-design-css` `forbidden-hex-in-jsx` lint gate will fail
|
||
the build (see `.convoys/cleanup-legacy-design-css.md` for the planned
|
||
gate).
|
||
|
||
## Layer overview
|
||
|
||
| Layer | Purpose | Tokens |
|
||
| --- | --- | --- |
|
||
| Base palette | Theme-tuned background + text + accent colors | `--bg-*`, `--text-*`, `--accent-*`, `--border` — pre-existing |
|
||
| Surface (glass) | Translucent panel fills | `--glass-surface-{low,mid,high}` |
|
||
| Blur + saturate | `backdrop-filter` ingredients | `--glass-blur-{low,mid,high}`, `--glass-saturate` |
|
||
| Rim-light | Edge definition on glass surfaces | `--rim-light-{inner,outer}` |
|
||
| Ember rim | Brand-accent rings for interactive primaries | `--ember-rim-color` (RGB triple), `--ember-rim-{subtle,pronounced}` |
|
||
| Elevation | Shadow stacks | `--elevation-{flat,ambient,pronounced}` |
|
||
| Modal scrim | Backdrop fill behind modals | `--modal-scrim` |
|
||
|
||
## Surface tokens
|
||
|
||
Three-step legibility ramp. Higher number = more opaque.
|
||
|
||
| Token | Light value | Dark value | Use |
|
||
| --- | --- | --- | --- |
|
||
| `--glass-surface-low` | `rgba(254, 252, 248, 0.55)` | `rgba(26, 15, 10, 0.55)` | Modal panels (inside a scrim), card detail, inline glass sub-panels |
|
||
| `--glass-surface-mid` | `rgba(254, 252, 248, 0.68)` | `rgba(26, 15, 10, 0.68)` | Sidebar rail, header strip, mobile bottom-bar |
|
||
| `--glass-surface-high` | `rgba(254, 252, 248, 0.82)` | `rgba(26, 15, 10, 0.82)` | Popovers, dropdowns, tooltips |
|
||
|
||
Always paired with `backdrop-filter: blur(...) saturate(var(--glass-saturate))`.
|
||
|
||
## Blur + saturate tokens
|
||
|
||
| Token | Value | Use |
|
||
| --- | --- | --- |
|
||
| `--glass-blur-low` | `12px` | Inline glass panels with adjacent flat surfaces |
|
||
| `--glass-blur-mid` | `20px` | Default panel + nav |
|
||
| `--glass-blur-high` | `32px` | Modal scrim — heavy, immersive |
|
||
| `--glass-saturate` | `140%` | Vibrancy boost on all glass surfaces |
|
||
|
||
Theme-independent (same value in both themes).
|
||
|
||
## Rim-light 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)` | 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 |
|
||
|
||
Consumed inside `box-shadow:` lists, typically together: `box-shadow:
|
||
var(--rim-light-inner), var(--rim-light-outer), var(--elevation-ambient);`.
|
||
|
||
## Ember rim tokens
|
||
|
||
Brand-accent rings. Pronounced on interactive primaries; subtle on
|
||
ambient surfaces.
|
||
|
||
| Token | Light value | Dark value | Use |
|
||
| --- | --- | --- | --- |
|
||
| `--ember-rim-color` | `216, 67, 21` (RGB triple) | same | Composable: `rgba(var(--ember-rim-color), 0.42)` for custom alphas |
|
||
| `--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)` | Ambient surfaces (nav rail rest state, header) |
|
||
| `--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)` | Interactive primaries (buttons, focused inputs, selected cards) |
|
||
|
||
Dark theme rim alphas are slightly higher to compensate for ember orange
|
||
reading less vibrant on dark backgrounds (eye-perception correction).
|
||
|
||
## Elevation tokens
|
||
|
||
| Token | Light value | Dark value | Use |
|
||
| --- | --- | --- | --- |
|
||
| `--elevation-flat` | `none` | `none` | Explicit "no shadow" (inline glass sub-panels) |
|
||
| `--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)` | Default elevated surfaces (popovers, sidebar) |
|
||
| `--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)` | Modal panels, card detail full view |
|
||
|
||
Light theme uses warm-brown-tinted shadows (`rgba(45, 24, 16, ...)`)
|
||
for thematic consistency. Dark theme uses pure black for crisp depth.
|
||
|
||
## Modal scrim token
|
||
|
||
| Token | Light value | Dark value |
|
||
| --- | --- | --- |
|
||
| `--modal-scrim` | `rgba(45, 24, 16, 0.35)` | `rgba(0, 0, 0, 0.55)` |
|
||
|
||
Combined with `backdrop-filter: blur(var(--glass-blur-high))` (32px)
|
||
at the `<Modal>` primitive level (sub-convoy #2 ships that primitive).
|
||
|
||
## Composite recipes
|
||
|
||
Six common compositions. These are documentation patterns — they're
|
||
NOT new CSS variables. Primitive authors compose them as shown.
|
||
|
||
| Recipe | CSS composition |
|
||
| --- | --- |
|
||
| 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) | `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);` |
|
||
|
||
## Contrast measurements (WCAG 2.2 AA target)
|
||
|
||
Glass surfaces composited over the **default** `--bg-primary` (best
|
||
case). For the "glass over busy card art" worst case, see § "When NOT
|
||
to use glass" below.
|
||
|
||
### Light theme
|
||
|
||
| Glass token | vs `--text-primary` (#2d1810) | vs `--text-secondary` (#5d4037) | AA pass? |
|
||
| --- | --- | --- | --- |
|
||
| `--glass-surface-low` (α=0.55) | 15.18 : 1 | 6.86 : 1 | ✅ ✅ AAA / AA |
|
||
| `--glass-surface-mid` (α=0.68) | 15.18 : 1 | 6.86 : 1 | ✅ ✅ |
|
||
| `--glass-surface-high` (α=0.82) | 15.18 : 1 | 6.86 : 1 | ✅ ✅ |
|
||
| Fallback (α=0.92) | 15.18 : 1 | 6.86 : 1 | ✅ ✅ |
|
||
|
||
### Dark theme
|
||
|
||
| Glass token | vs `--text-primary` (#fff8f0) | vs `--text-secondary` (#d7c4b0) | AA pass? |
|
||
| --- | --- | --- | --- |
|
||
| `--glass-surface-low` (α=0.55) | 17.84 : 1 | 11.42 : 1 | ✅ ✅ AAA / AAA |
|
||
| `--glass-surface-mid` (α=0.68) | 17.84 : 1 | 11.42 : 1 | ✅ ✅ |
|
||
| `--glass-surface-high` (α=0.82) | 17.84 : 1 | 11.42 : 1 | ✅ ✅ |
|
||
| Fallback (α=0.92) | 17.84 : 1 | 11.42 : 1 | ✅ ✅ |
|
||
|
||
### Ember rim (WCAG SC 1.4.11 — non-text contrast, AA = 3:1)
|
||
|
||
| Rim token | Light contrast | Dark contrast | AA pass? |
|
||
| --- | --- | --- | --- |
|
||
| `--ember-rim-subtle` | 3.18 : 1 | 4.41 : 1 | ✅ ✅ |
|
||
| `--ember-rim-pronounced` | 4.92 : 1 | 6.18 : 1 | ✅ ✅ |
|
||
|
||
## When NOT to use glass
|
||
|
||
The contrast measurements above assume glass over `--bg-primary`.
|
||
Glass over arbitrary card grids or vibrant card images is not
|
||
guaranteed-legible. Four rules:
|
||
|
||
1. **`--glass-surface-low`** — ONLY use inside a `--modal-scrim` (the
|
||
scrim pre-darkens / pre-blurs the page; contrast becomes predictable).
|
||
2. **`--glass-surface-mid`** — Use over surfaces that are themselves
|
||
flat (sidebar rails over the page background; NOT over card grids).
|
||
3. **`--glass-surface-high`** — Use for popovers, but ensure the popover's
|
||
contents would hit 4.5:1 against `--bg-primary` directly. At 0.82α
|
||
the surface is functionally a tinted-flat panel.
|
||
4. **Never** place body text on a glass surface positioned over a card
|
||
grid without an opaque inner panel.
|
||
|
||
## Per-card grid performance budget
|
||
|
||
`backdrop-filter` is GPU-expensive. Stacked instances on long card
|
||
grids hurt scroll performance.
|
||
|
||
- **Card grid items** (`components/CardItem.js`): NO `backdrop-filter`.
|
||
Solid `--bg-secondary` background + cheap shadow + rarity glow.
|
||
- **Card grid containers** (the wrapping panel): MAY use glass.
|
||
- **Card detail view** (`components/CardDetailView.js`): full glass.
|
||
- **Card3D hover preview**: keeps 3D transform; uses
|
||
`--elevation-pronounced` + `--ember-rim-pronounced`; no
|
||
`backdrop-filter`.
|
||
|
||
This is the contract sub-convoy #5 (`liquid-glass-card-surfaces`)
|
||
implements. See `.convoys/liquid-glass-card-surfaces.md` for detail.
|
||
|
||
## Browser support + fallback
|
||
|
||
`backdrop-filter` is supported in all evergreen browsers:
|
||
|
||
| Browser | Support |
|
||
| --- | --- |
|
||
| Safari 18+ (macOS, iOS) | Native |
|
||
| Chrome / Edge 76+ | Native |
|
||
| Firefox 103+ | Native |
|
||
| Safari 9–17 | `-webkit-backdrop-filter` prefix needed |
|
||
| Chrome 17–75, Firefox <103, IE 11 | **Unsupported — fallback fires** |
|
||
|
||
Global support >97% (caniuse 2026-06-03). Fallback fires on <3% of
|
||
sessions and collapses the surface ramp to 0.92 / 0.95 / 0.98 — visually
|
||
similar to flat panels but preserves ramp ordering.
|
||
|
||
## Reduced motion
|
||
|
||
This document does not document animations — those are governed by
|
||
`docs/MOTION_SYSTEM.md` (shipped by sub-convoy #7,
|
||
`.convoys/motion-system-pass.md`). When that doc lands, it MUST
|
||
respect `@media (prefers-reduced-motion: reduce)` for every animation.
|
||
|
||
## Deprecations
|
||
|
||
- **`fire-glow-bg`** (page-level background animation in
|
||
`styles/globals.css` lines ~755–757) — scheduled for deletion by
|
||
`motion-system-pass` (sub-convoy #7). Replacement: localized
|
||
`ember-float` accent on landing hero only. Do NOT consume
|
||
`fire-glow-bg` in new code.
|
||
- **`--accent-blue` / `--accent-purple` / `--accent-pink` aliases**
|
||
(`styles/globals.css` lines ~116–118, ~146–149) — scheduled for
|
||
deletion by `cleanup-legacy-design-css` (sub-convoy #8). They alias
|
||
flame/ember/gold and are pre-Deck-Hearth-era debt. Do NOT consume
|
||
in new code; use the canonical `--accent-flame` / `--accent-ember`
|
||
/ `--accent-gold` directly.
|
||
- **`.gradient-text-blue` / `.gradient-text-purple` / `.glow-blue` /
|
||
`.glow-purple` / `.glow-pink`** — same; deletion in #8.
|
||
- **`.fire-glow` / `.ember-glow`** — utility classes superseded by
|
||
`--ember-rim-{subtle,pronounced}`. Deletion in #8.
|
||
|
||
## Related convoys
|
||
|
||
- `.convoys/liquid-glass-redesign.md` — umbrella epic.
|
||
- `.convoys/liquid-glass-design-tokens.md` — this token surface.
|
||
- `.convoys/liquid-glass-modal-and-surface-primitive.md` — `<GlassSurface>` + `<Modal>` primitives consuming these tokens.
|
||
- `.convoys/cleanup-legacy-design-css.md` — sweeps deprecations.
|
||
- `.convoys/motion-system-pass.md` — motion taxonomy (parallel scope).
|
||
````
|
||
|
||
## AGENTS.md update
|
||
|
||
Insert this exact section between `## Product vocabulary` (current last
|
||
line: ~27) and `## 1. Project overview` (current line: 29). The new
|
||
section becomes a sibling to "Product vocabulary" — a documentation
|
||
home for visual-system guidance:
|
||
|
||
```markdown
|
||
## Visual language
|
||
|
||
Deck Hearth's visual direction is **Liquid Glass** (in-progress as of
|
||
2026-06-03 — see `.convoys/liquid-glass-redesign.md` umbrella). Every
|
||
translucent surface (modals, sidebar, header, popovers, card detail)
|
||
composes the canonical token surface defined in `styles/globals.css`
|
||
and documented in [`docs/DESIGN_TOKENS.md`](docs/DESIGN_TOKENS.md).
|
||
**Do not** hardcode hex in `.js` files; the post-cleanup
|
||
`forbidden-hex-in-jsx` gate (sub-convoy #8) will fail the build.
|
||
|
||
Three rules of thumb:
|
||
|
||
- **Surfaces are glass.** Modal panels, sidebars, dropdowns, and the
|
||
header strip use `--glass-surface-{low,mid,high}` + `backdrop-filter`
|
||
composition recipes from `docs/DESIGN_TOKENS.md` § "Composite recipes".
|
||
- **Brand warmth is accent, not panel fill.** Ember (`#d84315`), flame
|
||
(`#ff6f00`), and gold (`#ffab40`) read as light cast onto glass — via
|
||
`--ember-rim-{subtle,pronounced}` rings, focus glow, and gradient
|
||
buttons. They are **NOT** the canonical panel-background color.
|
||
- **No `backdrop-filter` on card grid items.** GPU budget — glass goes
|
||
on grid containers and detail views, not per-card. See
|
||
`docs/DESIGN_TOKENS.md` § "Per-card grid performance budget".
|
||
|
||
```
|
||
|
||
## Acceptance criteria
|
||
|
||
- [ ] `styles/globals.css` contains the verbatim CSS block above —
|
||
added in a comment-fenced region between the existing dark-theme
|
||
`]` (line 74) and the `/* Apply theme colors */` comment (line 76).
|
||
- [ ] `styles/globals.css` `git diff` shows **only insertions** — no
|
||
existing line is modified or deleted.
|
||
- [ ] `docs/DESIGN_TOKENS.md` exists at `docs/DESIGN_TOKENS.md` with
|
||
the full content above.
|
||
- [ ] `AGENTS.md` has a new `## Visual language` section between
|
||
`## Product vocabulary` and `## 1. Project overview` (per
|
||
§ "AGENTS.md update").
|
||
- [ ] **No `.js` file is modified.** Verify with `git diff --stat
|
||
'*.js'` returning empty.
|
||
- [ ] **No existing CSS rule is modified.** Verify the diff against
|
||
`styles/globals.css` shows only additive ranges.
|
||
- [ ] `npm run lint` exits 0.
|
||
- [ ] `npm run test:run` exits 0 (vitest 21/21).
|
||
- [ ] `npm run test:smoke` (against the preview) exits 0 (3/3).
|
||
- [ ] `Screenshot diff` workflow on the PR shows **zero or
|
||
trivially-noisy** diff — no rendered pixel should change because
|
||
no consumer of the new tokens is added. Architect must explain any
|
||
non-trivial diff before merge.
|
||
- [ ] tests added: N/A (CSS-vars + docs only; no functional surface to
|
||
unit-test in this brief).
|
||
- [ ] no scope expansion: no file edited outside `files:` in the
|
||
frontmatter.
|
||
|
||
## Anti-scope (must not do)
|
||
|
||
- ❌ Touch any `.js` file under `components/**`, `pages/**`, `lib/**`,
|
||
`test/**`.
|
||
- ❌ Delete or modify any existing CSS variable, utility class, or
|
||
keyframe in `styles/globals.css`. The cleanup is sub-convoy #8.
|
||
- ❌ Wire `tailwind.config.js` to the new tokens. Pure CSS-var surface
|
||
for v1; the Tailwind extension is a queued follow-up.
|
||
- ❌ Create `components/ui/` — that's sub-convoys #2 + #3.
|
||
- ❌ Create `docs/MOTION_SYSTEM.md` — that's sub-convoy #7.
|
||
- ❌ Add a new keyframe / animation. Motion work is sub-convoy #7.
|
||
- ❌ Add a CI grep gate. The `forbidden-*-css` gates land in #8.
|
||
|
||
## Verification commands (for the implementer to run pre-PR)
|
||
|
||
```bash
|
||
# 1. Confirm only the three in-scope files changed
|
||
git diff --stat -- 'components/**' 'pages/**' 'lib/**' 'test/**' '*.js' '*.jsx'
|
||
# Expected: empty (no .js / .jsx changes)
|
||
|
||
git diff --stat
|
||
# Expected: 3 files: styles/globals.css, docs/DESIGN_TOKENS.md, AGENTS.md
|
||
|
||
# 2. Confirm the styles/globals.css diff is purely additive (no deletions)
|
||
git diff styles/globals.css | grep -E '^-[^-]' | head
|
||
# Expected: empty (only the leading 3-dash --- header lines, no deletions)
|
||
|
||
# 3. Confirm no consumer of the new tokens was added (would be scope creep)
|
||
git diff -- 'components/**' 'pages/**' | grep -E 'var\(--glass-|var\(--rim-light|var\(--ember-rim|var\(--elevation|var\(--modal-scrim'
|
||
# Expected: empty
|
||
|
||
# 4. Lint + test + smoke
|
||
npm run lint
|
||
npm run test:run
|
||
# Smoke needs the preview URL — your call whether to run locally vs let CI handle.
|
||
```
|
||
|
||
## Rationale (≤3 sentences)
|
||
|
||
This brief lands the token foundation for the Liquid Glass redesign
|
||
umbrella as a purely additive, zero-rendered-change PR — the most
|
||
conservative possible foundation move, designed to let downstream
|
||
sub-convoys (#2 modals, #3 forms, #4 layout, #5 cards, #6 public,
|
||
#7 motion) consume a stable, documented vocabulary. The "boot-the-brief"
|
||
shape was verified against the audit's contrast measurements and the
|
||
existing `styles/globals.css` line topology; no dep changes, no .js
|
||
touched, no existing rule deleted. Visual diff is expected to be empty
|
||
because no consumer of the new tokens is added in this brief — any
|
||
pixel-level diff is a bug and must be explained before merge.
|