deckhearth/.convoys/unify-glass-panel-surfaces.md
varutasu 66a4d7b721
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 14:09:40 -05:00

648 lines
34 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

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

---
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. **`<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` L8288,
`pages/signup.js` L227232) — 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 (L698709)
- `components/Layout.js` sidebar profile dropdown (L8896)
- `components/ui/TopSearchBar.js` UserMenu dropdown (L214222)
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` L6172) — 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` L132224, L278345)
**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` L8288 + `pages/signup.js` L227232: 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 (L698709)
- `components/Layout.js` sidebar profile dropdown (L8896)
- `components/ui/TopSearchBar.js` UserMenu dropdown (L214222)
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:** LOWMED — 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` L6172: 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 (L290303) — intentionally opaque per
AGENTS.md GPU-budget rule.
- `<CardItem>` list-mode token cleanup (L183284) — 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 17.
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.