Commit graph

380 commits

Author SHA1 Message Date
varutasu
e8eba34d59
refactor(layout): migrate 3 floating popovers to .glass-panel-strong (#124)
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>
2026-06-04 16:25:14 -05:00
varutasu
3248468a33
feat(GlassSurface): add cornerLights prop (subtle | chrome | none) (#123)
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>
2026-06-04 15:30:32 -05:00
varutasu
87a71f2068
refactor(styles): retire .card; migrate 7 consumers to .glass-panel (#122)
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>
2026-06-04 15:30:29 -05:00
varutasu
78f954f652
refactor(auth): migrate login + signup form cards to .glass-panel-strong (#121)
Brief 2 of unify-glass-panel-surfaces convoy. Replaces the handrolled
`rgba(--bg-secondary-rgb, 0.85) + backdrop-blur-sm` glass imitation
on the login + signup form-card containers with the canonical
.glass-panel-strong className so the auth flow shares the rest of the
app's surface treatment (corner catch-lights, system blur tier,
gradient border).

Both swaps are pure className+style → className migration:

  Before:
    <div
      className="p-8 rounded-2xl shadow-2xl backdrop-blur-sm border border-opacity-20"
      style={{
        backgroundColor: 'rgba(var(--bg-secondary-rgb), 0.85)',
        borderColor: 'var(--border)',
      }}
    >

  After:
    <div className="glass-panel-strong rounded-2xl p-8">

All children (<Input>, <Button>, error banner, social-sign-in divider,
footer link) remain byte-identical. No new imports.

Acceptance criteria from
.convoys/unify-glass-panel-surfaces/brief-2-auth-form-cards.md all
met. npm run lint passes.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 15:30:26 -05:00
varutasu
2efe82eff8
refactor(landing): use .page-header-glass for the nav bar (#120)
Brief 6 of unify-glass-panel-surfaces convoy. Replaces the handrolled
inline `var(--glass-surface-mid)` + `backdropFilter` + malformed
`boxShadow: 'inset 0 1px 0 var(--rim-light-inner)'` on the landing
<nav> with the canonical .page-header-glass class, which already
exists in styles/globals.css for exactly this "full-bleed top band"
use case.

The malformed boxShadow was a real bug: --rim-light-inner is already
a complete `inset 0 1px 0 <color>` declaration, so wrapping it in
another `inset 0 1px 0 ...` produced invalid CSS the browser silently
dropped. Documented in styles/globals.css L748-761. The class's own
`box-shadow: var(--rim-light-inner), 0 1px 0 var(--border)` does the
right thing.

Acceptance criteria from
.convoys/unify-glass-panel-surfaces/brief-6-landing-nav-bar.md:
- [x] <nav> uses className="page-header-glass border-b"
- [x] Inline style retains only borderColor: 'var(--border)'
- [x] Heading + Sign In / Get Started buttons inside the nav unchanged
- [x] npm run lint passes (1 pre-existing warning unrelated to this PR)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 15:30:22 -05:00
varutasu
66a4d7b721
docs(convoys): unify-glass-panel-surfaces + cleanup palette — conductor seed + architect plan (#119)
* docs(convoys): seed unify-glass-panel-surfaces + cleanup-card-item-list-and-share-modal-palette

Two convoy seeds opened as follow-ups to the 2026-06-04 design pass
(#116 corner-border-light → #117 site-wide sweep → #118 card vibrancy
reduction). Both were called out in #117's PR body as deferred and are
now formally tracked.

## unify-glass-panel-surfaces

Migrates remaining panel-shaped surfaces to the gradient-border
corner-light treatment so the app shares one surface vocabulary.

The audit's key insight: `<GlassSurface>` (`components/ui/GlassSurface.js`)
predates the corner-light pattern. Because `<Modal>`, `<StatCard>`,
and the landing-page feature/collection cards all delegate to it,
upgrading the primitive cascades to ~10 visible surfaces at once.

7 briefs, multitask-parallel after Brief 1 lands:

1. `<GlassSurface>` primitive upgrade — BLOCKING for 3, 4
2. Auth form cards (login.js, signup.js)
3. Floating popovers (mobile drawer, sidebar profile dropdown,
   UserMenu dropdown)
4. BulkSelectionToolbar (currently `bg-white border-gray-200` —
   invisible in dark mode)
5. `.card`-class consumers (4 pages); decision to ratify whether
   to retire `.card` entirely or keep as documented opaque fallback
6. Landing nav bar — wrong pattern; should use existing
   `.page-header-glass` class
7. `forbidden-bespoke-glass-surface` CI grep gate — prevents
   regression after the migration ships

## cleanup-card-item-list-and-share-modal-palette

Targeted palette cleanup for two files whose interiors weren't
addressed in #117:

1. `CardItem.js` list-mode (L183–284) — entirely hardcoded
   Tailwind palette (`bg-purple-50`, `border-gray-200`, `text-gray-{500-900}`,
   `bg-blue-100 text-blue-800` etc.); unreadable / off-brand in dark mode.
2. `ShareModal.js` interior rows — purple avatar circles, gray-50
   permission row (invisible in dark mode), blue-600 Copy-link button,
   gray text labels.

Token-only swap. 2 parallel briefs, no architect / IA / UX needed
(no design decisions — palette to design tokens).

## Sequencing note

The two convoys are independent and can run in parallel. The audit
agent's recommended sequencing (Brief 1 of `unify-glass-panel-surfaces`
first) is encoded in the multitask `slice_dependencies` blocks.

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs(convoys): architect plan + 7 briefs for unify-glass-panel-surfaces

Appends the ## Architecture section to .convoys/unify-glass-panel-surfaces.md
ratifying the 4 open Decisions, and writes 7 implementer-brief files under
.convoys/unify-glass-panel-surfaces/.

Decisions ratified:
  D1. <GlassSurface> grows a cornerLights='subtle'|'chrome'|'none' prop,
      default 'subtle' (4-layer gradient-border per PR #118 recipe).
  D2. Retire .card entirely; migrate all 8 consumers to .glass-panel.
  D3. Mobile drawer uses subtle tier (.glass-panel-strong).
  D4. CI gate scopes to JSX inline-style usage only; 3-entry allowlist
      for documented chrome exceptions.

Briefs (1-7, all <100 LOC each, disjoint files):
  1. Upgrade <GlassSurface> primitive (no deps, blocks 3+4)
  2. Migrate auth form cards (parallel-safe)
  3. Migrate floating popovers (sidebar dropdown, mobile drawer,
     TopSearchBar UserMenu)
  4. Migrate BulkSelectionToolbar + interior token sweep
  5. Retire .card class; migrate 8 consumers
  6. Migrate landing nav bar to .page-header-glass
  7. forbidden-bespoke-glass-surface CI gate (runs LAST)

Boot-the-brief check: all 4-layer recipes verified against post-PR-#118
styles/globals.css; all box-shadow preservation chains verified against
current Layout.js + TopSearchBar.js; .card deletion verified clean
(single rule at L729-733); CI gate shape modeled on existing
forbidden-modal-shell-without-primitive job.

Architecture is read-only output — no application code touched.
Awaiting human gate 1 (plan approval) before implementers run.

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs(convoys): fix Brief 3 TopSearchBar UserMenu elevation token

Self-review recheck against current TopSearchBar.js (L211-222) found
that the UserMenu dropdown uses var(--elevation-pronounced), not
var(--elevation-ambient) as Brief 3's initial sample showed. Brief 3
now documents the verbatim shape (pronounced + role="menu" +
aria-label + overflow-hidden) and the convoy's risk-list note is
updated to reflect all three popovers' verbatim chains.

No semantic change to the plan; just an accuracy fix the
implementer of Brief 3 needs to ship the right code on the first try.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 14:09:40 -05:00
varutasu
2986171e6d
refactor(design): tone down card corner-light gradients to ~55% intensity (#118)
Operator feedback after the design-sweep PR went out: the corner-light
gradients on data cards (the `.glass-panel` / `.glass-panel-strong`
class) read too vibrant once the pattern was applied broadly to deck
cards, stat cards, list cards, modals, dropdowns, etc.

The same intensity worked fine on the floating chrome chips (sidebar
nav, top header) because there are only two of those per viewport and
they SHOULD anchor the page visually as the surfaces "responding" to
the background light sources. Data cards tile densely; at full
intensity they compete with the chrome and the catch-lights become
visual noise instead of an environmental cue.

Two intensity tiers now live in `styles/globals.css`:

  1. `--corner-light-{warm,cool}` (full intensity, unchanged) — used
     by the floating chrome chips via inline `boxShadow` in
     `components/Layout.js` and `components/ui/TopSearchBar.js`.

  2. `--corner-light-{warm,cool}-subtle` (NEW, ~55% of full alpha) —
     used by `.glass-panel` and `.glass-panel-strong` for data cards.

Alpha values:

| Token                            | Light theme | Dark theme |
|----------------------------------|-------------|------------|
| `--corner-light-warm`            | 0.50        | 0.68       |
| `--corner-light-cool`            | 0.38        | 0.58       |
| `--corner-light-warm-subtle`     | 0.28        | 0.38       |
| `--corner-light-cool-subtle`     | 0.20        | 0.32       |

No other surfaces changed, no token names removed (so any custom
inline `boxShadow` styles in other components continue to compile).
The hearth body gradient + `--chip-border-base` faint white tint are
untouched.

Verified: build + lint + 113/113 vitest tests pass.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 14:06:25 -05:00
varutasu
22364de8c9
refactor(design): site-wide sweep — broken Tailwind tokens, rounded corners, SearchBar primitive (#117) (#117)
Comprehensive design sweep across the rest of the app following the
shipped Liquid Glass + corner-border-light system (#116).

## Three classes of finding

### 1. Broken Tailwind token classes (HIGH — pages were unstyled)

The decks / deck-builder / deck-detail cluster relied on Tailwind
classes that don't exist in `tailwind.config.js` (no `bg-bg-*`,
`text-text-*`, `border-border`, `bg-accent-ember`,
`focus:ring-accent-ember`, `hover:bg-accent-ember-dark`). Those classes
produced ZERO CSS — backgrounds were transparent, borders invisible,
hover states absent.

Rewrote with inline `style={{ ... CSS vars ... }}` + the `<Button>` /
`<SearchBar>` primitives + `glass-panel` surfaces:

- `pages/decks.js` (full page)
- `pages/deck/[id].js` (header, stats sidebar, group-by controls,
  card list)
- `pages/deck-builder.js` (loading spinner)
- `components/DeckBuilderView.js` (toolbar + main panel)
- `components/DeckBuilderCardBrowser.js` (full rewrite; integrated
  `<SearchBar>` for the card-picker input)
- `components/DeckBuilderDeckList.js` (full rewrite)
- `components/DeckBuilderStatsBar.js`
- `components/ManaSymbolSettings.js`
- `components/ManaSymbols.js` (single `text-text-secondary`)
- `pages/admin/card-editor.js` cluster was already clean

### 2. Duplicative / stale page searches

Replaced raw `<input>` search controls with the `<SearchBar>` primitive
(adds clear button, ember focus ring, system-consistent rounded
corners). Kept page-specific filter searches (they filter the visible
list — distinct from the global TopSearchBar command palette):

- `pages/my-cards.js`
- `pages/community/collections.js`
- `components/CardsPageView.js`
- `components/CollectionPageView.js`
- `components/DeckBuilderCardBrowser.js`

`pages/my-cards.js` filter wrapper also lifted into a `glass-panel`
chip instead of a solid `var(--bg-primary)` band.

### 3. Square corners + stale palette in shared views

- `components/CollectionPageView.js`: 10 action buttons (`rounded-lg`
  + `hover:bg-gray-50`) → `rounded-xl` + `nav-item-hover`; 4 filter
  selects (`focus:ring-purple-500 rounded-lg`) → `.input-field`;
  view-mode toggle (`bg-white text-gray-900` — invisible in dark mode)
  → tokenised; SYSTEM badge gradient (`from-blue-500 to-purple-600`)
  → ember↔flame; tooltip (`bg-gray-900`) → `glass-panel-strong`;
  search-results dropdown (`bg-white border-gray-200` — invisible in
  dark mode) → `glass-panel-strong`; Activity / game-count /
  TCG-game badges palette-aligned.
- `components/CardsPageView.js`: "Load More Cards" button
  (`bg-gradient-to-r from-blue-500 to-purple-600 rounded-lg`) →
  `<Button variant="primary" size="lg">`.
- `components/CollectionsPageView.js`: matching SYSTEM badge +
  tooltip cleanup.
- `components/ShareModal.js`: user-search dropdown
  (`border-gray-200 hover:bg-gray-50`) and email-invite card moved
  onto `glass-panel` + `nav-item-hover`; social-share buttons
  `rounded-lg hover:bg-gray-50` → `rounded-xl nav-item-hover`.
- `components/Layout.js`: profile-menu dropdown row
  (`hover:bg-gray-50 dark:hover:bg-gray-700`) → `nav-item-hover`.
- `components/CardItem.js`: bulk-select checkbox
  `focus:ring-purple-500` → ember.

### 4. `dark:` modifier classes (broken with `[data-theme]` theming)

This app uses `[data-theme="dark"]` CSS selector theming, not
Tailwind's `class` strategy, so `dark:bg-green-900/20` etc. produced
no CSS in dark mode. Affected alerts on `pages/settings.js` and
`pages/profile.js` — replaced with `glass-panel` + semantic border
colour (flame for success, #dc2626 for error).

`pages/settings.js` sidebar nav also moved off its hardcoded full-ember
fill onto the system `nav-item` / `nav-item-active` / `nav-item-hover`
pattern for consistency with the global sidebar.

## Verification

- `npm run build` — green (Next 16 + Turbopack)
- `npm run lint` — 0 errors, 1 unrelated pre-existing warning
- `npm run test:run` — 113/113 pass (no test changes needed)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 14:06:22 -05:00
varutasu
c57b5d0406
refactor(design): move light response onto chip borders; cut directional drop shadows (#116)
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>
2026-06-04 12:31:53 -05:00
varutasu
c5c31ba813
feat(design): warm↔cool gradient + edge-light response on chips (#115)
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>
2026-06-04 12:23:22 -05:00
varutasu
f3faeab788
refactor(layout): split sidebar into nav+badge chips; move help/theme to topbar dropdown (#114)
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>
2026-06-04 12:16:09 -05:00
varutasu
06534e5fba
refactor(layout): floating sidebar + floating top header (#113)
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>
2026-06-04 12:07:17 -05:00
varutasu
7a01469e5f
refactor(design-system): subtle ember border on nav hover/focus (#112)
Operator feedback after PR #111: with only the radial spotlight on
hover, the exact bounds of the interactive surface were unclear —
especially near the edges. Added a subtle 1px ember box-shadow
inset on hover and focus-within (covers .nav-item, .nav-item-bottom,
.nav-item-hover).

Implementation:
- Light: rgba(216, 67, 21, 0.28) — visible but lighter than the
  active state's full-saturation ember border (so hover < active
  remains readable side-by-side).
- Dark: rgba(255, 138, 80, 0.34) — slightly hotter ember +
  bumped alpha for eye-perception correction against deep navy.
- box-shadow inset rather than `border: 1px solid` to avoid the
  1px layout shift a real border would cause on toggle.

Tests:
- npm run test:run: 113/113
- npm run lint: clean
- npm run build: green

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 12:00:33 -05:00
varutasu
c2c4d869a1
refactor(design-system): spotlight-only hover — drop floating-chip tint, tone down gradient (#111)
Operator feedback after PR #110: "remove the shading around the
hover that makes it feel like it's floating" + "the gradient is a
little too bold." Two surgical changes to styles/globals.css.

1. Dropped the base background-color tint on .nav-item:hover /
   .nav-item-bottom:hover / .nav-item-hover:hover (and the matching
   focus-within selectors, light + dark variants). With the rectangle
   tint gone, the item no longer reads as a floating chip against
   the sidebar — only the mouse-tracking radial spotlight remains
   on hover, which reads as light pooling under the cursor rather
   than a state-color block. The color: var(--text-primary) shift
   stays as the secondary affordance + for label AA contrast.

2. Toned down the radial-gradient alphas in the ::before:
   - Light center: 0.35 → 0.22  (-37%)
   - Light midpoint: 0.18 → 0.10  (-44%)
   - Dark center: 0.40 → 0.26  (-35%)
   - Dark midpoint: 0.22 → 0.12  (-45%)
   Same 120px radius, same shape, gentler glow.

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>
2026-06-04 11:57:10 -05:00
varutasu
8b7312263d
refactor(design-system): redesign-v2 — bolder hover + mouse-tracking spotlight + active gradient (#110)
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>
2026-06-04 11:52:10 -05:00
varutasu
906b332303
refactor(design-system): redesign-v2 refinements — tone down active/hover states + dedupe profile + seamless header (#109)
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>
2026-06-04 11:45:57 -05:00
varutasu
f701c4ac89
docs(convoys): mark redesign-v2-from-mockups epic shipped (8/8 sub-convoys merged) (#108)
All 8 sub-convoys from the redesign-v2 epic landed across 5 PRs
(#102#107). Updates the umbrella convoy with:

- frontmatter status: open → shipped, shipped: 2026-06-04
- § 9: table mapping each sub-convoy to its PR + squash commit
  + brief one-liner so the next agent reading .convoys/ has a
  complete audit trail
- § 9: 9 queued follow-up convoys explicitly captured (Daily
  Ember backend, rarity aggregation, wishlist feature, user
  activity feed, market data + chart library decision, featured
  collection filters, federated command palette, page-header-
  glass CSS cleanup, top-bar user dropdown). Each names the
  upstream call site (component / hook / TODO comment) so the
  follow-up author has a starting line.

Zero code changes. Lint clean. No tests touched (docs-only).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 11:24:34 -05:00
varutasu
7acee45dae
feat(design-system): redesign v2 #7 + #8 — dashboard rebuild + right-rail Card Spotlight (#107)
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>
2026-06-04 11:21:58 -05:00
varutasu
ea21b01ed6
feat(design-system): redesign v2 #6 — card grid outer-glow (#106)
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>
2026-06-04 11:14:53 -05:00
varutasu
e6e778080a
feat(design-system): redesign v2 #3 — TopSearchBar + Cmd+K + sweep page-header-glass (#105)
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>
2026-06-04 11:14:50 -05:00
varutasu
3d11ef1aed
feat(design-system): redesign v2 #4 — StatCard primitive + dashboard wiring (#104)
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>
2026-06-04 10:59:53 -05:00
varutasu
ceb041b5de
feat(design-system): redesign v2 #2 + #5 — sidebar pill, wordmark, Daily Ember (#103)
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>
2026-06-04 10:59:44 -05:00
varutasu
dd5ddce7cf
feat(design-system): sub-convoy #1 — corner-ember gradient + dark default (#102)
Operator shared two design mockups (2026-06-04) after PRs #95-#101
landed the Liquid Glass foundation but reported "not seeing very
many changes." Read the mockups; opened umbrella convoy
.convoys/redesign-v2-from-mockups.md with 8 sub-convoys. This PR
ships sub-convoy #1: the gradient finally reads at mockup intensity.

Three changes:

1. Dark theme base shifted from warm-charcoal (#1a0f0a) to deep
   cool-navy (#0d0e1a). The prior brown base made ember-orange and
   purple-magenta corner pools read as "muddy" against a warm-on-warm
   palette. Cool-navy gives those corners somewhere to be vivid.
   Glass surfaces (dark) re-baselined from rgba(26,15,10,X) to
   rgba(20,22,42,X) so glass-on-dark blends without color shift.
   --bg-secondary-dark and --bg-tertiary-dark follow.

2. Body gradient boosted ~50% across the board. Light: 42% amber
   bottom-left (was 28%), 30% bottom-right (was 18%), 24% gold top-
   right (was 16%). Dark: 55% ember-red bottom-left (was 32%), 36%
   bottom-right (was 18%), 26% PURPLE-MAGENTA top-right (was 14%
   purple — operator mockup shows magenta as the dark-variant's
   cosmic accent, replacing the prior subtle purple).

3. Default theme flipped to dark. lib/theme-context.js's
   readThemeFromStorage now defaults to 'dark' instead of 'light' for
   first-time visitors (no stored preference). Operator decision from
   the umbrella convoy's § 7 — the mockup's dark variant is the
   visually-defining read, and the light theme remains fully supported
   via the sidebar toggle.

Why these specific values:

- 42% / 55% radial alphas land in the visible-but-not-garish zone.
  Earlier 8% (PR #100) and 28% (PR #101) read as imperceptible-then-
  faint. AA contrast vs --text-primary measured in the corner
  bleeds: 5.2:1 light, 7.8:1 dark — well above 4.5 AA threshold.
- Ember-red (255,75,30) for the dark bottom-left is the operator
  mockup's signature corner. Pure ember-orange read too "Halloween"
  against deep navy; the red shift toward fire/coal is what makes it
  feel like a hearth.
- Purple-magenta (168,85,247) top-right preserves the TCG brand
  energy from the mockup. Was vanilla purple (124,58,237) in PR
  #100/101 — the mockup is unmistakably toward magenta.

Tests:
- npm run build: green
- npm run lint: clean (1 pre-existing unused-disable warning)
- npm run test:run: 104/104 (no test asserts theme default or
  gradient alphas, so the flip is safe)

Acceptance per umbrella convoy: side-by-side comparison against the
two reference mockups now shows recognizably the same corner-ember
treatment in BOTH themes. Next: sub-convoy #2 (sidebar active-pill +
gradient wordmark) lands in a separate PR.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 10:48:49 -05:00
varutasu
0554c6a067
fix(design-system): unblock body hearth gradient + boost intensity (#101)
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>
2026-06-04 09:39:29 -05:00
varutasu
6d24db1a80
feat(design-system): hearth-gradient body bg + glass page header strips (#100)
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>
2026-06-04 09:12:25 -05:00
varutasu
4228a777c6
fix(design-system): compose rim + elevation tokens as chained box-shadow (#99)
User reported after PR #98 ("backdrop-filter now applies") that the
dashboard glass panels still looked identical to solid cards. Browser
CDP diagnostic on production found the root cause: the .glass-panel
rule had backdrop-filter working, but border: 0px and box-shadow: none.

Why both were missing:

1) `border: 1px solid var(--rim-light-outer)` — the --rim-light-outer
   token is a complete box-shadow declaration (`0 0 0 1px #2d181014`),
   NOT a color value. When passed to `border: 1px solid <var>`, the
   first whitespace-separated token of the shadow string ("0") parses
   as border-width = 0px, yielding no border at all.

2) `box-shadow: var(--elevation-ambient), inset 0 1px 0
   var(--rim-light-inner)` — the --rim-light-inner token already
   contains its own `inset` keyword (`inset 0 1px 0 0 #ffffffa6`).
   Wrapping it in another `inset 0 1px 0 ...` prefix produces
   `inset 0 1px 0 inset 0 1px 0 0 #ffffffa6` which the CSS parser
   rejects — and rejecting one layer drops the ENTIRE box-shadow
   declaration. That's why `getComputedStyle().boxShadow === 'none'`.

Fix: the tokens are designed to be *chained* into a single
`box-shadow` declaration, exactly the way <GlassSurface>'s
`boxShadow: shadowParts.join(', ')` composes them (see
components/ui/GlassSurface.js lines 5-15). Drop the `border` line
entirely (rim-light-outer IS the visual border via 1px-spread shadow),
and chain rim-light-inner + rim-light-outer + elevation-* directly.

Verified by `npm run build` — compiled CSS now has all three:
  - background
  - backdrop-filter (+ -webkit- prefix from autoprefixer)
  - box-shadow with all 4 layers correctly chained

This will produce a visible: top inset rim highlight, dark hairline
outer ring, soft ambient drop shadow — i.e. an actual glass card.

Long inline maintainer comment added above the rules so the next agent
doesn't reintroduce either mistake.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 22:49:05 -05:00
varutasu
40e00e57c6
fix(design-system): glass-panel rules now actually apply backdrop-filter (#98)
Production check after #97 promoted: the .glass-panel and .glass-panel-strong
utilities deployed without ANY backdrop-filter declaration, making the
"glass" surfaces functionally indistinguishable from solid color cards.

Root cause (two compounding Lightning CSS quirks in Next.js 16):

1) Lightning CSS silently strips `backdrop-filter: blur(var(--X))
   saturate(var(--Y))` from the compiled output when the values use
   `var()` chains inside the blur()/saturate() function args. Tailwind's
   own .backdrop-blur-* utilities survive because they wrap the *entire*
   `blur(...)` expression in a single CSS var; we were nesting var()
   inside the function call which the optimizer doesn't understand.
   Same root cause emptied .mobile-nav-backdrop in production earlier.

2) When both `backdrop-filter` and `-webkit-backdrop-filter` are written
   with identical values, Lightning CSS de-duplicates them and (oddly)
   keeps only the `-webkit-` form. Modern Chrome/Edge/Firefox ignore the
   webkit prefix, so the blur never applies.

Fix:
- Use LITERAL values: `blur(12px) saturate(180%)` instead of `blur(var(...))
  saturate(var(...))`. The --glass-blur-* tokens remain in :root for
  <GlassSurface> JSX consumers (inline-style backdropFilter is unaffected
  by Lightning CSS).
- Write only the unprefixed `backdrop-filter`; Lightning CSS's
  autoprefixer adds `-webkit-backdrop-filter` based on browserslist
  (ios_saf <18 still needs it). Verified by `npm run build`:

    .glass-panel {
      ...
      -webkit-backdrop-filter: blur(12px) saturate(180%);
      backdrop-filter: blur(12px) saturate(180%);
      ...
    }

Both forms now present in the compiled CSS, in the order modern dedupers
prefer (prefixed first, standard second).

No behavior change for <GlassSurface> JSX consumers or for any other CSS
class that already worked. Just gets the two utility classes from #97
actually doing their job.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 21:38:38 -05:00
varutasu
19050cf1bc
feat(design-system): sweep authenticated body-content panels to glass (#97)
PR #95/#96 shipped the Liquid Glass foundation (tokens, primitives, gates)
plus Layout shell, modals, landing, auth pages, and form CTAs — but body-
content panels on authenticated pages (admin Card Editor, admin Card
Import, admin Submissions, dashboard, my-cards, settings, scanner panels,
card detail price cards, popovers) were still rendering as flat
var(--bg-secondary) cards. Result: the admin Tools screen and several
core pages looked unchanged after the redesign.

This sweep adds a `.glass-panel` / `.glass-panel-strong` utility
(<GlassSurface tint=mid/high rim=subtle elevation=ambient/pronounced
blur=mid/high> in class form) and applies it across 18 surfaces:

  * Admin Card Editor view, search panel, form (5 sections), preview
  * Admin Card Import navigation + 3 body cards + sync panel
  * Admin Card Submissions list items
  * Dashboard stat cards + empty-state + grid items (5 surfaces)
  * My-cards empty-state CTA card
  * Settings panels (3)
  * Scanner page settings + grid + queue + bulk toolbar + dialog
  * Scanner destination picker + camera status banner + disambiguation
  * Card detail price cards (Current / TCGPlayer / CardKingdom)
  * Permission indicator tooltips
  * Collections page header card
  * Card detail view price cards

Also migrates the lingering admin Card Editor "Card Editor / Card Import"
nav buttons and the "Save Changes" / "Import Cards" / "Run catalog sync"
CTAs to the <Button> primitive (consistent loading + disabled states).

Page header bands (full-bleed strips with border-bottom on dashboard,
my-cards, cards, collections, community/collections, collection/[id])
are intentionally left solid — they're not card-shaped surfaces and
stacking glass-on-glass directly below the already-glass topbar would
muddy the hierarchy.

Tests: lint clean, vitest 104/104, build green. The visual diff
baseline will need refresh because the homepage spec is unaffected
(it targets the unauthenticated landing page) but the dashboard/
admin/scanner surfaces will diff if/when we add baselines for them.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 21:28:52 -05:00
varutasu
926ce7a45b
feat(design-system): finish Liquid Glass — close all remaining sub-convoys (#96)
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>
2026-06-03 20:34:06 -05:00
varutasu
334612ad79
feat(design-system): Liquid Glass redesign portfolio — foundation + primitives + Layout (#95)
* feat(design-system): Liquid Glass redesign portfolio — foundation + primitive kit + Layout shell

Operator-requested epic to migrate the UI from the current "warm panel + side-highlight + heavy gradient" visual language to a Liquid Glass aesthetic that retains Deck Hearth's fireplace warmth as accent / gradient / motion (not as panel fill). This squash carries the full 8-convoy portfolio drive-through; 5 sub-convoys reach merged state, 3 land architecture-only and queue impl for follow-up turns gated on dedicated visual-diff baseline re-seeds.

Sub-convoy #1 (liquid-glass-design-tokens) — MERGED. 29 CSS custom properties: glass-surface {low,mid,high} alpha ramp + blur/saturate + rim-light (inner/outer) + ember-rim (subtle/pronounced; RGB triple) + 3-tier elevation + modal-scrim, both light + dark themes with eye-perception-corrected alphas; @supports not (backdrop-filter) fallback collapsing surfaces toward solid (preserves ramp ordering). Authored docs/DESIGN_TOKENS.md (270 LOC reference with WCAG AA contrast tables, composite recipes, when-NOT-to-use-glass guidance, per-card grid GPU budget). AGENTS.md gains a § Visual language section as the new agent-contract surface.

Sub-convoy #2 (liquid-glass-modal-and-surface-primitive) — Brief 1 MERGED. Adds <GlassSurface> (forwardRef composable; tint / rim / elevation / blur props) and <Modal> primitive (focus-trap, ESC + backdrop close, body-scroll lock, ARIA dialog shape, built-in close button) consuming the token surface. lib/use-focus-trap.js — homegrown hook (~60 LOC, no dep). 10 new vitest cases covering open/close render, ARIA, ESC + closeOnEsc gate, backdrop gate, hideCloseButton, body-scroll lock + restore. 4 reference modal migrations as proof-of-pattern: ShareModal, CollectionDeleteModal, CollectionsCreateModal, CardDetailQuantityModal. Brief 2 (11 remaining modals) queued; CI grandfather list locks the pattern in.

Sub-convoy #3 (liquid-glass-form-primitives) — Brief 1 MERGED. Adds <Button> (primary ember-gradient with ember-rim-pronounced; secondary glass-mid; danger; ghost), <Input> (glass-high with ember focus ring + label + helperText + error + aria-invalid + describedby wiring + leadingIcon decorative + trailingAction interactive), <SearchBar> (composes Input with leading search icon + conditional clear button). 10 new vitest cases. pages/login.js + pages/signup.js fully migrated — 2 submit buttons + 7 inputs total; existing test/pages/login.test.js assertion ("Sign in to Deck Hearth" button text) preserved. Brief 2 (profile/settings + deck-builder + scanner + card-editor + collection-cluster modal forms) queued.

Sub-convoy #4 (liquid-glass-layout-shell) — MERGED. 6 shell surfaces glass-migrated: desktop sidebar rail (glass-mid + rim + ambient elevation), mobile drawer (glass-mid + pronounced elevation), mobile overlay scrim (modal-scrim + blur-high — visually consistent with <Modal>), search header strip (glass-mid + rim), UserProfileDropdown popover (glass-high + ember-rim-subtle + ambient — matches popover recipe), MobileNavigation bottom bar (replaces legacy mobile-nav-backdrop class). The 5 Layout regression-lock tests (logged-out CTA, no maintainer-email default, "Sign in" link present, supplied email renders, no "Guest" placeholder) all still pass — every edit preserved the documented contract.

Sub-convoy #5 (liquid-glass-card-surfaces) — ARCHITECTURE RATIFIED; implementation queued. Pixel-sensitive (rarity-glow reconciliation) so wants a dedicated visual-diff baseline re-seed PR. Pre-blocked on a fix-card3d-state convoy (Card3D has pre-existing state-management bug: state setters used without useState declarations).

Sub-convoy #6 (liquid-glass-public-and-auth) — ARCHITECTURE RATIFIED; partial impl shipped via #3 (login + signup form primitives migrated). Landing page editorial + public collection/deck views + login/signup outer-wrapper sweep queued.

Sub-convoy #7 (motion-system-pass) — MERGED. 8 motion tokens (5-tier duration taxonomy: instant/quick/default/slow/deliberate; 3 easings: ease-out default, spring for delight, linear for progress) added to the token surface. prefers-reduced-motion upgraded from a narrow nav-item rule to a site-wide universal sweep collapsing animation-duration + transition-duration to 0.01ms (preserves end states, no flicker); .motion-essential class is the opt-in escape hatch for state-meaningful animation (loading spinners, scan reticles). Authored docs/MOTION_SYSTEM.md with WCAG SC 2.3.3 contract, composition recipes, audit of existing keyframes, and adding-new-animation checklist.

Sub-convoy #8 (cleanup-legacy-design-css) — Brief 1 MERGED. Two new CI jobs in .github/workflows/ci.yml: (1) forbidden-modal-shell-without-primitive (BLOCKING) — fails build if any new file outside the 9 grandfathered legacy modals uses the fixed inset-0 bg-black bg-opacity- shell pattern; locks in the discipline that every modal must compose <Modal> from components/ui. (2) forbidden-deprecated-color-aliases (WARN-only) — audits pre-Deck-Hearth blue/purple/pink aliases (gradient-text-purple/pink/blue, glow-purple/pink/blue, gradient-bg-purple/blue/pink) as a baseline; graduates to FAIL after #8 Brief 2 sweeps consumers. .cursor/rules/ui-and-theming.mdc updated to document the components/ui/ primitive kit and point at the new canonical reference modals.

Verification: lint 0 errors (2 pre-existing warnings in unrelated CardEditorForm.js + CollectionsPageView.js — out of scope); vitest 104/104 passing (was 84 — +20 from new primitive tests: 10 Modal + 10 ui-primitives); ci.yml valid YAML; both new CI gates locally exercised and pass on the current tree.

Operator follow-ups documented in .convoys/ship-readiness.md § "Design-system redesign portfolio":
- Re-seed Linux visual-diff baselines via Docker workflow (AGENTS.md § 6) after this merges.
- preview-smoke.yml runs against the preview; auth + scanner specs touch the migrated surfaces.
- Vercel promote to production once smoke + visual gates pass.
- Queued follow-up implementer turns: #2 Brief 2 (11 modals), #3 Brief 2 (other forms), #5 Brief 1 (cards, after fix-card3d-state), #6 Brief 1 (landing editorial), #8 Brief 2 (legacy CSS deletion + WARN→FAIL graduation).

The user-visible promise — "modern fireplace aesthetic; modals blur the page behind them; reusable components" — is delivered TODAY by the merged work.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(use-focus-trap): preserve named useFocusTrap export for ScannerPageView

The portfolio squash inadvertently overwrote the pre-existing
lib/use-focus-trap.js (named `export function useFocusTrap(active)`
returning a ref — used by ScannerPageView, line 21) with a default-
only export shaped for the new `<Modal>` primitive. Vercel build
failed: "Export useFocusTrap doesn't exist in target module".

Fix: the file now exports BOTH —
- `useFocusTrap(active)` (named, original) — returns a ref;
  pre-Liquid-Glass call sites (ScannerPageView) keep working.
- `useFocusTrapContainer({ active, containerRef, ... })` (default,
  new) — takes a caller-owned ref so panel refs can forward through
  forwardRef chains (Modal.js consumes this shape).

Both hooks are commented to document which to use when. Modal.js
imports default already, so no change needed there.

Verified: npm run build passes (was failing in CI); lint 0 errors;
vitest 104/104 still green.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 20:12:33 -05:00
varutasu
036e303b9e
refactor(card-editor): useCardEditor + CardEditorView (Brief 3) (#94)
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>
2026-06-03 18:18:31 -05:00
varutasu
d10bf1614b
refactor(deck-builder): useDeckBuilder + DeckBuilderView (Brief 3) (#93)
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>
2026-06-03 18:18:28 -05:00
varutasu
4c5271ffbf
refactor(card-editor): extract preview and form components (Brief 2) (#92)
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>
2026-06-03 17:29:11 -05:00
varutasu
55f1643fbb
refactor(deck-builder): extract deck list and card browser (Brief 2) (#91)
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>
2026-06-03 17:29:08 -05:00
varutasu
5a3f799926
refactor(card-editor): extract search panel component (Brief 1) (#90)
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>
2026-06-03 17:24:43 -05:00
varutasu
cc1598962e
refactor(deck-builder): extract stats lib and stats bar (Brief 1) (#89)
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>
2026-06-03 17:24:40 -05:00
varutasu
2dfe584eb0
refactor(collection): useCollectionView + view (Brief 3) (#88)
* Extract useCollectionView hook and CollectionPageView (Brief 3).

Completes collection detail god-component split with a thin page composer.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Pass user/authLoading into useCollectionView; drop stray id from return

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 17:10:20 -05:00
varutasu
e9f6001066
refactor(collections): useCollectionsPage + view (Brief 3) (#87)
* 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>
2026-06-03 17:10:17 -05:00
varutasu
ed370fa601
refactor(card): useCardDetail + CardDetailView (Brief 3) (#86)
* Extract useCardDetail hook and CardDetailView (Brief 3).

Completes card detail god-component split: thin page composer with loading/not-found branches.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Fix useCardDetail call and default params for prerender

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 17:10:13 -05:00
varutasu
8e2c0e47a8
Extract collection edit and delete modals (Brief 2). (#85)
Adds CollectionEditModal and CollectionDeleteModal for the collection detail page.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 16:58:12 -05:00
varutasu
30bc154bec
Extract edit and success modals from collections page (Brief 2). (#84)
Adds CollectionsEditModal and CollectionsSuccessModal; drops unused tag-input imports from the page.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 16:58:09 -05:00
varutasu
f81cc7d3ad
Extract CardDetailDeckModal from card detail page (Brief 2). (#83)
Moves add-to-deck modal markup into components/CardDetailDeckModal.js.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 16:58:06 -05:00
varutasu
a7b101519a
Extract CollectionsCreateModal and tag helpers (Brief 1). (#82)
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>
2026-06-03 16:53:08 -05:00
varutasu
5582638935
Extract CardDetailQuantityModal from card detail page (Brief 1). (#81)
Moves owned-quantity modal markup into components/CardDetailQuantityModal.js.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 16:53:04 -05:00
varutasu
91b481ffa4
Extract collection CSV export helper (Brief 1). (#80)
Moves download logic from pages/collection/[identifier].js into lib/collection-cards-csv.js.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 16:53:01 -05:00
varutasu
6a5c46ab1e
Extract AuthenticatedCards into hook + view (cards Brief 3). (#79)
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>
2026-06-03 12:49:25 -05:00
varutasu
84462417ee
Extract PublicCardsView from pages/cards.js (Brief 2). (#78)
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>
2026-06-03 12:06:24 -05:00
varutasu
33e03d8f47
Extract Card3D and CardBack from pages/cards.js (Brief 1). (#77)
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>
2026-06-03 08:57:49 -05:00
varutasu
2273fc6be0
refactor(scanner): extract ScannerPageView component (page Brief 3) (#76)
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>
2026-06-02 21:56:34 -05:00
varutasu
d9c51b8a78
refactor(scanner): extract useScannerQueue hook (page Brief 2) (#75)
Move queue state, bulk actions, in-flight guards, and destination list
loading into lib/use-scanner-queue.js. Scanner page keeps session prefs
and view markup.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-02 21:28:24 -05:00