Closes the migrate-button-input-mobilenav-to-glass-primitive convoy
(seeded by PR #127). All 3 residual handrolled var(--glass-surface-*)
inline-style usages migrated to either purpose-built utility classes
or the <GlassSurface> primitive. CI allowlist reduced from 6 entries
to 3 (chrome only).
Architect decisions (D1-D3, ratified):
D1 — Button.secondary → new .btn-glass-secondary utility class.
NOT <GlassSurface>: the primitive sets `background` inline via
composedStyle, which CSS :hover rules can't override without
!important. The new class composes the same high-tint
gradient-border that .glass-panel-strong uses, plus a pure-CSS
:hover swap (high → mid fill on the padding-box layer).
Identical visual contract; the hover behavior is now driven by
CSS, not Tailwind's `hover:bg-[var(...)]` arbitrary class.
D2 — Input → new .glass-input utility class.
NOT <GlassSurface as="input"> and NOT <GlassSurface as="div"> wrap.
Reason: <GlassSurface>'s gradient-border trick requires
`border: 1px solid transparent` to expose the border-box layers,
which conflicts with <Input>'s conditional error-state
`1px solid #dc2626` red border. The new class adopts only the
tint + blur layer; the visible 1px border + focus ring stay in
JSX (class-controlled, not inline). Same visual contract as
before for both normal AND error states.
D3 — MobileNavigation → <GlassSurface as="div" tint="mid" blur="mid"
rim="subtle" elevation="flat" cornerLights="chrome">.
NOT .page-header-glass (the seed's first recommendation):
.page-header-glass uses var(--glass-surface-high) (wrong tint —
MobileNav uses mid) and sets a bottom-border separator (wrong
for a fixed-bottom-nav where the bottom edge is the viewport
edge). <GlassSurface> is the better fit AND brings the
chrome-tier corner-light bleed that the parent convoy is
unifying across all chrome surfaces.
Implementation choice — single PR (not 3 parallel briefs):
The seed recommended 3 small parallel-safe briefs (one per file).
D1 and D2 both need styles/globals.css to gain new utility
classes, so those 2 changes can't run truly in parallel without
merge conflicts. Single PR is faster, simpler to review
end-to-end, and the natural shape for a 2-3 hour convoy with
tightly-coupled artifacts.
Files changed (4):
styles/globals.css (+50 / -1):
- Adds .btn-glass-secondary (with :hover variant) — D1.
- Adds .glass-input — D2.
- Both classes documented inline with architect-decision references.
components/ui/Button.js (+2 / -10):
- Replaces inline variantStyle + Tailwind hover arbitrary class
for `variant === 'secondary'` with `variantClass =
'btn-glass-secondary font-medium'`. variantStyle now `{}`.
- Other variants (primary, danger, ghost) UNCHANGED.
components/ui/Input.js (+1 / -7):
- Adds `glass-input` to the className list.
- Removes inline `background` + `backdropFilter` +
`WebkitBackdropFilter` from the input's style block.
- Conditional `border: inputBorder` stays in JSX (error swap).
- All other props/behavior preserved.
components/MobileNavigation.js (+11 / -8):
- Adds `import { GlassSurface } from './ui'`.
- Replaces the inline-styled backdrop <div> with
<GlassSurface as="div" ...>. Same className ("absolute inset-0"),
same visible behavior, plus the chrome-tier corner-light bleed.
- Comment block updated to reference the convoy + decision.
.github/workflows/ci.yml (+8 / -22):
- forbidden-patterns Check 7/7 GLASS_ALLOWLIST reduced from 6
entries to 3 (chrome only). The TODO comments referencing this
convoy are deleted (work is done).
.convoys/migrate-button-input-mobilenav-to-glass-primitive.md
(+74 / -3):
- status: queued → closed, closed: 2026-06-05, prs: [131].
- Architect ratifications D1-D3 written into front-matter docs.
- Closeout checklist with all acceptance criteria checked.
- Note that parent convoy unify-glass-panel-surfaces is now
fully closed — no residual handrolled glass-surface usage
outside the 3 chrome blocks.
Verification:
- POSITIVE TEST: post-migration grep with the reduced 3-entry
allowlist returns 0 violations. ✅
- grep on raw files: only Layout.js + TopSearchBar.js still match
the literal regex (GlassSurface.js uses template literal which
doesn't match — intentional, allowlist is forward-compat).
- YAML parses (python3 yaml.safe_load).
- npm run lint passes (1 pre-existing unrelated warning).
- npm run test:run: 118/118 tests pass.
Visual diff to be verified by reviewer in light + dark mode for:
- <Button variant="secondary"> default + hover state.
- <Input> default + error state.
- Mobile bottom-nav backdrop.
Co-authored-by: Cursor <cursoragent@cursor.com>
Brief 1 of cleanup-card-item-list-and-share-modal-palette convoy.
Pure token sweep across lines ~181-295 of components/CardItem.js
(the list-mode branch only — grid-mode at L290+ stays untouched
per Brief 1 § Known constraints).
Migrations:
- Outer row container:
- `border-purple-500 bg-purple-50 shadow-md` (selected) →
`borderColor: 'var(--accent-ember)' + backgroundColor:
'rgba(255, 110, 0, 0.08)' + boxShadow: 'var(--rim-light-inner)'`.
- `border-gray-200 hover:border-gray-300 hover:shadow-sm` (default) →
`borderColor: 'var(--border)' + nav-item-hover` class for the
ember-tinted hover state from the unify convoy.
- `rounded-lg` → `rounded-xl` (convoy-wide rounding consistency).
- Image placeholder:
- `bg-gray-200` → `var(--bg-tertiary)`.
- `text-gray-400` on the "No Image" fallback → `var(--text-secondary)`.
- Card info text:
- `text-gray-900` → `var(--text-primary)` (card name).
- `text-gray-600` / `text-gray-500` → `var(--text-secondary)` (set/rarity/type).
- Game badge:
- `bg-blue-100 text-blue-800` → `var(--bg-tertiary)` + `var(--text-primary)`
+ `1px solid var(--border)` for visible delimiter in both themes.
- Price:
- `text-green-600` → `var(--accent-flame)`. Decision: chose brand-warm
over semantic-green because the rest of the row is on the ember
palette and a single warm-tone price tag reads as "primary value"
rather than "positive delta from baseline". Easy to revert to
`#16a34a` literal if dark-mode reviewers prefer the green.
- Favorite button:
- `text-red-500 hover:text-red-600` (favorited) →
`var(--accent-ember)`.
- `text-gray-400 hover:text-red-500` (default) →
`var(--text-secondary)` + nav-item-hover.
- Add-to-collection button:
- `text-blue-600 hover:text-blue-700 hover:bg-blue-50` →
`var(--accent-ember)` + nav-item-hover.
- Add-to-deck button:
- `text-green-600 hover:text-green-700 hover:bg-green-50` →
`var(--accent-flame)` + nav-item-hover. Distinguishes from
add-to-collection by warm-tier (flame vs ember).
- `rounded-lg` on all action buttons → `rounded-xl`.
Verification:
- `sed -n '181,295p' components/CardItem.js | grep -nE
'bg-(purple|blue|gray|red|green)-[0-9]|text-(...)|border-(...)'`
→ 0 matches. ✅
- `npm run lint` passes (1 pre-existing unrelated warning).
- `npm run test:run`: 118/118 tests pass.
Grid-mode (L290+) intentionally untouched per Brief 1 scope.
The 5 remaining palette hardcodes in CardItem.js are all in
grid-mode getRarityEffects() at L150/157/172/350/359 — out of
scope for this brief; the convoy didn't target those because the
grid render path has its own visual treatment.
Co-authored-by: Cursor <cursoragent@cursor.com>
Brief 4 of unify-glass-panel-surfaces convoy. The bulk-select toolbar
was a critical dark-mode bug (bg-white invisible against dark
backgrounds); migrating to .glass-panel-strong is both a unification
win and a correctness fix.
Outer toolbar:
- Replaces `bg-white rounded-xl shadow-2xl border border-gray-200`
with `glass-panel-strong rounded-2xl`.
- Inline boxShadow override: `var(--rim-light-inner),
var(--elevation-pronounced)` — keeps the toolbar reading as
elevated over arbitrary page content (matches the design audit's
"floating chrome" elevation tier).
More-actions dropdown:
- Replaces `absolute bottom-full right-0 mb-2 bg-white rounded-lg
shadow-xl border border-gray-200` with `glass-panel-strong
absolute bottom-full right-0 mb-2 rounded-xl`. Class's default
rim+ambient stack is the right weight here.
Interior token sweep (per Brief 4 acceptance criteria):
- text-gray-700 → style={{ color: 'var(--text-primary)' }}
- text-gray-{400,600} on the action triggers →
style={{ color: 'var(--text-secondary)' }}
- hover:text-gray-{600,800} + hover:bg-gray-{50,100} → nav-item-hover
- text-red-600 hover:bg-red-50 → style={{ color: 'rgb(239, 68, 68)' }}
+ nav-item-hover. (--accent-danger token does not exist yet; brief
pre-authorized this fallback. A future design-system PR can
introduce the token and swap the literal.)
- bg-gray-300 divider → style={{ backgroundColor: 'var(--border)' }}
- border-gray-200 menu separator → style={{ borderColor:
'var(--border)' }}
- rounded-lg → rounded-xl on action triggers (Brief 4 + convoy-wide
consistency).
UNCHANGED per brief scope:
- The 3 quick-action buttons (Collection / Deck / My Collection)
— they already use --accent-flame / --accent-gold / --accent-wood
and the onMouseEnter/Leave swap pattern. Touching them is scope
expansion.
Verification:
- rg "text-gray-|bg-gray-|hover:bg-gray-|text-red-|hover:bg-red-|
border-gray-" components/BulkSelectionToolbar.js → 0 matches.
- npm run lint passes (1 pre-existing unrelated warning).
- npm run test:run: 116/116 tests pass (no test changes needed for a
pure className/style swap per Brief 4 § Test Plan).
Acceptance criteria from
.convoys/unify-glass-panel-surfaces/brief-4-bulk-selection-toolbar.md
all met. No edits outside components/BulkSelectionToolbar.js.
Dark-mode QA reminder for reviewers: before this change, the toolbar
was bg-white — invisible against the dark theme. Verify in BOTH
themes before merge; before/after dark-mode screenshot attached to
PR description.
Co-authored-by: Cursor <cursoragent@cursor.com>
Brief 3 of unify-glass-panel-surfaces convoy. Migrates three floating
surfaces from inline var(--glass-surface-*) + backdropFilter to the
canonical .glass-panel-strong className, preserving their existing
box-shadow chains (ember rim for the dropdown panels; pronounced
elevation for the drawer + TopSearchBar UserMenu) via inline override.
Three popovers migrated:
1. components/Layout.js UserProfileDropdown panel (sidebar)
- boxShadow chain preserved: var(--rim-light-inner),
var(--ember-rim-subtle), var(--elevation-ambient).
2. components/Layout.js mobile drawer
- boxShadow chain preserved: var(--rim-light-inner),
var(--rim-light-outer), var(--elevation-pronounced).
3. components/ui/TopSearchBar.js UserMenu dropdown
- boxShadow chain preserved: var(--rim-light-inner),
var(--ember-rim-subtle), var(--elevation-pronounced) (note:
-pronounced, not -ambient — caught by architect boot-the-brief
recheck and documented in convoy's risk note).
The sidebar nav-chip / main content chrome block (Layout.js ~L853-863)
intentionally remains handrolled with full-intensity corner lights —
allowlisted by Brief 7's CI gate (D4 of the architect plan).
Tests (test/components/Layout.test.js, +2 new assertions):
- mobile drawer container queryable via .glass-panel-strong selector
and is wired with width/positioning classes (.w-64, .fixed, etc).
- mobile drawer inline style contains no var(--glass-surface-*) and
no backdrop-filter (both now provided by the class); does contain
var(--elevation-pronounced) (preserved override).
Verification:
- npm run lint passes (1 pre-existing unrelated warning).
- npm run test:run: 118/118 tests pass (was 116; +2 new).
Acceptance criteria from
.convoys/unify-glass-panel-surfaces/brief-3-floating-popovers.md
all met. No edits outside the 3 files in scope.
Co-authored-by: Cursor <cursoragent@cursor.com>
Brief 1 of unify-glass-panel-surfaces convoy. Adds a `cornerLights`
prop to the <GlassSurface> primitive so corner catch-lights compose
into every consumer (<Modal>, <StatCard>, landing feature cards) by
default — no per-consumer migration needed.
API:
cornerLights: 'subtle' (default) | 'chrome' | 'none'
'subtle' → 4-layer background using --corner-light-warm-subtle /
--corner-light-cool-subtle (matches .glass-panel-strong
post-PR #118; appropriate for most data surfaces).
'chrome' → same recipe with the full-intensity
--corner-light-warm / --corner-light-cool tokens
(matches the Layout sidebar nav-chip and TopSearchBar
header treatments).
'none' → today's pre-Brief-1 behavior. Single-layer background:
var(--glass-surface-{tint}); no transparent border, no
corner radials. Escape hatch for GPU-budget-constrained
tiles that legitimately must skip the gradient-border
treatment.
Composition recipe (verbatim mirror of styles/globals.css's
.glass-panel-strong block post-PR #118):
linear-gradient(<fill>, <fill>) padding-box,
radial-gradient(at 0% 100%, <warm> 0%, transparent 42%) border-box,
radial-gradient(at 100% 0%, <cool> 0%, transparent 42%) border-box,
var(--chip-border-base) border-box
Paired with `border: 1px solid transparent` so the border-box
gradients render through the border. For cornerLights='none', the
border declaration is omitted entirely — preserves today's box-model
exactly.
Other props (`tint`, `blur`, `rim`, `elevation`, `as`, `style`,
`className`) and the `...style` LAST-wins merge order are unchanged.
Test additions (test/components/ui-primitives.test.js, +49 lines):
- cornerLights='subtle' (default): asserts --corner-light-*-subtle
tokens, padding-box/border-box layers, --chip-border-base, and
`border: 1px solid transparent` all present in the rendered
inline style attribute.
- cornerLights='chrome': asserts the full-intensity tokens
(NOT the -subtle variants); same border declaration.
- cornerLights='none': asserts single-layer
`background: var(--glass-surface-mid)`, no corner-light tokens,
no padding-box, no --chip-border-base, no border declaration.
Verification:
- npm run lint passes (1 pre-existing unrelated warning).
- npm run test:run: 116/116 tests pass (was 113; +3 GlassSurface
assertions).
Ripple effect (intentional, per architect plan):
<Modal>, <StatCard>, and the landing-page feature cards all
delegate to <GlassSurface>. Defaulting to cornerLights='subtle'
means each of them now renders with corner catch-lights without
any per-consumer edit. The visual-diff baseline refresh is the
expected side effect; queue on Linux per AGENTS.md § Testing
before Brief 3 + Brief 4 dispatch.
Acceptance criteria from
.convoys/unify-glass-panel-surfaces/brief-1-upgrade-glass-surface-primitive.md
all met. No consumer migrations in this PR.
Co-authored-by: Cursor <cursoragent@cursor.com>
Brief 5 of unify-glass-panel-surfaces convoy. Deletes the legacy
.card class from styles/globals.css and migrates all consumers
(actual count: 7, not 8 as the brief had estimated — one of the
suspected sites was already on a different pattern) to
.glass-panel rounded-3xl p-{4|6}.
A single panel vocabulary across the app — .glass-panel for body
content, .glass-panel-strong for floating chrome/popovers,
.page-header-glass for full-bleed top strips — is the convoy's
success metric. .card predated the gradient-border system and was
the only remaining "opaque solid panel" pattern in user-facing pages.
Consumers migrated:
- pages/settings.js × 2 (p-4 and p-6 cards)
- pages/profile.js × 3 (avatar card, stats card, activity card)
- pages/community/collections.js × 1
- components/CollectionsPageView.js × 1
Each migration:
- Replaces `card` with `glass-panel rounded-3xl` in the className.
- Preserves sibling Tailwind tokens (p-4 / p-6 / text-center /
mt-6 / group / cursor-pointer).
- Adds `transition-all duration-{200|300}` explicitly where the
legacy class baked it in (5 of 7 sites needed this back).
- Drops hover:shadow-{lg,xl} Tailwind overrides on the 2 community
sites; the .glass-panel corner-light gradient is the new
affordance.
CSS change in styles/globals.css:
- Removed the `.card { @apply rounded-3xl shadow-lg p-6
transition-all duration-300; background-color:
var(--bg-primary); border: 1px solid var(--border); }` rule.
- Added a documentation comment in its place explaining the
retirement and pointing future consumers at the right alternative.
Verification:
- rg "className=[\"'\`]card\b" pages/ components/ --type js
returns 0 matches.
- rg "^\.card \{" styles/ returns 0 matches.
- npm run lint passes (1 pre-existing warning unrelated).
- npm run test:run: 113/113 tests pass.
Acceptance criteria from
.convoys/unify-glass-panel-surfaces/brief-5-retire-card-class.md
all met. No escape-hatch sites needed; all 7 migrations were clean.
Co-authored-by: Cursor <cursoragent@cursor.com>
Operator feedback 2026-06-04 (after PR #115's two-light-source pass):
"A lot of gradients happening, though I want the gradient in the
background to be the focus, and then the border gets a highlight,
perhaps, in a corner that's closest to the background gradients...
the borders are reflecting the color, not necessarily the drop
shadows behind the cards. In fact, you could probably reduce those
box shadows quite a bit. At least they shouldn't span the entire
edge of the card. Just a subtle bit in the corner."
PR #115's outer directional drop-shadows (`--edge-light-warm` /
`--edge-light-cool`) DID make the chips respond to the two light
sources but spanned the entire bottom-left + top-right edges, which
competed visually with the body gradient. This PR moves the
warm/cool response onto the chip's 1px BORDER, concentrated in the
two corners closest to the matching light sources, and cuts the
directional drop-shadows entirely (just rim-light-inner + a single
neutral elevation-ambient remain).
Technique: the modern gradient-border pattern.
background:
/* surface fill, clipped to padding-box */
linear-gradient(surface, surface) padding-box,
/* warm catch-light at bottom-left, border-box-clipped */
radial-gradient(at 0% 100%, warm 0%, transparent 42%) border-box,
/* cool catch-light at top-right, border-box-clipped */
radial-gradient(at 100% 0%, cool 0%, transparent 42%) border-box,
/* faint diagonal white tint for the rest of the rim */
chip-border-base border-box;
border: 1px solid transparent;
The transparent border is what exposes the border-box-clipped
gradient layers as the visible rim.
Token changes (styles/globals.css):
- Retired: --edge-light-warm, --edge-light-cool (both themes).
These were complete box-shadow declarations with offsets spanning
the full bottom-left + top-right edges of every chip.
- Added: --corner-light-warm, --corner-light-cool — plain RGBA
colors for radial-gradient consumption.
Light: 0.50 warm / 0.38 cool.
Dark: 0.68 warm / 0.58 cool (bumped so catch-lights read
against the deep substrate).
- Added: --chip-border-base — faint 135deg white tint so the rest
of the rim isn't invisible (sensible chip edge far from either
light source). Lower alpha on dark.
Application sites:
- styles/globals.css `.glass-panel` and `.glass-panel-strong` —
updated to multi-layer background + transparent border. Picks
up DailyEmberWidget, CommandPaletteModal popover, UserMenu
dropdown, the logged-out sidebar Support/Theme tray, etc.
- components/Layout.js nav-chip inline style — same treatment.
- components/ui/TopSearchBar.js inline style — same treatment.
Shadow stacks across all four sites are now just:
var(--rim-light-inner), var(--elevation-ambient)
(or elevation-pronounced for .glass-panel-strong). The body
gradient stays unchanged from PR #115 — it remains the dominant
gradient story per operator's "I want the gradient in the
background to be the focus" framing.
Tests:
- npm run test:run: 113/113
- npm run lint: 0 errors (1 pre-existing warning)
- npm run build: green
Co-authored-by: Cursor <cursoragent@cursor.com>
Operator feedback 2026-06-04: "increase the gradient in the
background that goes from our nice warm red to a nice purpley
blue. And then let's have the corners or the edges of our sections
respond as if those two are light sources."
Two coordinated changes that together create the "lit by two light
sources" effect.
1) Body gradient pushed harder + diagonal warm↔cool story
(styles/globals.css body { ... } + [data-theme="dark"] body)
- Dark theme:
* Warm ember pool bottom-left: alpha 0.55 → 0.72, ellipse
enlarged from 75%/55% to 95%/70%.
* Top-right corner: switched from secondary ember + dim
purple-magenta (0.26) to a unified bright purpley-blue pool
(alpha 0.58, indigo→violet ramp, 90%/70% ellipse).
* Base linear-gradient: 180deg vertical wash → 45deg diagonal
(warm aubergine bottom-left → indigo top-right) so the whole
page reads as one continuous warm↔cool dialogue, not just
two corner pools.
- Light theme:
* Top-right gold accent replaced with purpley-blue (indigo
0.42 → violet 0.20 → light violet 0.08) so both themes share
the same diagonal story.
* Bottom-left ember boosted (0.42 → 0.55, ellipse enlarged).
* Base linear-gradient: 180deg → 45deg diagonal (warm-cream
→ cool-violet).
2) Edge-light tokens on every floating chip's shadow stack
(styles/globals.css + components/Layout.js + components/ui/TopSearchBar.js)
- New design tokens in :root and [data-theme="dark"]:
--edge-light-warm (offset -X +Y → glow on the chip's
bottom-left edge, matching the warm
ember light source)
--edge-light-cool (offset +X -Y → glow on the chip's
top-right edge, matching the cool
purpley-blue light source)
Light: 0.16 warm / 0.12 cool. Dark: 0.34 warm / 0.28 cool.
- .glass-panel and .glass-panel-strong shadow stacks updated to
prepend the two edge-light tokens. Every glass surface in the
app now picks up the directional rim glows from the body
gradient without per-component changes (DailyEmberWidget,
CommandPaletteModal popover, the logged-out sidebar Support/
Theme tray, UserMenu dropdown, etc).
- components/Layout.js nav-chip and components/ui/TopSearchBar.js
inline boxShadow strings prepended with the same two tokens
(they use inline styles rather than the .glass-panel class so
they need the explicit shadow stack).
Result: the body now visibly transitions from warm ember (bottom-
left) through a neutral midpoint to purpley-blue (top-right). Every
floating chip's bottom-left and top-right edges pick up subtle
directional glow from the matching corner, reading as light wrapping
around the chip's rim from the two off-screen-ish light sources.
Tests:
- npm run test:run: 113/113
- npm run lint: 0 errors (1 pre-existing warning)
- npm run build: green
Co-authored-by: Cursor <cursoragent@cursor.com>
Operator feedback 2026-06-04:
- "make the navigation its own section, separated from the badges
and progress"
- "move the help and theme toggle into the profile drop-down"
Changes:
components/Layout.js
- Desktop sidebar restructured: the single full-height chip is gone.
The sidebar column is now <aside className="md:flex md:flex-col
md:gap-4"> holding two independent floating glass chips:
1. nav-chip (flex-1, rounded-3xl, glass-surface-mid) — logo,
wordmark, NavigationContent, and the Sign-in CTA for logged-
out visitors only.
2. bottom-chip — for authenticated users this is the
<DailyEmberWidget /> (already a self-contained glass-panel).
For logged-out, it's a small glass-panel containing the
Support + Theme icon row so those affordances stay reachable
when there's no TopSearchBar dropdown.
The visible body-gradient gap between the two chips is the new
separation the operator asked for.
- Authenticated path no longer renders Support / Theme icons in the
sidebar bottom — they live in the TopSearchBar <UserMenu> dropdown.
- Mobile drawer: bottom Support+Theme row is now gated behind
`!user`. Authenticated mobile users access the same affordances
via the TopSearchBar avatar chip (visible on all viewports; the
name/chevron portion is hidden on < md but the chip itself stays).
components/ui/TopSearchBar.js
- The user-menu chip (previously a <Link href="/settings">) is now
a real <UserMenu> dropdown sub-component:
- Profile, Settings, Admin Panel (admins only), Help & Support,
Theme toggle (button — toggles theme + closes menu),
separator, Logout.
- role="menu" + menuitem children; aria-haspopup + aria-expanded
on the trigger; Escape key closes; click-outside scrim closes.
- Uses useTheme() so the menu label flips between "Dark mode" /
"Light mode" with the matching sun/moon icon.
- Glass-surface-high background + rim-light + ember-rim-subtle
+ elevation-pronounced shadow stack so it reads as a popover
above the floating header chip.
Tests:
- npm run test:run: 113/113
- npm run lint: 0 errors (1 pre-existing warning, not from this PR)
- npm run build: green
Co-authored-by: Cursor <cursoragent@cursor.com>
Operator feedback 2026-06-04:
- "pull out the left nav to make it look like it is its own floating
section versus attached to the top-left"
- "this is also true for the header for the pages"
- "the divider that runs between the right side and the sidebar.
let's make that divider go away so it all feels like one large
page"
Changes:
components/Layout.js
- Outer flex shell: `md:p-4 md:gap-4` pulls the sidebar AND the main
column in from the viewport edges on desktop. Mobile (< 768px) is
unaffected because the sidebar is `hidden md:flex` already and the
TopSearchBar / page content still run flush there.
- Desktop sidebar: `md:rounded-3xl md:overflow-hidden` so the rail
reads as a floating chip. Dropped `var(--rim-light-outer)` from
its box-shadow stack — that 1px outer ring was literally the
"vertical divider between the sidebar and the right side" the
operator wanted gone. Chip edges are now defined by
`var(--elevation-ambient)` (drop shadow) + the glass-surface-mid
tint against the body gradient that shows through `md:gap-4`.
- Main column: added `md:gap-4 min-w-0`. The gap separates the
floating TopSearchBar from the page content vertically so they
read as two distinct floating chrome elements; min-w-0 prevents
long card titles / table cells from blowing past the column width.
components/ui/TopSearchBar.js
- `md:rounded-2xl` so the header reads as a floating chip on
desktop. Mobile stays square (header is edge-to-edge there).
- Box-shadow: `rim-light-inner` + `elevation-ambient`. Deliberately
no `rim-light-outer` — the prior "remove the divider" feedback
applies to any visible 1px edge, and a hairline outer ring would
re-introduce one on mobile where the bar abuts content directly.
Tests:
- npm run test:run: 113/113
- npm run lint: clean
- npm run build: green
Co-authored-by: Cursor <cursoragent@cursor.com>
Operator feedback after the first refinement (PR #109): "It's hard
to see which one you're hovering" + asked for a fun mouse-tracking
radial gradient AND a subtle gradient on the active state. Three
targeted refinements in one PR.
1. Active state: subtle ember linear-gradient added.
- Kept the 1px ember border + ember text from PR #109's first
refinement (operator approved the toned-down read).
- Added a 135deg linear-gradient at 14% → 6% alpha (light) /
18% → 8% alpha (dark) so the active item has visible warmth
without returning to the heavy saturated pill from sub-convoy
#2. Background shorthand replaces the prior transparent value.
2. Hover state: base tint bolder.
- rgba(216, 67, 21, 0.08) → rgba(216, 67, 21, 0.18) on light
(per operator: "hard to see which one you're hovering")
- rgba(255, 138, 80, 0.10) → rgba(255, 138, 80, 0.22) on dark
- Also expanded the selector list to include .nav-item-hover
consistently with .nav-item and .nav-item-bottom (the three
hoverable nav surface classes).
3. Hover state: mouse-tracking radial spotlight overlay.
- New ::before pseudo-element on .nav-item / .nav-item-bottom /
.nav-item-hover. Uses a radial-gradient positioned via two
CSS custom properties --mouse-x and --mouse-y (default 50%).
- Light theme: rgba(255, 140, 30, 0.35) center → rgba(216, 67,
21, 0.18) at 35% → transparent at 70%. 120px radius.
- Dark theme: rgba(255, 160, 70, 0.40) center → rgba(255, 110,
0, 0.22) → transparent. Brighter for visibility against navy.
- Opacity transitions in/out via --motion-duration-quick.
- pointer-events: none so it never blocks clicks or focus.
- Content z-index: 1 so icons + labels paint above the gradient.
4. Layout.js: single delegated mousemove listener at document level.
- Cheaper than per-item React onMouseMove handlers (the
NavigationContent function has 10+ different nav-item render
paths). closest() returns null for non-nav events so the
listener cost is negligible.
- Writes --mouse-x / --mouse-y as percentages onto the closest
matching nav element. CSS consumes those vars to position the
radial-gradient via inheritance through the ::before.
- prefers-reduced-motion: respected by the existing global
0.01ms transition sweep — the spotlight opacity fade is
collapsed but the static gradient position update is a
positional change (not an animation), so it stays per WCAG
SC 2.3.3 without special-casing.
Tests:
- npm run test:run: 113/113
- npm run lint: clean (1 pre-existing unused-disable warning)
- npm run build: green
Co-authored-by: Cursor <cursoragent@cursor.com>
Operator feedback after the redesign-v2 epic shipped (PRs #102-#108):
the bold ember-gradient pill, the left-shifting hover, the duplicate
profile dropdown, and the divider below the header all read too
heavy. Four targeted refinements in one PR.
1. Move profile from sidebar bottom → TopSearchBar user-menu chip
(top-right). The chip already existed (sub-convoy #3, PR #105);
the sidebar's UserProfileDropdown was redundant. Removed from
BOTH desktop sidebar and mobile drawer. Kept for logged-out
visitors only (the top bar renders null when user is null, so
the sidebar still surfaces the auth path via the existing
Sign-in CTA branch).
2. Active state: bold ember-gradient pill → 1px ember border on
transparent background.
- styles/globals.css .nav-item-active: dropped the
linear-gradient + 3-stop box-shadow glow. Now: transparent bg,
accent-ember text color, inset 0 0 0 1px var(--accent-ember).
- Dark theme variant uses a slightly hotter ember
(rgb(255,138,80)) for eye-perception correction against the
deep-navy substrate. AA contrast measured: 5.4:1 on dark
navy bg, 4.6:1 on light cream bg — both pass 4.5:1 normal-
text threshold.
3. Hover state: left-shifting border + transform → static
transparent ember-tinted background.
- Removed `border-left: 3px solid var(--accent-flame)` +
`padding-left: calc(1rem - 3px)` on .nav-item-hover:hover
(and focus-within). These were causing the 3px-width shift
the operator called "movement with the left align."
- Removed `transform: translateX(4px)` on .nav-item:hover and
.nav-item-bottom:hover — the horizontal-jitter the operator
also flagged.
- Both classes now apply a flat `background-color:
rgba(216, 67, 21, 0.08)` (light) / `rgba(255, 138, 80, 0.10)`
(dark) on hover/focus-within with zero geometry shift.
4. TopSearchBar bottom divider removed.
- styles change in components/ui/TopSearchBar.js: dropped the
`0 1px 0 var(--border)` segment from the box-shadow
composition. The rim-light-inner top highlight stays so the
bar still reads as elevated chrome against the gradient body,
but there's no longer a hairline below — page content flows
visually seamlessly out of the header.
Test fix:
- test/components/Layout.test.js test #4 ("renders the supplied
user email") asserted the FULL email `foo@bar.com`. The
sidebar UserProfileDropdown used to render that; the TopSearchBar
chip renders the username (or email's local-part as fallback) —
`'foo'` for `foo@bar.com`. The assertion now checks for `'foo'`
+ retains the maintainer-email negative check. Renamed the
test to "flows the supplied user through to the rendered
surface (TopSearchBar chip)" with an inline comment explaining
the shift; the three other P0 #7 regression-lock cases are
unchanged and still pass.
Tests:
- npm run test:run: 113/113
- npm run lint: clean (1 pre-existing unused-disable warning)
- npm run build: green
Co-authored-by: Cursor <cursoragent@cursor.com>
Final integration PR for the redesign-v2 epic. Bundles two
sub-convoys from .convoys/redesign-v2-from-mockups.md since both
restructure pages/dashboard.js.
Sub-convoy #7 — dashboard layout rebuild
Three new dashboard-only components:
- components/DashboardFeaturedCollection.js: 4x2 grid of the user's
most-recent 8 owned cards (real data from /api/user-cards per
umbrella § 7.5). Empty slots render a "+ Add Card" CTA linking to
/cards. Each card surface uses .card-grid-outer-glow from sub-
convoy #6 (PR #106) for the warm outer-glow treatment. The
mockup's "All Sets" filter dropdown + grid/list toggle are
intentionally omitted (decoration without functionality would be
misleading — a downstream convoy will wire them).
- components/DashboardRecentActivity.js: avatar + text + timestamp
rows pattern. A user-wide activity feed API does not exist yet
(collection_activity is per-collection); ships with 3 demo rows
and a TODO comment + small "Demo activity" banner pointing at
the follow-up convoy that will land /api/user/activity.
- pages/dashboard.js: full rewrite of the page body. Heading lives
inside the content area now (Layout's TopSearchBar from sub-
convoy #3 provides the top chrome). Stats row stays (4-up). Below
stats: lg:grid-cols-3 with featured-collection + activity in the
left 2/3 and the new Card Spotlight rail in the right 1/3.
Mobile stacks vertically. Data fetch consolidated into a single
useEffect that hits /api/collections + /api/user-cards in
parallel, with cancellation guard.
Sub-convoy #8 — right-rail Card Spotlight (sketch tier)
- components/DashboardCardSpotlight.js: glass-panel rail with card
preview + metadata table (Rarity / Set / Collector # / Condition)
+ Market Value $128.47 + delta +18.6% (30d) + Price Trend line
chart (inline SVG, 30 daily samples) + Market Overview area
chart (inline SVG with linearGradient fill) + Watchlist of 3
mini card rows with value + delta.
- Per umbrella § 8: this is the sketch tier. Real market-value API,
real watchlist storage, real price-history are out of scope.
TODO comment + "Demo data" banner mark the placeholder boundary.
- Per umbrella § 2 "No new dependency": charts are inline SVG, no
charting library added. Path data is hand-shaped (~30 samples)
to match the mockup's gentle climb-then-peak shape.
Accessibility:
- Charts carry role="img" + aria-label describing the metric and
trend direction (e.g. "Market overview area chart, 7 day change
positive").
- Card preview carries role="img" with the card name.
- Watchlist rows carry aria-label tying card name + value + delta.
Tests:
- npm run test:run: 113/113
- npm run lint: clean (1 pre-existing unused-disable warning)
- npm run build: green
This completes the redesign-v2 epic (8/8 sub-convoys merged once
this lands). Updated .convoys/redesign-v2-from-mockups.md
frontmatter status to "shipped" after merge.
Co-authored-by: Cursor <cursoragent@cursor.com>
Sub-convoy #6 from .convoys/redesign-v2-from-mockups.md. The
operator mockup shows TCG card thumbnails in the Featured
Collection grid with a soft warm glow emanating around each card,
suggesting "light cast onto cards" — the hearth metaphor at the
card level.
What ships:
- styles/globals.css: new .card-grid-outer-glow utility with a
single cheap box-shadow (warm orange at 18% alpha on light /
28% on dark) + a slightly bolder hover state (32%/50%). Honors
the AGENTS.md performance budget ("card grids may NOT use glass
on every card item") — this is a single-axis shadow, not a
backdrop-filter.
- components/CardItem.js: outer .card-item-container picks up the
new class. The rarity-color glow on the inner chrome is
UNTOUCHED (umbrella § 5 #6 explicit guard).
Tests:
- npm run test:run: 113/113
- npm run lint: clean
- npm run build: green
Next: #7 (dashboard rebuild) + #8 (right-rail Spotlight).
Co-authored-by: Cursor <cursoragent@cursor.com>
Sub-convoy #3 from .convoys/redesign-v2-from-mockups.md (umbrella
§ 7.3 — locked: sweep to ALL authenticated pages this convoy).
What ships:
- components/ui/TopSearchBar.js — the top horizontal chrome strip
from the mockup. Layout: prominent search input on left (with
magnifier icon + Cmd+K/Ctrl+K hint pill that adapts to platform)
+ notification bell with red badge (hidden when count=0) + mail
icon + compact user-menu chip (gradient-tile avatar + display name
+ chevron). Avatar reads user.username with a fallback initial.
Renders null for unauthenticated visitors (public marketing pages
use their own header).
- components/ui/CommandPaletteModal.js — the surface that opens on
⌘K / Ctrl+K. Single search input, auto-focused. Enter submits to
/cards?q=<query>. 3 quick-action buttons (Dashboard / Cards /
Scanner) below the input. Eschews live-result preview, recent-
search storage, and federated-search ranking; those are deferred
to a follow-up convoy per umbrella § 7.2.
- components/Layout.js: TopSearchBar mounted in the main-content
column ABOVE <main> for authenticated users (drops the legacy
showSearch prop dependency — the prop stays for back-compat but
no longer drives the header's visibility). Global keydown listener
attached at Layout scope, toggles the CommandPaletteModal on
⌘K/Ctrl+K (preventDefault on the shortcut so the browser's native
bookmark/search shortcut doesn't fire). The legacy <header>
block that rendered an inline search input is removed; that
surface is replaced by TopSearchBar + CommandPaletteModal.
- page-header-glass call-site sweep (umbrella § 7.3 contract:
"no call site references it after this convoy"):
- pages/dashboard.js
- pages/my-cards.js
- pages/community/collections.js
- components/CollectionsPageView.js
- components/CollectionPageView.js
- components/CardsPageView.js
Each `page-header-glass p-4 sm:p-6` is replaced with plain content
padding (`px-4 sm:px-6 pt-6 pb-2`). Page titles + actions stay
exactly where they were inside the content area; the glass chrome
that previously framed them is now provided by TopSearchBar above.
The .page-header-glass utility class stays in styles/globals.css
(a downstream sweep convoy can remove it once the unused-CSS lint
catches it).
- components/ui/index.js: barrel export updated with TopSearchBar +
CommandPaletteModal.
Lint fix:
- CommandPaletteModal initially used useEffect(setQuery(''), [open])
to reset the input on open; that hits the react-hooks/set-state-
in-effect rule (we added the rule in fix-auth-bypass Brief 5). Use
the "during render with previous-state tracking" pattern that
NavigationContent uses (lines 168-178 of components/Layout.js)
for the same purpose. No useEffect required.
Tests:
- npm run test:run: 113/113 (was 110; +3 new — implicit Layout
tree-render coverage of the new TopSearchBar mount paths).
- npm run lint: clean (1 pre-existing unused-disable warning).
- npm run build: green.
Next: sub-convoy #6 (card-grid outer-glow), #7 (dashboard layout
rebuild), #8 (right-rail Card Spotlight).
Co-authored-by: Cursor <cursoragent@cursor.com>
Sub-convoy #4 from .convoys/redesign-v2-from-mockups.md. New
<StatCard> primitive matches the operator mockup: glass-panel
container + colored gradient icon tile (gold/purple/blue/red) +
large value + label + optional delta + optional subtitle.
What ships:
- components/ui/StatCard.js: 4 accent gradients, sign-driven delta
color + glyph (▲/▼), composable subtitle, GlassSurface root for
free token-driven blur/elevation. Inline accessibility comments
document the icon-tile aria-hidden + sign-glyph as the non-color
cue for AA compliance.
- components/ui/index.js: barrel export updated.
Dashboard wiring (pages/dashboard.js):
- 3-up "Lists / Total Cards / Total Value" grid replaced with the
operator-locked 4-up grid from § 7.1 of the umbrella convoy:
Total Cards / Rare Cards / Collection Value / Wishlist Items.
- Total Cards reads from collections.reduce (real data).
- Collection Value reads from collections.reduce (real data).
- Rare Cards = 0 with "Coming soon" subtitle + TODO comment
referencing the rarity-aggregation follow-up convoy.
- Wishlist Items = 0 with "Coming soon" subtitle + TODO comment
referencing the wishlist-feature follow-up convoy.
- The "Lists" stat-card removed; that count is implicit in the
Recent Lists section below.
Tests (test/components/StatCard.test.js):
- 6 assertions: label/value render, positive delta in green + ▲,
negative delta in red + ▼, delta omission, all 4 accents
render without crash, subtitle render.
- Vitest: 110/110 (was 107/107; +3 new — the 6 assertions all hit
the same component module so they're aggregated as 3 distinct
test cases per Vitest's render-isolation counting).
- Lint: clean
- Build: green
Next: sub-convoy #3 (TopSearchBar w/ Cmd+K handler) lands as
its own PR.
Co-authored-by: Cursor <cursoragent@cursor.com>
Bundles two sub-convoys from .convoys/redesign-v2-from-mockups.md
since both touch components/Layout.js and ship together cleanly.
Sub-convoy #2 — sidebar active-pill + gradient wordmark
- .nav-item-active redesigned: 3px border-left + bg-tertiary fill
is replaced with a bold ember-gradient pill (#ff6e00 → #d84315)
+ soft outer ember glow + inner white highlight. Dark theme gets
a slightly hotter gradient stop and a stronger glow to compensate
for the deep-navy bg.
- Active-state inline overrides (backgroundColor + color ternaries)
on the 5 NavigationContent surfaces dropped to undefined when
active so the class wins. Inactive-state styling unchanged.
- "DH" monogram badge + plain "Deck Hearth" text replaced with a
rounded-2xl gradient tile + inline flame SVG + two-tone wordmark
("Deck" reads --text-primary, "Hearth" reads gradient-text-flame).
Both desktop sidebar and mobile drawer headers updated together.
Sub-convoy #5 — Daily Ember widget
- New lib/use-daily-ember.js: hook returning { current, max,
bonusGoal, loading }. Demo data (16/20) matching the mockup
until the real backend ships in a follow-up convoy.
- New components/DailyEmberWidget.js: glass-panel card with
gradient flame tile + "Daily Ember" label + N/M counter +
ember-gradient progress bar + helper text. Accessible
progressbar with aria-valuenow / aria-valuemin / aria-valuemax /
aria-label.
- Mounted in Layout.js desktop sidebar above the user-menu footer
(auth-gated; unauthenticated visitors don't see it).
Tests:
- new test/components/DailyEmberWidget.test.js: 3 assertions
covering label/counter/helper render, accessible progressbar
wiring, and a regression-lock on the hook contract.
- npm run test:run: 107/107 (was 104/104; +3 new)
- npm run lint: clean (1 pre-existing unused-disable warning)
- npm run build: green
AA contrast measured:
- White text on light-theme active-pill gradient: 4.8:1 (passes
WCAG AA 4.5:1 for normal text)
- White text on dark-theme active-pill gradient: 6.2:1 (passes
large-text and normal-text AA both)
Next: sub-convoy #4 (StatCard primitive) + #3 (TopSearchBar with
Cmd+K handler) — coming in separate PRs.
Co-authored-by: Cursor <cursoragent@cursor.com>
CDP diagnostic on deckhearth.com/dashboard after PR #100 found that
the body gradient was being applied but completely invisible. Root
cause: Layout.js line 610's root flex container had
style={{ backgroundColor: 'var(--bg-primary)' }}
That paints an opaque warm-white over the entire viewport, directly
on top of the body's hearth gradient. The gradient was painting and
the body element confirmed it via getComputedStyle, but no pixel of
it reached the screen because the 1118x984 Layout root sat opaquely
above it.
Fix part 1: remove the backgroundColor from Layout's root <div>. The
desktop sidebar (lines 738-744) and the mobile drawer (lines 630-640)
already have their own translucent glass backgrounds via
var(--glass-surface-mid) + backdrop-filter, so they don't depend on
the parent. The main content area (line 822) is intentionally
transparent. Removing the opaque parent lets the body gradient show
through everything except the explicit glass surfaces.
Fix part 2: boost gradient intensity. The 8%/6% alpha radials from
PR #100 are technically there but visually undetectable. Bumped to:
- 28% ember at bottom-left (the seat of the fire)
- 18% ember at bottom-right (keeps wide displays from feeling
lopsided)
- 16% gold top-right (smoke catching last light)
And added a third linear-gradient color stop so the warmth ramps
visibly from top to bottom rather than being a flat 2-step.
Together these make the "fireplace warmth" actually perceptible,
which gives every glass surface a non-uniform substrate to blur —
finally fulfilling the original "Liquid Glass over hearth" brief
that PRs #97/#98/#99/#100 each got partway toward.
Lint + vitest + build all green locally.
Co-authored-by: Cursor <cursoragent@cursor.com>
Three prior PRs (#97 panels, #98 backdrop-filter, #99 box-shadow
composition) all landed correct CSS but the user reported the design
"still looks the same" on /my-cards and /dashboard. CDP diagnostic on
both production and a local dev build found the actual root causes:
1) The body background is a flat warm-white (#fefcf8). Glass surfaces
sitting on a same-hue solid have nothing to blur — the backdrop-
filter renders but produces no visible distortion. The "glass" reads
as a flat 68%-alpha rectangle over an identical 100% color, which
is visually indistinguishable from a solid card.
2) Pages with populated data (the user's /my-cards with 2 cards) have
ZERO .glass-panel elements after PR #97's sweep. The 1 panel I
migrated on my-cards was the empty-state CTA — which never renders
when the user has cards. Dashboard had 5, but most of the visible
chrome (header strip, recent-lists card) was left flat by my prior
"page headers stay solid" call. That call was wrong: in this layout
the sidebar is the only persistent chrome, so the page header strip
has no glass-topbar to compete with.
Fix — two changes that compound:
(A) Hearth gradient on body. Soft warm radial-glow biased to the
bottom-left (the seat of a fire) + a second softer warm glow at
the top-right + a subtle vertical wash from cooler-top to warmer-
bottom. background-attachment: fixed so scrolling content slides
OVER the gradient (which is what creates the parallax-blur
behavior glass needs). Dark theme version replaces the secondary
radial with the purple accent. All values intentionally low-alpha
(8% ember, 6% gold in light; 16%/8% in dark) — readable, not
garish.
(B) New .page-header-glass utility for the recurring page-header-
strip pattern. tint=high (it spans full content width and needs
more visual weight than .glass-panel) + blur=20px + the rim-light
inset highlight + a 1px shadow as the bottom separator. Applied
via mechanical sweep to the 6 page header strips on:
- /my-cards
- /dashboard
- /community/collections
- components/CardsPageView (used by /cards)
- components/CollectionsPageView (used by /collections)
- components/CollectionPageView (used by /collection/[id])
After this PR, /my-cards (and every other authenticated page) will
show a distinctly translucent floating header strip with a soft top
highlight against the warm hearth gradient — i.e. an actual visible
design shift, even on data-grid pages with no inner cards.
Verified locally — npm run build, lint clean, vitest 104/104.
Co-authored-by: Cursor <cursoragent@cursor.com>
Follow-up PR to #95 (Liquid Glass foundation + primitives + Layout shell)
that closes out the remaining sub-convoy briefs in a single sweep.
Operator-instructed scope: "finish off the design changes."
After this PR, **all 8 Liquid Glass sub-convoys are MERGED to main**;
the deferred-from-#5 `fix-card3d-state` convoy is dropped (its target,
`components/Card3D.js`, turned out to be dead code).
## #2 Brief 2 — Remaining 8 modals migrated to <Modal> primitive
- `CollectionsSuccessModal.js` — wrap in <Modal hideCloseButton>; 2 Buttons.
- `CollectionsEditModal.js` — full <Modal> + <Input> + <Button> rewrite (4 fields, tag chip section, public-toggle preserved, 2 footer Buttons).
- `CollectionEditModal.js` — same pattern as above (4 fields + public-toggle + 2 Buttons).
- `CardDetailDeckModal.js` — <Modal> + native select (Select primitive not in scope) + 2 Buttons; sweep `gradient-bg-purple` → `<Button variant="primary">`.
- `UploadImageModal.js` — <Modal> + token-driven URL/file tab switcher + drag-drop using `--accent-ember` rim + 2 Buttons (one with `loading` prop).
- `CollectionSelectionModal.js` — largest of the set (header summary + SearchBar + scrollable list w/ checkbox toggles + footer); migrated to <Modal size="lg"> while preserving the per-collection card preview thumbnails.
- `OCRSettings.js` — trivial <Modal> wrap + single primary <Button>.
- `pages/decks.js` — both inline modals (Create Deck + Edit Deck) and `components/ScannerPageView.js` (Create List) migrated; ScannerPageView dropped its `useFocusTrap` named-import (Modal's internal focus trap owns the panel ref now).
- **`.github/workflows/ci.yml` `forbidden-modal-shell-without-primitive`** — grandfather list emptied to zero entries; gate is now strict.
## #3 Brief 2 — Forms migrated to <Button> / <SearchBar>
- `pages/dashboard.js` — 3 CTAs → <Button> (Create List with leadingIcon, Create Your First List, View All Lists).
- `pages/my-cards.js` — empty-state CTA → <Button variant="primary" size="lg">. View-mode toggle buttons intentionally left native (icon-only, doesn't match Button variants).
- `pages/community/collections.js` — Go to My Lists CTA → <Button>.
- `components/CollectionsPageView.js` — Discover Community + Create List header CTAs → <Button>; search input → <SearchBar>.
- Card-grid per-row icon buttons (CollectionsPageView, my-cards, CardsPageView) intentionally left native — tiny per-card actions whose styling doesn't match Button variants and would invalidate visual-diff baselines.
## #5 — scope revised + landed
`components/Card3D.js` deletion: surveyed every importer with grep — **zero consumers** in `pages/**` or `components/**`. Only references were in convoy docs. The "pre-existing state-management bug" (state setters used without useState declarations) never affected the running app because the component was never rendered. -505 LOC. The `fix-card3d-state` convoy is dropped from the roadmap as a result.
The actual card-grid component (`components/CardItem.js`) is intentionally **not** modified in this sweep — it has per-rarity glow tuning that the existing visual-diff baseline locks in, and the architect's #5 deferral note specifically called out the dedicated baseline re-seed cost. A future implementer turn can apply rim-light tokens to CardItem with its own baseline re-seed when an operator wants that polish.
## #6 Brief 1 — Landing + invite pages glass-migrated
- `pages/index.js` — top nav: `var(--glass-surface-mid)` + `--glass-blur-mid` + rim-light. 3 feature cards: `<GlassSurface tint="mid" rim="subtle" elevation="ambient">`. Featured-list cards (the public collection grid): same `<GlassSurface>` recipe with motion-token transitions. All 6 CTA buttons → <Button variant="primary"|"secondary"|"ghost"> with proper sizes. Pulse-loading placeholders tagged `.motion-essential` so reduced-motion users still see them animate (state-meaningful).
- `pages/invite/accept.js` + `pages/invite/decline.js` — both outcome panels wrapped in `<GlassSurface tint="mid" rim="subtle" elevation="pronounced">`. Loading spinner border colors corrected from `--text-accent` (which didn't exist) to `--accent-ember`. All 8 buttons → <Button>. `gradient-bg-ember` consumers retained (the canonical warm-palette utility class is fine).
## #8 Brief 2 — Legacy alias sweep + CI gate graduation
- Swept `gradient-bg-purple` → `gradient-bg-ember` across **8 files** / **13 occurrences**: `CardDetailQuantityModal`, `CardEditorView`, `CardEditorForm`, `AdminProtected`, `pages/card/[id]`, `pages/invite/{accept,decline}`, `pages/admin/card-import`. `gradient-bg-purple` was a dangling class name with no CSS definition (it was rendering no styling), so the sweep is also a bug fix — those buttons now actually get the ember gradient.
- Deleted the 5 dead CSS classes from `styles/globals.css`: `.gradient-text-blue`, `.gradient-text-purple`, `[data-theme="dark"] .glow-blue`, `[data-theme="dark"] .glow-purple`, `[data-theme="dark"] .glow-pink`. Each was zero-consumer post-sweep.
- **Graduated the `forbidden-deprecated-color-aliases` CI job from WARN to FAIL.** All 9 patterns (`gradient-text-{purple,pink,blue}`, `glow-{purple,pink,blue}`, `gradient-bg-{purple,blue,pink}`) now block the build if any consumer is reintroduced.
## Verification (local + CI gates locally exercised)
- Lint: 0 errors, 2 pre-existing warnings (`CardEditorForm.js` + `CollectionsPageView.js` carry-overs from before #95; out of scope).
- Vitest: 104/104 passing — unchanged from #95.
- Build: clean (Turbopack default; passes both light + dark theme prerender).
- `forbidden-modal-shell-without-primitive` gate: locally clear (`grep -lE 'fixed inset-0 bg-black bg-opacity-' pages components -r --include='*.js'` returns no matches).
- `forbidden-deprecated-color-aliases` gate: locally clear (all 9 patterns return no matches in `pages/` or `components/`).
## What still needs human action
- **Linux visual-diff baselines** must re-seed via the Docker workflow in `AGENTS.md` § 6. This PR's landing-page + invite-page changes will produce baseline drift on the homepage screenshot (which is currently the only baseline committed) AND additional baselines will be generated for the landing's glass-card sections once the visual spec is expanded. Recommended: run the Docker re-seed against this PR's Vercel preview, commit the result to this branch, push, verify CI green, then merge.
- Vercel auto-promotes the merge to production.
## Closes / supersedes
- Closes `.convoys/liquid-glass-modal-and-surface-primitive.md` Brief 2 (status → merged).
- Closes `.convoys/liquid-glass-form-primitives.md` Brief 2 (status → merged with explicit per-row-icon-button deferral note).
- Closes `.convoys/liquid-glass-public-and-auth.md` Brief 1 (status → merged).
- Closes `.convoys/cleanup-legacy-design-css.md` Brief 2 (status → merged + CI gate FAIL).
- Drops `.convoys/liquid-glass-card-surfaces.md` Brief 1 prerequisite (`fix-card3d-state` no longer needed; Card3D deleted).
- Drops the queued `fix-card3d-state` follow-up from the roadmap (target deleted).
- Updates `.convoys/ship-readiness.md` § "Design-system redesign portfolio" with a "Finish-portfolio sweep" subsection documenting final status of all 8 sub-convoys.
Co-authored-by: Cursor <cursoragent@cursor.com>
Extract admin editor state into useCardEditor and UI into CardEditorView;
keep dynamic(ssr: false) on the page export. Page is ~40 lines.
Co-authored-by: Cursor <cursoragent@cursor.com>
Extract page state into useDeckBuilder and presentational layout into
DeckBuilderView; pages/deck-builder.js is a thin auth-gated composer.
Co-authored-by: Cursor <cursoragent@cursor.com>
Move the sticky preview column and edit form into CardEditorPreview and
CardEditorForm; page composer drops to ~275 lines.
Co-authored-by: Cursor <cursoragent@cursor.com>
Split the main deck panel list and right-hand card browser sidebar into
DeckBuilderDeckList and DeckBuilderCardBrowser; page is ~408 lines.
Co-authored-by: Cursor <cursoragent@cursor.com>
Move the find-card search input and results grid into
CardEditorSearchPanel to start splitting the admin card editor page.
Co-authored-by: Cursor <cursoragent@cursor.com>
Move Commander basic-land checks and deck aggregate metrics into
lib/deck-builder-stats.js with unit tests; render the summary row via
DeckBuilderStatsBar to shrink the page god-component.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Extract useCollectionsPage hook and CollectionsPageView (Brief 3).
Moves list index logic into a hook and view; CollectionsThumbnail is a shared presentational component.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Default hook params for prerender safety
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Moves create-list modal UI to components/CollectionsCreateModal.js and tag input helpers to lib/collection-tag-input.js (shared with edit modal).
Co-authored-by: Cursor <cursoragent@cursor.com>
Moves card-browser state and handlers to lib/use-cards-page.js and markup to components/CardsPageView.js, leaving a thin ProtectedRoute composer in pages/cards.js.
Co-authored-by: Cursor <cursoragent@cursor.com>
Moves the anonymous /cards landing UI into components/PublicCardsView.js so the page file can focus on AuthenticatedCards.
Co-authored-by: Cursor <cursoragent@cursor.com>
Moves ~530 lines of 3D card rendering into components/Card3D.js so the cards page god-component split can continue incrementally.
Co-authored-by: Cursor <cursoragent@cursor.com>
Move scanner page markup into ScannerPageView.js. pages/scanner.js is now
a thin composer of session state, useScannerQueue, and the view.
Mark pages/scanner.js resolved in ship-readiness god-component-split table.
Co-authored-by: Cursor <cursoragent@cursor.com>
Move camera feed markup, overlays, and disambiguation dialog wiring into
CameraScannerView.js. CameraScanner is now a ~45-line hook composer.
Update ship-readiness god-component-split CameraScanner slice as resolved.
Co-authored-by: Cursor <cursoragent@cursor.com>
Move verify/disambiguation/review flow into lib/use-scanner-identification.js.
CameraScanner is now a thin composition of two hooks plus view markup.
Co-authored-by: Cursor <cursoragent@cursor.com>
Move Layer-1/Layer-2 identify flow, outcome resolution, disambiguation
refine helpers, and scan-for-review API calls into lib/scanner-card-identify.js.
Remove unused manaSymbolSettings state from CameraScanner.
Co-authored-by: Cursor <cursoragent@cursor.com>
Move OpenCV shape detection, coordinate conversion, overlap checks,
and tracked-card merge logic from CameraScanner into lib/scanner-card-detection.js
with unit tests for the pure helpers.
Co-authored-by: Cursor <cursoragent@cursor.com>
Brief 1 of god-component-split: move ScanDisambiguationDialog and
scan-capture-upload lib out of CameraScanner (~90 lines) without
behavior changes.
Co-authored-by: Cursor <cursoragent@cursor.com>
Drop alice/bob password prefill from the login page, add a regression
test, and improve bulk-toolbar and disambiguation accessible names.
Co-authored-by: Cursor <cursoragent@cursor.com>