docs(convoys): unify-glass-panel-surfaces + cleanup palette — conductor seed + architect plan (#119)
* docs(convoys): seed unify-glass-panel-surfaces + cleanup-card-item-list-and-share-modal-palette
Two convoy seeds opened as follow-ups to the 2026-06-04 design pass
(#116 corner-border-light → #117 site-wide sweep → #118 card vibrancy
reduction). Both were called out in #117's PR body as deferred and are
now formally tracked.
## unify-glass-panel-surfaces
Migrates remaining panel-shaped surfaces to the gradient-border
corner-light treatment so the app shares one surface vocabulary.
The audit's key insight: `<GlassSurface>` (`components/ui/GlassSurface.js`)
predates the corner-light pattern. Because `<Modal>`, `<StatCard>`,
and the landing-page feature/collection cards all delegate to it,
upgrading the primitive cascades to ~10 visible surfaces at once.
7 briefs, multitask-parallel after Brief 1 lands:
1. `<GlassSurface>` primitive upgrade — BLOCKING for 3, 4
2. Auth form cards (login.js, signup.js)
3. Floating popovers (mobile drawer, sidebar profile dropdown,
UserMenu dropdown)
4. BulkSelectionToolbar (currently `bg-white border-gray-200` —
invisible in dark mode)
5. `.card`-class consumers (4 pages); decision to ratify whether
to retire `.card` entirely or keep as documented opaque fallback
6. Landing nav bar — wrong pattern; should use existing
`.page-header-glass` class
7. `forbidden-bespoke-glass-surface` CI grep gate — prevents
regression after the migration ships
## cleanup-card-item-list-and-share-modal-palette
Targeted palette cleanup for two files whose interiors weren't
addressed in #117:
1. `CardItem.js` list-mode (L183–284) — entirely hardcoded
Tailwind palette (`bg-purple-50`, `border-gray-200`, `text-gray-{500-900}`,
`bg-blue-100 text-blue-800` etc.); unreadable / off-brand in dark mode.
2. `ShareModal.js` interior rows — purple avatar circles, gray-50
permission row (invisible in dark mode), blue-600 Copy-link button,
gray text labels.
Token-only swap. 2 parallel briefs, no architect / IA / UX needed
(no design decisions — palette to design tokens).
## Sequencing note
The two convoys are independent and can run in parallel. The audit
agent's recommended sequencing (Brief 1 of `unify-glass-panel-surfaces`
first) is encoded in the multitask `slice_dependencies` blocks.
Co-authored-by: Cursor <cursoragent@cursor.com>
* docs(convoys): architect plan + 7 briefs for unify-glass-panel-surfaces
Appends the ## Architecture section to .convoys/unify-glass-panel-surfaces.md
ratifying the 4 open Decisions, and writes 7 implementer-brief files under
.convoys/unify-glass-panel-surfaces/.
Decisions ratified:
D1. <GlassSurface> grows a cornerLights='subtle'|'chrome'|'none' prop,
default 'subtle' (4-layer gradient-border per PR #118 recipe).
D2. Retire .card entirely; migrate all 8 consumers to .glass-panel.
D3. Mobile drawer uses subtle tier (.glass-panel-strong).
D4. CI gate scopes to JSX inline-style usage only; 3-entry allowlist
for documented chrome exceptions.
Briefs (1-7, all <100 LOC each, disjoint files):
1. Upgrade <GlassSurface> primitive (no deps, blocks 3+4)
2. Migrate auth form cards (parallel-safe)
3. Migrate floating popovers (sidebar dropdown, mobile drawer,
TopSearchBar UserMenu)
4. Migrate BulkSelectionToolbar + interior token sweep
5. Retire .card class; migrate 8 consumers
6. Migrate landing nav bar to .page-header-glass
7. forbidden-bespoke-glass-surface CI gate (runs LAST)
Boot-the-brief check: all 4-layer recipes verified against post-PR-#118
styles/globals.css; all box-shadow preservation chains verified against
current Layout.js + TopSearchBar.js; .card deletion verified clean
(single rule at L729-733); CI gate shape modeled on existing
forbidden-modal-shell-without-primitive job.
Architecture is read-only output — no application code touched.
Awaiting human gate 1 (plan approval) before implementers run.
Co-authored-by: Cursor <cursoragent@cursor.com>
* docs(convoys): fix Brief 3 TopSearchBar UserMenu elevation token
Self-review recheck against current TopSearchBar.js (L211-222) found
that the UserMenu dropdown uses var(--elevation-pronounced), not
var(--elevation-ambient) as Brief 3's initial sample showed. Brief 3
now documents the verbatim shape (pronounced + role="menu" +
aria-label + overflow-hidden) and the convoy's risk-list note is
updated to reflect all three popovers' verbatim chains.
No semantic change to the plan; just an accuracy fix the
implementer of Brief 3 needs to ship the right code on the first try.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 15:09:40 -04:00
---
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
2026-06-05 07:18:30 -04:00
status: closed
closed: 2026-06-04
2026-06-04 17:47:26 -04:00
queued_followup:
- migrate-button-input-mobilenav-to-glass-primitive
2026-06-05 07:18:30 -04:00
prs:
- 120 # Brief 6 — landing nav → .page-header-glass
- 121 # Brief 2 — auth form cards → .glass-panel-strong
- 122 # Brief 5 — retire .card; migrate 7 consumers
- 123 # Brief 1 — < GlassSurface > cornerLights prop
- 124 # Brief 3 — floating popovers → .glass-panel-strong
- 125 # Brief 4 — BulkSelectionToolbar + token sweep
- 127 # Brief 7 — forbidden-bespoke-glass-surface CI gate (Check 7/7)
docs(convoys): unify-glass-panel-surfaces + cleanup palette — conductor seed + architect plan (#119)
* docs(convoys): seed unify-glass-panel-surfaces + cleanup-card-item-list-and-share-modal-palette
Two convoy seeds opened as follow-ups to the 2026-06-04 design pass
(#116 corner-border-light → #117 site-wide sweep → #118 card vibrancy
reduction). Both were called out in #117's PR body as deferred and are
now formally tracked.
## unify-glass-panel-surfaces
Migrates remaining panel-shaped surfaces to the gradient-border
corner-light treatment so the app shares one surface vocabulary.
The audit's key insight: `<GlassSurface>` (`components/ui/GlassSurface.js`)
predates the corner-light pattern. Because `<Modal>`, `<StatCard>`,
and the landing-page feature/collection cards all delegate to it,
upgrading the primitive cascades to ~10 visible surfaces at once.
7 briefs, multitask-parallel after Brief 1 lands:
1. `<GlassSurface>` primitive upgrade — BLOCKING for 3, 4
2. Auth form cards (login.js, signup.js)
3. Floating popovers (mobile drawer, sidebar profile dropdown,
UserMenu dropdown)
4. BulkSelectionToolbar (currently `bg-white border-gray-200` —
invisible in dark mode)
5. `.card`-class consumers (4 pages); decision to ratify whether
to retire `.card` entirely or keep as documented opaque fallback
6. Landing nav bar — wrong pattern; should use existing
`.page-header-glass` class
7. `forbidden-bespoke-glass-surface` CI grep gate — prevents
regression after the migration ships
## cleanup-card-item-list-and-share-modal-palette
Targeted palette cleanup for two files whose interiors weren't
addressed in #117:
1. `CardItem.js` list-mode (L183–284) — entirely hardcoded
Tailwind palette (`bg-purple-50`, `border-gray-200`, `text-gray-{500-900}`,
`bg-blue-100 text-blue-800` etc.); unreadable / off-brand in dark mode.
2. `ShareModal.js` interior rows — purple avatar circles, gray-50
permission row (invisible in dark mode), blue-600 Copy-link button,
gray text labels.
Token-only swap. 2 parallel briefs, no architect / IA / UX needed
(no design decisions — palette to design tokens).
## Sequencing note
The two convoys are independent and can run in parallel. The audit
agent's recommended sequencing (Brief 1 of `unify-glass-panel-surfaces`
first) is encoded in the multitask `slice_dependencies` blocks.
Co-authored-by: Cursor <cursoragent@cursor.com>
* docs(convoys): architect plan + 7 briefs for unify-glass-panel-surfaces
Appends the ## Architecture section to .convoys/unify-glass-panel-surfaces.md
ratifying the 4 open Decisions, and writes 7 implementer-brief files under
.convoys/unify-glass-panel-surfaces/.
Decisions ratified:
D1. <GlassSurface> grows a cornerLights='subtle'|'chrome'|'none' prop,
default 'subtle' (4-layer gradient-border per PR #118 recipe).
D2. Retire .card entirely; migrate all 8 consumers to .glass-panel.
D3. Mobile drawer uses subtle tier (.glass-panel-strong).
D4. CI gate scopes to JSX inline-style usage only; 3-entry allowlist
for documented chrome exceptions.
Briefs (1-7, all <100 LOC each, disjoint files):
1. Upgrade <GlassSurface> primitive (no deps, blocks 3+4)
2. Migrate auth form cards (parallel-safe)
3. Migrate floating popovers (sidebar dropdown, mobile drawer,
TopSearchBar UserMenu)
4. Migrate BulkSelectionToolbar + interior token sweep
5. Retire .card class; migrate 8 consumers
6. Migrate landing nav bar to .page-header-glass
7. forbidden-bespoke-glass-surface CI gate (runs LAST)
Boot-the-brief check: all 4-layer recipes verified against post-PR-#118
styles/globals.css; all box-shadow preservation chains verified against
current Layout.js + TopSearchBar.js; .card deletion verified clean
(single rule at L729-733); CI gate shape modeled on existing
forbidden-modal-shell-without-primitive job.
Architecture is read-only output — no application code touched.
Awaiting human gate 1 (plan approval) before implementers run.
Co-authored-by: Cursor <cursoragent@cursor.com>
* docs(convoys): fix Brief 3 TopSearchBar UserMenu elevation token
Self-review recheck against current TopSearchBar.js (L211-222) found
that the UserMenu dropdown uses var(--elevation-pronounced), not
var(--elevation-ambient) as Brief 3's initial sample showed. Brief 3
now documents the verbatim shape (pronounced + role="menu" +
aria-label + overflow-hidden) and the convoy's risk-list note is
updated to reflect all three popovers' verbatim chains.
No semantic change to the plan; just an accuracy fix the
implementer of Brief 3 needs to ship the right code on the first try.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 15:09:40 -04:00
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. ** `<GlassSurface>` 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 `<Modal>` (and therefore
every modal in the app), `<StatCard>` (dashboard stat tiles),
and the public landing-page feature/collection cards all delegate
to `<GlassSurface>` , 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 `<GlassSurface>` primitive
Add the 4-layer gradient-border pattern to the `<GlassSurface>`
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:
- `<Modal>` → corner lights on every modal
- `<StatCard>` → 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 `<div className="card">` 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 `<header className="page-header-glass border-b ...">` . 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; `<GlassSurface>` 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
- `<CardItem>` grid-mode (L290– 303) — intentionally opaque per
AGENTS.md GPU-budget rule.
- `<CardItem>` 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 `<GlassSurface>` 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 + `<GlassSurface>`
API spec (the `cornerLights` prop)
- [ ] Design-system auditor: confirm subtle-tokens are the right
default for the upgraded `<GlassSurface>`
- [ ] Brief 1 — `<GlassSurface>` 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. ** `<GlassSurface>` 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 `<GlassSurface>`
in its className (no inline `var(--glass-surface-*)` background).
2. `<GlassSurface>` 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 `<GlassSurface>` 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. `<GlassSurface>` `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 `<CardItem>` 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 `<GlassSurface>` 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 `<div className="glass-panel-strong rounded-2xl p-8">` | 2 |
| `pages/signup.js` | modified | Replace L227-232 inline glass imitation with `<div className="glass-panel-strong rounded-2xl p-8">` | 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 `<nav className="page-header-glass border-b">` . Drop the malformed `boxShadow: 'inset 0 1px 0 var(--rim-light-inner)'` (the `.page-header-glass` class already sets the correct inset rim). | 6 |
| `.github/workflows/ci.yml` | modified | Add `forbidden-bespoke-glass-surface` job per D4. | 7 |
### API surface
N/A. This convoy is pure UI/CSS surface composition. No new routes,
no Zod schema additions, no rate-limit changes.
### Schema diff
N/A. No DB changes. No `migrations/` files added; no
`scripts/add-*.js` follow-up. The schema-map-fresh CI job will not
fire on any of these PRs (the conditional in `ci.yml` L47-52 checks
`migrations/` / `scripts/add-*` / `scripts/fix-*` /
`scripts/setup-neon-db.js` / `docs/SCHEMA_MAP.md` — none touched).
### Test plan
| Brief | Unit | Component | Smoke | Visual diff | Notes |
| --- | --- | --- | --- | --- | --- |
| 1 | — | **3 new assertions** in `test/components/ui-primitives.test.js` covering `cornerLights='subtle'` (default, expects `border: 1px solid transparent` in the rendered style attribute), `cornerLights='chrome'` (expects `--corner-light-warm` token reference, not `-subtle` ), `cornerLights='none'` (expects no `border` declaration, no gradient layers — same shape as today). | Existing smoke tests cover Modal open/close; nothing new. | **Baseline refresh required** after Brief 1 lands — Modal/StatCard/landing feature cards all gain corner lights. Plan: queue the refresh as a single Linux-baselined commit immediately after Brief 1 merges, before Briefs 3/4 ship. | The most consequential brief; the assertions are the regression lock. |
| 2 | — | None needed (presentational class swap). | Existing `tests/smoke/auth.spec.js` covers login/signup happy path; nothing new. | Auth pages diff expected. | — |
| 3 | — | Regression-lock in `test/components/Layout.test.js` : render Layout with `isMobileMenuOpen={true}` (mocked) and assert the drawer container's `className` includes `glass-panel-strong` . Render with `<UserProfileDropdown>` open (mocked) and assert its dropdown container's `className` includes `glass-panel-strong` . | Existing smoke covers nav clicks. | Layout dropdown/drawer visual diff expected. | — |
| 4 | — | None needed (rare to bulk-select in a smoke test). | — | Expected visual diff on the bulk-select toolbar — but only fires when one of the visual-diff `tests/visual/*` specs actually triggers bulk-select. Probably won't surface in the diff at all; rely on manual QA + dark-mode side-by-side screenshot for review. | The dark-mode invisibility is the regression we're closing; verify manually post-merge. |
| 5 | — | None needed (className swap). | — | 4 pages worth of visual diff expected. | — |
| 6 | — | None needed. | Existing smoke `tests/smoke/landing.spec.js` covers landing nav rendering. | Expected on landing. | — |
| 7 | **Negative test** documented in the brief's acceptance criteria: an implementer dry-run that adds a scratch `style={{ background: 'var(--glass-surface-low)' }}` to a `components/` file should fail the new `forbidden-bespoke-glass-surface` job. Verified once by the architect via the boot-the-brief check below; not committed. | — | — | — | The grep gate runs in CI; no test code needed. |
Reference test files for the brief authors:
- `test/components/ui-primitives.test.js` (Button/Input/SearchBar test
patterns, jsdom env, cleanup pattern; Brief 1 extends in-file).
- `test/components/Layout.test.js` (5 regression-lock assertions
established by `fix-layout-default-user` PR #15 ; Brief 3 extends).
### Risk list
1. **Brief 1 ripple effect** (**MED**). The `<GlassSurface>` primitive
feeds `<Modal>` , `<StatCard>` , and 4-5 landing-page feature cards.
Adding `border: 1px solid transparent` to every consumer changes
the rendered geometry by 2px in both axes — most consumers don't
notice (their `padding` /`gap` absorbs it), but any consumer that
relied on a 0-border box-model for pixel-exact layout will shift.
The `<Modal>` panel sets `maxWidth` in pixels and is centered via
flex — it's not sensitive. `<StatCard>` is in a 4-up grid — also
not sensitive. The landing feature cards use `grid` with `gap`
— not sensitive. But the visual-diff baseline refresh is
non-negotiable; queue it before Briefs 3 + 4 ship.
2. **Brief 1 + GPU-budget constraint** (**LOW-MED**). AGENTS.md
forbids `backdrop-filter` per `CardItem` thumbnail. `<CardItem>`
does NOT use `<GlassSurface>` today, so adding gradient-border to
`<GlassSurface>` doesn't pull it in. The `'none'` cornerLights
value exists as a defensive escape hatch for any future tile
surface that DOES want `<GlassSurface>` without the gradient
layers. Brief 1's brief explicitly forbids changing CardItem
anywhere in this convoy.
3. **Brief 3 popovers' z-index + box-shadow chain** (**LOW-MED**).
`.glass-panel-strong` ships with
`var(--rim-light-inner), var(--elevation-ambient)` . Today's
inline-style on the profile dropdown adds
`var(--ember-rim-subtle)` as a third shadow for ember emphasis;
today's mobile drawer uses `var(--elevation-pronounced)` (heavier
elevation than ambient). The brief MUST preserve these via an
inline-style override chained onto the class's box-shadow — DON'T
just drop them. Verbatim shape documented in Brief 3.
4. **Brief 4 dark-mode regressions** (**LOW**). The bulk-select
toolbar is currently `bg-white` — invisible in dark mode. The
migration FIXES that regression but Brief 4's reviewer should
capture before/after dark-mode screenshots so the fix is
visible in PR review.
5. **Brief 5 `.card` deletion risk** (**LOW-MED, conditional**).
Per D2, all 8 consumers migrate to `.glass-panel` . If any one of
them breaks visually (the architect doesn't expect this; the
8 sites are all "panel with content inside" surfaces), the
brief includes an escape hatch to revert that one site to
inline `style={{ background: 'var(--bg-secondary)' }}` rather
than holding the whole class deletion. If MORE than 1 of 8
needs the escape hatch, the brief's reviewer is expected to
push back and propose keeping `.card` .
6. **Brief 6 `.page-header-glass` already-applied check** (**LOW**).
The implementer must verify `pages/index.js` 's nav bar isn't
already inside another `.page-header-glass` ancestor (it isn't —
landing has no shared Layout); otherwise nested
`backdrop-filter` s would compound.
7. **Brief 7 false-positive risk** (**LOW**). The grep gate must
correctly distinguish JSX inline-style usage of
`var(--glass-surface-*)` from CSS class definitions in
`styles/globals.css` . The architect-prescribed approach is to
restrict the grep to files under `components/**` and `pages/**`
(excluding `.css` ), which by construction means JSX inline-style
only. Brief 7 includes a verbatim grep command shape and the
2-entry allowlist.
8. **PR #117 + PR #118 not yet merged** (**LOW, dependency**). This
convoy's `depends_on:` frontmatter lists both. If either fails
review, the briefs may need to refactor against a different
baseline. Recommended: hold Brief 1 dispatch until PR #117 + PR
#118 are both squash-merged to main.
### Decomposition
| Brief # | Title | Files | Depends on | LOC est. |
| --- | --- | --- | --- | --- |
| 1 | Upgrade `<GlassSurface>` primitive with `cornerLights` prop | `components/ui/GlassSurface.js` , `test/components/ui-primitives.test.js` | — | ~80 |
| 2 | Migrate auth form cards to `.glass-panel-strong` | `pages/login.js` , `pages/signup.js` | — | ~20 |
| 3 | Migrate floating popovers to `.glass-panel-strong` | `components/Layout.js` , `components/ui/TopSearchBar.js` , `test/components/Layout.test.js` | 1 | ~90 |
| 4 | Migrate `BulkSelectionToolbar` to `.glass-panel-strong` + token sweep | `components/BulkSelectionToolbar.js` | 1 | ~70 |
| 5 | Retire `.card` class; migrate 8 consumers to `.glass-panel` | `pages/profile.js` , `pages/settings.js` , `pages/community/collections.js` , `components/CollectionsPageView.js` , `styles/globals.css` | — | ~30 |
| 6 | Migrate landing nav bar to `.page-header-glass` | `pages/index.js` | — | ~10 |
| 7 | `forbidden-bespoke-glass-surface` CI gate | `.github/workflows/ci.yml` | 1, 2, 3, 4, 5, 6 | ~40 |
All briefs target < 400 LOC ; the largest ( Brief 3 ) is ~ 90 LOC . Files
across briefs are disjoint — verified against the slice_dependencies
block above. No two parallel writers on the same file.
Parallel dispatch recommended for Briefs 1, 2, 5, 6 (no deps, disjoint
files). Briefs 3 + 4 wait for Brief 1 to land (per D1 — they may
simplify by adopting the upgraded primitive's `cornerLights='subtle'`
default rather than handrolling). Brief 7 runs LAST so the grep gate
doesn't fail the build on in-flight migrations.
### Boot-the-brief check
Read-only verification against the post-PR-#117 + post-PR-#118 tree.
1. **Dep-set check.** No new npm dependencies added by any brief. The
gradient-border recipe is pure CSS; the `cornerLights` prop is
pure React composition; the CI gate is a shell `grep` invocation
in a YAML job. No `pnpm view` / `package.json` audit needed.
2. **Verbatim code-shape check.**
- Brief 1's 4-layer recipe matches `.glass-panel-strong` 's current
shape verbatim — verified against `styles/globals.css` post-PR
#118 (the `background:` block uses
`linear-gradient(var(--glass-surface-high), ...) padding-box,
radial-gradient(at 0% 100%, var(--corner-light-warm-subtle) ...) border-box,
radial-gradient(at 100% 0%, var(--corner-light-cool-subtle) ...) border-box,
var(--chip-border-base) border-box`). The primitive's
`'chrome'` value uses the non-`-subtle` tokens; both pairs exist
in `:root` + `[data-theme="dark"]` (verified).
- Brief 3's box-shadow preservation: the sidebar profile dropdown
ships `var(--rim-light-inner), var(--ember-rim-subtle),
var(--elevation-ambient)`; the mobile drawer ships
`var(--rim-light-inner), var(--rim-light-outer),
var(--elevation-pronounced)`; the TopSearchBar UserMenu dropdown
ships `var(--rim-light-inner), var(--ember-rim-subtle),
var(--elevation-pronounced)` (verified during boot-the-brief
recheck — uses `-pronounced` , not `-ambient` , because it floats
higher in the viewport). All three are preserved as inline
`style={{ boxShadow: '...' }}` overrides on the
`.glass-panel-strong` element (the class's default
`var(--rim-light-inner), var(--elevation-ambient)` would otherwise
lose the ember-rim and either heavier elevation). Brief 3
documents the verbatim chain.
- Brief 5's class deletion: the `.card` block in
`styles/globals.css` is a single rule; deleting it is a clean
diff (verified by reading L750-L770 area — no other `.card`
compound selectors exist).
- Brief 7's grep command shape: modeled on the existing
`forbidden-modal-shell-without-primitive` job (`ci.yml`
L200-L226), which uses
`grep -lE 'pattern' pages components -r --include='*.js'` .
Same shape works here. Allowlist is implemented via
`! -path 'components/ui/GlassSurface.js'` and an explicit
line-range exclusion for `components/Layout.js` and
`components/ui/TopSearchBar.js` (the chrome blocks).
3. **Cross-brief commitments.** None. Each brief lands a complete
migration of its scope; no brief introduces a stub or forward
declaration that another brief resolves. Brief 7's grep gate
passes ONLY AFTER Briefs 1-6 land — that's a dispatch ordering
constraint (encoded in `depends_on:` ), not a cross-brief
commitment in the role-architect sense.
All checks passed. No briefs need revision.