--- name: unify-glass-panel-surfaces classification: feature success_metric: | Every panel-shaped surface in the app (modals, popovers, form cards, dashboard widgets, page-level content cards) renders with the same gradient-border corner-light treatment that the floating chrome chips use — at the appropriate intensity tier for its surface class (full intensity for chrome, subtle for data cards, flat for opaque GPU-budget-constrained tiles like CardItem grid). Verified by visual diff + a forbidden grep gate that prevents reintroduction of bespoke `var(--glass-surface-*)` inline styles outside the documented exception list. skip: - ia status: open created: 2026-06-04 depends_on: - tone-down-card-corner-lights # PR #118 — establishes the subtle token tier - design-sweep-pass # PR #117 — applies .glass-panel broadly --- # Convoy: unify-glass-panel-surfaces Follow-on to the `redesign-v2-from-mockups` umbrella and the 2026-06-04 corner-border-light refinement (PR #116) + design-sweep (PR #117) + card-vibrancy reduction (PR #118). ## Why The gradient-border corner-light pattern (introduced in PR #116 and applied broadly in PR #117) is now the canonical surface treatment for the app. But the audit run on 2026-06-04 found that several panel-shaped surfaces still use the previous generation's "flat translucent fill" pattern — they predate the corner-light work and were never migrated: 1. **`` primitive** — the most consequential gap. `components/ui/GlassSurface.js` uses a single flat `var(--glass-surface-${tint})` background with no transparent border and no corner radials. Because `` (and therefore every modal in the app), `` (dashboard stat tiles), and the public landing-page feature/collection cards all delegate to ``, the gap cascades broadly. Upgrading the primitive fixes ~10 visible surfaces in one move. 2. **Auth form cards** (`pages/login.js` L82–88, `pages/signup.js` L227–232) — handrolled glass imitation using `rgba(--bg-secondary-rgb, 0.85)` + `backdrop-blur-sm` (Tailwind, not the system blur tokens). These are the first surfaces a new user sees; they should be canonical, not handrolled. 3. **Floating popovers / drawers** — - `components/Layout.js` mobile drawer (L698–709) - `components/Layout.js` sidebar profile dropdown (L88–96) - `components/ui/TopSearchBar.js` UserMenu dropdown (L214–222) All three render a translucent panel over arbitrary page content. None has the gradient-border treatment. The visual cue that says "this is an elevated surface" relies entirely on box-shadow, not on light response. 4. **`BulkSelectionToolbar`** (L47 and L117 dropdown) — uses hardcoded `bg-white border-gray-200`, invisible in dark mode. This is a floating toolbar over all content during bulk-select; should be `.glass-panel-strong`. 5. **`.card`-class consumers** — `pages/profile.js` (×3), `pages/settings.js` (×2), `pages/community/collections.js`, `components/CollectionsPageView.js`. The `.card` class (`styles/globals.css` L757) is a pre-redesign opaque solid-fill panel. Most consumers should migrate to `.glass-panel`; the class itself can either be retired or retained as a documented "opaque fallback" for special cases. 6. **Landing nav bar** (`pages/index.js` L61–72) — uses `var(--glass-surface-mid)` flat + an incorrectly double-wrapped `inset 0 1px 0` boxShadow. Not a gradient-border candidate (full-bleed bar; corner lights would be at viewport edges, not visible). Should use the existing `.page-header-glass` class instead. Unifying these means the entire app shares one surface vocabulary. Future work — new modals, new dashboards, new public pages — picks up the gradient-border treatment automatically because the primitives are correct. ## Scope ### In scope #### Brief 1 — Upgrade `` primitive Add the 4-layer gradient-border pattern to the `` component so its `tint`, `blur`, `rim`, `elevation` props compose with corner catch-lights. Default to the **subtle** corner-light tokens (`--corner-light-{warm,cool}-subtle`) since most consumers are data cards. Add a `cornerLights="chrome" | "subtle" | "none"` prop so floating chrome can opt up and special opaque tiles (CardItem grid mode) can opt out. Consumers automatically upgraded: - `` → corner lights on every modal - `` → corner lights on dashboard stat tiles - Landing feature cards (`pages/index.js` L132–224, L278–345) **Files:** `components/ui/GlassSurface.js`, `test/components/ui-primitives.test.js` (add a corner-light assertion). **Risk:** MED — the primitive's output structure changes (adds `border: 1px solid transparent`). Most consumers won't notice, but any consumer that set a custom `border` via style override could conflict. Audit needed. #### Brief 2 — Auth form cards `pages/login.js` L82–88 + `pages/signup.js` L227–232: replace the inline `className="p-8 rounded-2xl shadow-2xl backdrop-blur-sm border border-opacity-20" style={{ backgroundColor: 'rgba(...)' }}` shape with `className="glass-panel-strong rounded-2xl p-8"`. **Files:** `pages/login.js`, `pages/signup.js`. **Risk:** LOW. #### Brief 3 — Floating popovers / drawers Three independent floating surfaces: - `components/Layout.js` mobile drawer (L698–709) - `components/Layout.js` sidebar profile dropdown (L88–96) - `components/ui/TopSearchBar.js` UserMenu dropdown (L214–222) For each: drop the inline `background: var(--glass-surface-*)` + `backdropFilter` pair, add `className="glass-panel-strong rounded-2xl"` or `rounded-xl` to match existing dimensions. Preserve any additional `boxShadow: 'var(--ember-rim-subtle)'` adornments by chaining them onto the class's existing box-shadow (via inline style override). **Special handling for mobile drawer:** the drawer takes ~1/3 of the viewport. At full corner-light intensity it would be noisy. Use `.glass-panel-strong` (which uses the subtle tokens). **Files:** `components/Layout.js`, `components/ui/TopSearchBar.js`, `test/components/Layout.test.js` (regression assertions). **Risk:** LOW–MED — popovers appear over arbitrary page content. #### Brief 4 — `BulkSelectionToolbar` Replace `bg-white border-gray-200` on the toolbar (L47) and its "More actions" dropdown (L117) with `.glass-panel-strong rounded-2xl`. Sweep interior `text-gray-700 hover:bg-gray-100`, `text-red-600 hover:bg-red-50` to use `var(--text-primary)` / `nav-item-hover` and tokenized red for destructive actions. **Files:** `components/BulkSelectionToolbar.js`. **Risk:** MED — floats over all content during bulk-select mode; shadow/overflow bleed would be very visible. #### Brief 5 — `.card`-class consumers Audit every `
` usage. For each, pick the right migration: - Most likely: `glass-panel rounded-3xl p-6` (preserves rounded-3xl + p-6 from the old class). - Some may need to stay opaque (e.g. screenshot-friendly profile card with photo overlay) — keep `.card` and document the exception in `styles/globals.css`'s comment block. Decision to ratify: **retire `.card` entirely, OR keep as a documented opaque-panel alternative?** Architect's call. If kept, add a comment to `globals.css` explaining when to use which. **Files:** `pages/profile.js`, `pages/settings.js`, `pages/community/collections.js`, `components/CollectionsPageView.js`, optionally `styles/globals.css` (retire or document). **Risk:** LOW per page; sweep across 4 files. #### Brief 6 — Landing nav bar `pages/index.js` L61–72: replace the handrolled translucent header with `
`. The `.page-header-glass` class already exists in `globals.css` for exactly this "full-bleed top band" use case. The current bar also has a malformed `boxShadow: 'inset 0 1px 0 var(--rim-light-inner)'` (the token already contains `inset 0 1px 0`; double-wrapping breaks the cascade). Remove the malformed shadow. **Files:** `pages/index.js`. **Risk:** LOW — public page, no auth dependencies. #### Brief 7 — Forbidden grep gate Add a `forbidden-bespoke-glass-surface` job to `.github/workflows/ci.yml` that fails the build if `var(--glass-surface-(low|mid|high))` appears in JSX inline styles across `components/**` and `pages/**`, with a curated allowlist for the documented exceptions (intentional chrome treatment in Layout/TopSearchBar; `` component itself; any opaque-fallback `.card` consumers ratified in Brief 5). Prevents regression: future PRs can't reintroduce handrolled glass. **Files:** `.github/workflows/ci.yml`. **Risk:** LOW. ### Out of scope - `` grid-mode (L290–303) — intentionally opaque per AGENTS.md GPU-budget rule. - `` list-mode token cleanup (L183–284) — covered by the parallel `cleanup-card-item-list-and-share-modal-palette` convoy. - Adding any NEW surfaces or panels. - `.card` rename to `.opaque-panel` — Brief 5 may retire the class entirely; rename is a separate polish if it survives. ## Roles invoked 1. `role-architect` — surface-by-surface migration plan; ratifies the `cornerLights` prop shape for `` and the `.card` retire-vs-keep decision; writes Briefs 1–7. 2. `role-design-system-auditor` — visual regression review on each brief; confirms no chrome-vs-data-card hierarchy regressions. 3. `role-ux-reviewer` — light pass; ensures auth form contrast + mobile drawer legibility hold up post-migration. 4. `role-implementer` — one per brief; Briefs 2, 3, 4, 6 are parallel-safe (disjoint files, no shared component dependency chain). Brief 1 MUST land first because Briefs 3, 4 may end up simplifying their inline-style code by using the upgraded primitive instead. 5. `role-reviewer` — single post-PR review per brief. 6. `role-a11y-auditor` — focus-ring + keyboard nav for new floating surfaces (Brief 3 popovers especially). ## Todos - [ ] Architect: surface-by-surface migration plan + `` API spec (the `cornerLights` prop) - [ ] Design-system auditor: confirm subtle-tokens are the right default for the upgraded `` - [ ] Brief 1 — `` primitive upgrade (BLOCKING for Briefs 3, 4) - [ ] Brief 2 — auth form cards - [ ] Brief 3 — floating popovers (drawer, sidebar profile, UserMenu) - [ ] Brief 4 — BulkSelectionToolbar - [ ] Brief 5 — `.card` consumers; Decision: retire or keep - [ ] Brief 6 — landing nav bar (`.page-header-glass`) - [ ] Brief 7 — `forbidden-bespoke-glass-surface` CI gate - [ ] Post-PR review per brief - [ ] Visual-diff baseline refresh after Brief 1 lands ## Decisions to ratify 1. **`` API shape post-upgrade.** Add `cornerLights` prop with values `"chrome" | "subtle" | "none"`. Default `"subtle"`. Architect confirms or proposes alternative. 2. **Retire `.card` class entirely after Brief 5, or keep as documented opaque fallback?** Conservative: keep + document ("use when the surface must NOT have backdrop-filter — e.g. inside another modal, screen-reader-critical, or GPU-budget-constrained"). Aggressive: retire and migrate the handful of legitimate opaque cases to inline style. 3. **Mobile drawer corner-light intensity.** The drawer is a large surface. Subtle tokens (matching `.glass-panel-strong`) are likely right, but the architect should sanity-check visually before locking it in. 4. **CI gate scope for `forbidden-bespoke-glass-surface`.** Should it gate ALL `var(--glass-surface-*)` usage in JSX, or only inline `style={{ background: ... }}` usage? Recommended: only inline styles, since the tokens still need to be referenceable in `styles/globals.css`. ## Acceptance criteria 1. Every in-scope surface lists either `glass-panel`, `glass-panel-strong`, `page-header-glass`, or `` in its className (no inline `var(--glass-surface-*)` background). 2. `` primitive's output includes the 4-layer gradient-border pattern and a `border: 1px solid transparent`. 3. CI's `forbidden-bespoke-glass-surface` job passes; introducing a new bespoke `var(--glass-surface-low)` inline-style usage in a scratch commit makes it fail (negative test). 4. Build + lint + 113/113 vitest + Playwright smoke green. 5. Visual diff shows the expected differences (corner catch-lights appear on modals, dashboard stat tiles, auth cards, dropdowns) and no unexpected regressions on chrome/cards/CardItem grid. ## CI impact | Workflow / job | Behavior | | --- | --- | | `preview-smoke.yml` | Fires per brief. | | `visual-diff.yml` | **Fires + baseline refresh required** after Brief 1 lands (the primitive upgrade ripples through Modal/StatCard/landing). | | `lint` | Fires + new `forbidden-bespoke-glass-surface` gate after Brief 7. | | `test:` (vitest) | Fires; Brief 1 adds a corner-light assertion to `ui-primitives.test.js`. | ## Known constraints - **No-go zones honoured** — `components/Layout.js.backup`, `scripts/add-*.js` graveyard untouched. - **GPU budget for CardItem grid** — AGENTS.md explicitly prohibits `backdrop-filter` per card thumbnail (it multiplies); Brief 1 must NOT default `` to any rendering that would pull CardItem into that prohibition. ## Multitask dispatch ```yaml slice_dependencies: - brief: 1 depends_on: [] files: - components/ui/GlassSurface.js - test/components/ui-primitives.test.js - brief: 2 depends_on: [] files: - pages/login.js - pages/signup.js - brief: 3 depends_on: [1] # may simplify by using upgraded primitive files: - components/Layout.js - components/ui/TopSearchBar.js - test/components/Layout.test.js - brief: 4 depends_on: [1] files: - components/BulkSelectionToolbar.js - brief: 5 depends_on: [] files: - pages/profile.js - pages/settings.js - pages/community/collections.js - components/CollectionsPageView.js - styles/globals.css # if .card is retired / documented - brief: 6 depends_on: [] files: - pages/index.js - brief: 7 depends_on: [1, 2, 3, 4, 5, 6] # gate goes in LAST files: - .github/workflows/ci.yml ``` Briefs 2, 5, 6 can run in parallel with Brief 1. Briefs 3, 4 wait for Brief 1 to land so they can simplify by using the upgraded primitive. Brief 7 runs LAST so the grep gate doesn't fail the build on in-flight migrations. ## Out of scope follow-ups (queued) - **`retire-or-formalize-card-class`** — if Brief 5 Decision 2 keeps `.card` as opaque fallback, a future small convoy can rename it `.opaque-panel` for clarity. P3. - **`storybook-adoption-for-glass-surface`** — once the primitive has the full prop matrix (`tint`, `blur`, `rim`, `elevation`, `cornerLights`), it's a natural Storybook candidate. P2 DX. ## Architecture Run date: 2026-06-04. Architect: role-architect (this convoy). Reads: this convoy file, `AGENTS.md`, `.cursor/rules/*.mdc`, `components/ui/GlassSurface.js`, `components/ui/Modal.js`, `components/ui/StatCard.js`, `components/Layout.js`, `components/ui/TopSearchBar.js`, `components/BulkSelectionToolbar.js`, `pages/login.js`, `pages/signup.js`, `pages/index.js`, `pages/profile.js`, `pages/settings.js`, `pages/community/collections.js`, `components/CollectionsPageView.js`, `styles/globals.css` (`.card`, `.glass-panel`, `.glass-panel-strong`, `.page-header-glass`, the `--corner-light-*-subtle` tokens added in PR #118, the `--bg-secondary-rgb` token used by today's auth cards), `.github/workflows/ci.yml`, `test/components/ui-primitives.test.js`, `test/components/Layout.test.js`. ### Decisions ratified The convoy file's four open Decisions are resolved as follows. Implementer briefs cite these and MUST NOT renegotiate them mid-flight (if a brief discovers a reason to revisit, escalate via the mid-convoy scope-expansion process in `role-architect.md`). **D1. `` `cornerLights` prop shape.** Add `cornerLights = 'subtle' | 'chrome' | 'none'`, **default `'subtle'`**. - `'subtle'` → applies the 4-layer gradient with `--corner-light-warm-subtle` / `--corner-light-cool-subtle` (matches what `.glass-panel-strong` ships today; PR #118). - `'chrome'` → applies the 4-layer gradient with full-intensity `--corner-light-warm` / `--corner-light-cool`. Used by the Layout sidebar nav-chip and TopSearchBar header — those keep their inline styles for now; this prop value exists so future floating chrome doesn't have to re-handroll the gradient. - `'none'` → no transparent border, no radials, no `--chip-border-base` layer. Identical to today's primitive output. Used for the CardItem grid path and any other GPU-budget-constrained tile that legitimately must skip the gradient-border treatment. The gradient-border technique is the verbatim 4-layer recipe from `.glass-panel-strong` (`styles/globals.css` post-PR-#118): a padding-box solid linear-gradient of the fill, two border-box radial gradients for the corner catch-lights (warm 0%↔100%, cool 100%↔0%), and a border-box `var(--chip-border-base)` base layer. Combined with `border: 1px solid transparent` so the gradient renders through the border. **D2. `.card` class retire-or-keep.** **Retire the class entirely.** All 8 consumers migrate to `glass-panel rounded-3xl p-{6|4}`. The `.card` block in `styles/globals.css` is deleted. Rationale: a single glass vocabulary across the app is the convoy's success metric; keeping `.card` as a documented opaque fallback creates two parallel panel languages for future agents to choose between, which is exactly the kind of design-system bifurcation the convoy was scoped to eliminate. The GPU-budget concern that motivated keeping `.card` is specific to `` grid-mode (a non-`.card` consumer) — none of the 8 `.card` sites face that constraint (they're profile cards, settings panels, and a single community-page list tile, not a multiplied-per-thumbnail surface). Aggressive path; **risk** = one of the 8 sites has a hidden reason to be opaque that the architect didn't catch. Mitigation: Brief 5 includes a "if any consumer breaks visually post-migration, revert that one consumer to inline `style={{ background: 'var(--bg-secondary)' }}` and document the exception in the brief's post-merge note" escape hatch. That keeps the class deletion in the PR while leaving an explicit per-site fallback. If that escape hatch fires for more than 1 of the 8 consumers, the brief's reviewer is expected to push back and propose holding `.card` after all. **D3. Mobile-drawer corner-light intensity.** Use `.glass-panel-strong` (subtle tier). The drawer is a ~256px-wide fixed-position surface that covers ~1/3 of a mobile viewport. At full-intensity (`'chrome'`) the corner radials would dominate the drawer's interior nav text; at subtle they read as "elevated panel" without competing with content. The drawer is also a hidden surface most of the time — full intensity buys nothing for the rare moments it's open. **D4. CI gate scope.** Gate **inline-style usage only** — `var(--glass-surface-(low|mid|high))` appearing inside a JSX `style={{ background: ... }}` (or `backgroundColor:`, `background:` in template-string form) under `components/**` or `pages/**`. CSS class definitions in `styles/globals.css` (where the tokens are LEGITIMATELY chained to compose `.glass-panel` / `.glass-panel-strong` / `.page-header-glass`) are not gated. Allowlist entries: the Layout sidebar nav-chip block (`components/Layout.js` L858-861) and the TopSearchBar header block (the equivalent inline-style block in `components/ui/TopSearchBar.js`) — both intentionally retain `'chrome'`-tier handrolled gradients and were ratified as such in PR #116. The `` primitive itself is also allowlisted (it sets the background internally; that's its job). Brief 7 ships this gate as a separate `forbidden-bespoke-glass-surface` job in `ci.yml`, modeled on the existing `forbidden-modal-shell-without-primitive` gate's grep-and-allowlist shape. ### File plan | File | Action | Purpose | Brief | | --- | --- | --- | --- | | `components/ui/GlassSurface.js` | modified | Add `cornerLights` prop (`'subtle' \| 'chrome' \| 'none'`, default `'subtle'`); compose 4-layer gradient + `border: 1px solid transparent` when not `'none'` | 1 | | `test/components/ui-primitives.test.js` | modified | Add 3 corner-light assertions covering the 3 `cornerLights` values | 1 | | `pages/login.js` | modified | Replace L82-88 inline glass imitation with `
` | 2 | | `pages/signup.js` | modified | Replace L227-232 inline glass imitation with `
` | 2 | | `components/Layout.js` | modified | Sidebar profile dropdown (L88-96): drop inline background+blur+shadow, add `className="glass-panel-strong rounded-xl"`. Mobile drawer (L698-709): drop inline background+blur+shadow, add `className="glass-panel-strong"` and keep the slide-in transform classes. Sidebar nav-chip block (L858-861) untouched (chrome tier). | 3 | | `components/ui/TopSearchBar.js` | modified | UserMenu dropdown (L214-222): drop inline background+blur+shadow, add `className="glass-panel-strong rounded-xl"`. Header block (chrome tier) untouched. | 3 | | `test/components/Layout.test.js` | modified | Add regression-lock assertion: sidebar profile dropdown and mobile drawer carry `.glass-panel-strong` class. | 3 | | `components/BulkSelectionToolbar.js` | modified | L47 toolbar: replace `bg-white border-gray-200` with `glass-panel-strong rounded-2xl`. L117 dropdown: same swap on `rounded-lg` → `rounded-xl glass-panel-strong`. Sweep interior `text-gray-{600,700}` to `var(--text-primary/secondary)`; `hover:bg-gray-{50,100}` to `nav-item-hover`; `text-red-600 hover:bg-red-50` to `var(--accent-danger)` + ember-tinted hover. | 4 | | `pages/profile.js` | modified | 3 `.card` → `glass-panel rounded-3xl p-6` swaps. | 5 | | `pages/settings.js` | modified | 2 `.card` → `glass-panel rounded-3xl` swaps (one `p-4`, one `p-6`). | 5 | | `pages/community/collections.js` | modified | 1 `.card` → `glass-panel rounded-3xl` swap (preserve `group cursor-pointer hover:shadow-lg transition-all`). | 5 | | `components/CollectionsPageView.js` | modified | 1 `.card` → `glass-panel rounded-3xl` swap (preserve `hover:shadow-xl transition-all cursor-pointer group`). | 5 | | `styles/globals.css` | modified | **Delete** the `.card { ... }` rule (the class block that lives near L757). No other CSS changes. | 5 | | `pages/index.js` | modified | Landing nav bar (L61-72): replace handrolled inline style with `