Brief 7 (final brief) of unify-glass-panel-surfaces convoy. Adds the regression gate that prevents reintroduction of bespoke var(--glass-surface-low|mid|high) inline styles outside the documented allowlist. IMPLEMENTATION DEVIATION FROM BRIEF (DOCUMENTED): Brief 7 was authored before PR #126 (slash-ci-minutes convoy) consolidated the 6 grep-only forbidden-* jobs into a single forbidden-patterns job with sequential ::group:: sections. Adding Brief 7 as a standalone forbidden-bespoke-glass-surface job (the brief's verbatim shape) would partially undo PR #126's checkout amortization win. Instead, this PR adds the check as Check 7/7 inside the existing forbidden-patterns job — semantics, allowlist, and grep pattern are exactly as Brief 7 specifies; only the wrapper changes. Job display name updated: "Forbidden patterns (6 checks)" → "Forbidden patterns (7 checks)". ALLOWLIST EXPANSION (DOCUMENTED): Brief 7's planned 3-entry allowlist (the 3 chrome blocks) turned out to undercount the residual surface area. Three additional files still handroll their own var(--glass-surface-*) inline styles: - components/ui/Button.js (secondary variant) - components/ui/Input.js (input wrapper) - components/MobileNavigation.js (bottom-nav background) Per Brief 7's own note ("If you need to add a fourth allowlist entry, that's a design-system decision — open a new convoy"), the right call is to ship the gate NOW with a 6-entry allowlist (3 chrome + 3 pending-migration) and track the cleanup in a follow-up. This PR therefore also seeds `.convoys/migrate-button-input-mobilenav-to-glass-primitive.md` with the migration plan, open questions for the architect, and acceptance criteria. The 3 pending entries are tagged with inline `# TODO:` comments in ci.yml referencing the follow-up convoy. Local verification (per Brief 7's pre-merge negative test): - Positive test: grep on clean main → 0 violations outside the 6-entry allowlist. ✅ - Negative test: injected a scratch `const scratch = { background: 'var(--glass-surface-low)' }` line at EOF of pages/profile.js; grep correctly flagged it. ✅ - Revert verified: removing the scratch line returns the grep to 0 violations. ✅ The scratch change was NOT committed (per Brief 7's instructions). Convoy closeout: - `.convoys/unify-glass-panel-surfaces.md` status moved from `open` to `closing`; queued_followup field names the new convoy. The convoy lands fully when this PR merges. Verification: - npm run lint passes (1 pre-existing unrelated warning). - npm run test:run: 118/118 tests pass. - YAML parses (python3 yaml.safe_load). Files: - .github/workflows/ci.yml: +~70 lines (Check 7/7 step + final-exit copy edit). - .convoys/migrate-button-input-mobilenav-to-glass-primitive.md: new file, 113 lines. - .convoys/unify-glass-panel-surfaces.md: +2 lines (status + queued_followup fields). Co-authored-by: Cursor <cursoragent@cursor.com>
34 KiB
| name | classification | success_metric | skip | status | queued_followup | created | depends_on | ||||
|---|---|---|---|---|---|---|---|---|---|---|---|
| unify-glass-panel-surfaces | feature | 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. |
|
closing |
|
2026-06-04 |
|
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:
-
<GlassSurface>primitive — the most consequential gap.components/ui/GlassSurface.jsuses a single flatvar(--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. -
Auth form cards (
pages/login.jsL82–88,pages/signup.jsL227–232) — handrolled glass imitation usingrgba(--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. -
Floating popovers / drawers —
components/Layout.jsmobile drawer (L698–709)components/Layout.jssidebar profile dropdown (L88–96)components/ui/TopSearchBar.jsUserMenu 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.
-
BulkSelectionToolbar(L47 and L117 dropdown) — uses hardcodedbg-white border-gray-200, invisible in dark mode. This is a floating toolbar over all content during bulk-select; should be.glass-panel-strong. -
.card-class consumers —pages/profile.js(×3),pages/settings.js(×2),pages/community/collections.js,components/CollectionsPageView.js. The.cardclass (styles/globals.cssL757) 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. -
Landing nav bar (
pages/index.jsL61–72) — usesvar(--glass-surface-mid)flat + an incorrectly double-wrappedinset 0 1px 0boxShadow. Not a gradient-border candidate (full-bleed bar; corner lights would be at viewport edges, not visible). Should use the existing.page-header-glassclass 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.jsL132–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.jsmobile drawer (L698–709)components/Layout.jssidebar profile dropdown (L88–96)components/ui/TopSearchBar.jsUserMenu 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
.cardand document the exception instyles/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 parallelcleanup-card-item-list-and-share-modal-paletteconvoy.- Adding any NEW surfaces or panels.
.cardrename to.opaque-panel— Brief 5 may retire the class entirely; rename is a separate polish if it survives.
Roles invoked
role-architect— surface-by-surface migration plan; ratifies thecornerLightsprop shape for<GlassSurface>and the.cardretire-vs-keep decision; writes Briefs 1–7.role-design-system-auditor— visual regression review on each brief; confirms no chrome-vs-data-card hierarchy regressions.role-ux-reviewer— light pass; ensures auth form contrast + mobile drawer legibility hold up post-migration.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.role-reviewer— single post-PR review per brief.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 (thecornerLightsprop) - 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 —
.cardconsumers; Decision: retire or keep - Brief 6 — landing nav bar (
.page-header-glass) - Brief 7 —
forbidden-bespoke-glass-surfaceCI gate - Post-PR review per brief
- Visual-diff baseline refresh after Brief 1 lands
Decisions to ratify
<GlassSurface>API shape post-upgrade. AddcornerLightsprop with values"chrome" | "subtle" | "none". Default"subtle". Architect confirms or proposes alternative.- Retire
.cardclass 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. - 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. - CI gate scope for
forbidden-bespoke-glass-surface. Should it gate ALLvar(--glass-surface-*)usage in JSX, or only inlinestyle={{ background: ... }}usage? Recommended: only inline styles, since the tokens still need to be referenceable instyles/globals.css.
Acceptance criteria
- Every in-scope surface lists either
glass-panel,glass-panel-strong,page-header-glass, or<GlassSurface>in its className (no inlinevar(--glass-surface-*)background). <GlassSurface>primitive's output includes the 4-layer gradient-border pattern and aborder: 1px solid transparent.- CI's
forbidden-bespoke-glass-surfacejob passes; introducing a new bespokevar(--glass-surface-low)inline-style usage in a scratch commit makes it fail (negative test). - Build + lint + 113/113 vitest + Playwright smoke green.
- 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-*.jsgraveyard untouched. - GPU budget for CardItem grid — AGENTS.md explicitly
prohibits
backdrop-filterper card thumbnail (it multiplies); Brief 1 must NOT default<GlassSurface>to any rendering that would pull CardItem into that prohibition.
Multitask dispatch
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.cardas opaque fallback, a future small convoy can rename it.opaque-panelfor 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-strongships 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-baselayer. 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 byfix-layout-default-userPR #15; Brief 3 extends).
Risk list
- Brief 1 ripple effect (MED). The
<GlassSurface>primitive feeds<Modal>,<StatCard>, and 4-5 landing-page feature cards. Addingborder: 1px solid transparentto every consumer changes the rendered geometry by 2px in both axes — most consumers don't notice (theirpadding/gapabsorbs it), but any consumer that relied on a 0-border box-model for pixel-exact layout will shift. The<Modal>panel setsmaxWidthin pixels and is centered via flex — it's not sensitive.<StatCard>is in a 4-up grid — also not sensitive. The landing feature cards usegridwithgap— not sensitive. But the visual-diff baseline refresh is non-negotiable; queue it before Briefs 3 + 4 ship. - Brief 1 + GPU-budget constraint (LOW-MED). AGENTS.md
forbids
backdrop-filterperCardItemthumbnail.<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. - Brief 3 popovers' z-index + box-shadow chain (LOW-MED).
.glass-panel-strongships withvar(--rim-light-inner), var(--elevation-ambient). Today's inline-style on the profile dropdown addsvar(--ember-rim-subtle)as a third shadow for ember emphasis; today's mobile drawer usesvar(--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. - 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. - Brief 5
.carddeletion 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 inlinestyle={{ 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. - Brief 6
.page-header-glassalready-applied check (LOW). The implementer must verifypages/index.js's nav bar isn't already inside another.page-header-glassancestor (it isn't — landing has no shared Layout); otherwise nestedbackdrop-filters would compound. - Brief 7 false-positive risk (LOW). The grep gate must
correctly distinguish JSX inline-style usage of
var(--glass-surface-*)from CSS class definitions instyles/globals.css. The architect-prescribed approach is to restrict the grep to files undercomponents/**andpages/**(excluding.css), which by construction means JSX inline-style only. Brief 7 includes a verbatim grep command shape and the 2-entry allowlist. - 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.
- Dep-set check. No new npm dependencies added by any brief. The
gradient-border recipe is pure CSS; the
cornerLightsprop is pure React composition; the CI gate is a shellgrepinvocation in a YAML job. Nopnpm view/package.jsonaudit needed. - Verbatim code-shape check.
- Brief 1's 4-layer recipe matches
.glass-panel-strong's current shape verbatim — verified againststyles/globals.csspost-PR #118 (thebackground:block useslinear-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--subtletokens; 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 shipsvar(--rim-light-inner), var(--rim-light-outer), var(--elevation-pronounced); the TopSearchBar UserMenu dropdown shipsvar(--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 inlinestyle={{ boxShadow: '...' }}overrides on the.glass-panel-strongelement (the class's defaultvar(--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
.cardblock instyles/globals.cssis a single rule; deleting it is a clean diff (verified by reading L750-L770 area — no other.cardcompound selectors exist). - Brief 7's grep command shape: modeled on the existing
forbidden-modal-shell-without-primitivejob (ci.ymlL200-L226), which usesgrep -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 forcomponents/Layout.jsandcomponents/ui/TopSearchBar.js(the chrome blocks).
- Brief 1's 4-layer recipe matches
- 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.