# Young Bull · Design v4 → Production Handoff

> **Read me first, Claude Code.** This file is everything you need to ship the v4 design into the live Next.js app at `youngbullinvests.com`. Treat the HTML/CSS/JS in this project as a reference implementation, not a copy-paste target. Lift the design intent. Use the existing Next.js conventions.

---

## What you're inheriting

**This project (`bull design`)** is a static HTML reference build. It contains:

```
index.html              # v4 landing page — explains every deliverable
command.html            # /command rebuild (the centerpiece)
compare.html            # /compare — live overlap math + Venn + Shadow Forecast
brief.html              # /brief — collaborative morning brief, take-a-side voting
style.html              # Design library: 10 layers · 14 agents · 12 clusters · motion
heros/                  # 5 anchor hero cards (3 NotebookLM PNGs + 2 parametric HTML)
HANDOFF.md              # this file
assets/                 # CSS, JS, reference PNGs (NotebookLM Studio output)
pages/                  # v3 surfaces from the prior intensive (KEEP, don't delete)
v3-canvas.html          # archived prior design-canvas (reference only)
```

**The live repo (`young-bull-site`)** has:
- Vercel-deployed Next.js frontend
- 50+ API endpoints under `/api/`
- Supabase schema with `agent_personalities`, `agent_voices`, `ticker_metadata`, `ticker_categories`, `prices`, `portfolio_positions`, etc.
- Railway agents that write to Supabase (we never write from the browser)
- A v3 token system in `styles/yb.css`

Your job: port v4 design surfaces into the React/Next app, keep v3 surfaces alive where they still work, replace v3 versions of `/command`, `/compare`, `/brief` with v4 versions.

---

## The first 10 actions (in order)

If you do nothing else, do these:

```
1. Read this file in full. Yes, the whole thing.
2. Open command.html in a browser. Click a desk. Click "Dispatch a Desk".
   Toggle "My watchlist only". Try ⌘K. Feel the room.
3. Open compare.html. Paste tickers. Watch the Venn, layer bars, Shadow Forecast update.
4. Open brief.html. Vote on a side. Submit a take. Feel the editorial pacing.
5. Open style.html. Click a cluster card. Replay a motion signature.
6. Read assets/yb-tokens.css end-to-end. Memorise the token names.
7. Read assets/yb-agents.js end-to-end. The 14-agent roster lives here.
8. Diff this against the existing styles/yb.css and scripts/yb-palette.js.
   The v3 versions are simpler; v4 tokens are a superset (deeper bg, warmer ink).
9. Build the global <CommandPalette /> first. Mount at app root. It opens with ⌘K
   and surfaces every page, ticker, and agent. Lift assets/yb-palette.{js,css}.
10. Build /command second. It is the daily-driver. Lift the floor SVG layout
    from assets/yb-command.js (renderFloor function), the hero from command.html,
    and the feed from yb-command.js (renderFeed function).
```

---

## Surface → endpoint map (every wire)

Each v4 surface maps to existing endpoints in `young-bull-site/api/`. When you build the React component, hit these:

### `/command` — the agent room

| UI piece                       | Endpoint                                                              | Notes |
|--------------------------------|-----------------------------------------------------------------------|-------|
| Mastermind hero (TheAnalyst)   | `GET /api/command-voices?agent=TheAnalyst&limit=1&pinned=1`           | Pinned voice = the morning brief |
| Mastermind status (THINKING / ON STAGE / AT REST) | derived from `generated_at` of latest TheAnalyst voice           | <30m = ON STAGE, <4h = THINKING |
| Newsroom floor (14 desks)      | `GET /api/system` returns agent list + status + last_run              | `tier` column already added |
| On-deck rail (3 recent voices) | first 3 non-mastermind non-prompt voices from `/api/command-voices`   | Polls every 15s |
| Live feed                      | `GET /api/command-voices?limit=60`                                    | Already tier-filters via `KIND_GROUPS` |
| Voices/min counter             | derived client-side from voice timestamps in last 5 min               | — |
| Ask the Room                   | `POST /api/ask-room` with `{ text, user_id }`                         | Writes to `agent_voices` as `room_prompt`, voice_router fans out reactor replies |
| Dispatch                       | `POST /api/ask-room` with `{ text, dispatch_to: [...], task, target }` | NEW field shape: voice_router reads `dispatch_to` to route only to those agents |
| Spotlight agent profile        | `GET /api/agent?slug=Burry`                                            | SSR HTML, or use ?json=1 for in-app modal |
| Watchlist gold-glow            | `GET /api/me/positions` (auth-gated)                                  | Returns the user's ticker list |

### `/compare`

| UI piece                       | Endpoint                                                              | Notes |
|--------------------------------|-----------------------------------------------------------------------|-------|
| Quinn's 25-name book           | `GET /api/portfolio-live` (16-field whitelist)                        | Strip cost_basis, shares — public read |
| Layer classification of inputs | `GET /api/research-index` returns `[{ticker, layer}]`                 | Cache 1h, all 105 tickers |
| Layer distribution             | computed client-side from the response                                | — |
| Shadow Forecast                | `GET /api/shadow-forecast?tickers=MU,NBIS,...`                        | Synthetic backtest, server-side |
| Venn diagram                   | computed client-side; intersect user input with `/api/portfolio-live` | — |

### `/brief`

| UI piece                       | Endpoint                                                              | Notes |
|--------------------------------|-----------------------------------------------------------------------|-------|
| Today's brief body             | `GET /api/morning` returns `{ body_markdown, generated_at, vol }`     | Daily, generated by Railway dailyBrief agent |
| Room reactions rail            | `GET /api/command-voices?related_to=morning&limit=6`                  | Voices tagged with morning brief context |
| Audio overview player          | `GET /api/nblm?artifact=audio_overview&date=YYYY-MM-DD`               | NotebookLM Studio MP3 |
| Side vote                      | `POST /api/brief-vote { side, brief_id }`                             | New endpoint — anonymous rolling count |
| Submit a take                  | `POST /api/posts { brief_id, body }` (auth-gated for write)           | Hits `community_posts` table |

### `/research/:ticker`

| UI piece                       | Endpoint                                                              | Notes |
|--------------------------------|-----------------------------------------------------------------------|-------|
| Hero anchor card               | `GET /api/og?t=AVGO` server-side renders heros/avgo.html              | Or compose `<AnchorHero ticker={t} />` |
| Full research dump             | `GET /api/research?ticker=NBIS`                                       | Multi-source synthesis |
| Drawer (anywhere)              | `GET /api/research-vault?t=NBIS` returns SSR HTML for drawer body     | — |

### `/agent/:slug`

| UI piece                       | Endpoint                                                              | Notes |
|--------------------------------|-----------------------------------------------------------------------|-------|
| Full agent page                | `GET /api/agent?slug=Burry` SSR HTML                                  | Already built in v3 |
| Hash deep-link `/command#agent-burry` | opens the spotlight modal client-side                          | Wired in command.html init |

---

## Component name mapping (port these)

| HTML file here                  | React component there                  | Where to mount |
|---------------------------------|----------------------------------------|----------------|
| `command.html`                  | `<CommandRoom />`                      | `app/command/page.tsx` |
| Floor SVG (from yb-command.js)  | `<NewsroomFloor agents={ROSTER} />`    | inside `<CommandRoom />` |
| `heros/avgo.html`               | `<AnchorHero ticker="AVGO" />`         | `app/research/[ticker]/page.tsx` for anchors |
| Spotlight modal                 | `<AgentSpotlight slug onClose />`      | Portal at app root |
| Dispatch form                   | `<DispatchForm onSubmit />`            | inside `<AgentSpotlight />` AND inside Ask the Room |
| 12-cluster card                 | `<TickerCard ticker variant />`        | Used on /map, /research, /command coverage, /compare |
| `compare.html`                  | `<Compare />`                          | `app/compare/page.tsx` |
| `brief.html`                    | `<MorningBrief brief={data} />`        | `app/brief/page.tsx` |
| `style.html`                    | `<StyleLibrary />`                     | `app/style/page.tsx` — gate behind `?dev=1` |
| ⌘K palette                      | `<CommandPalette />`                   | Portal at app root, wired to keyboard listener |
| Ticker drawer                   | `<TickerDrawer ticker onClose />`      | Portal, opens on hash change |
| 14 monograms                    | `<AgentMonogram slug size />`          | Used in spotlight, hero, feed, every desk |

---

## Tier system contract

The new `agent_personalities.tier` column (added 2026-05-15) drives every surface decision. Match this exactly:

```ts
type Tier = 'mastermind' | 'core' | 'shift' | 'event' | 'system' | 'archived';

// Show-where rules (apply in this order)
function visibleOn(tier: Tier, slug: string, agent: any): {
  command: boolean;        // /command newsroom floor
  feed: boolean;           // /command live feed
  hero: boolean;           // /command mastermind hero card
  palette: boolean;        // ⌘K palette agent list
} {
  if (tier === 'archived' || tier === 'system') {
    return { command: false, feed: false, hero: false, palette: false };
  }
  if (tier === 'mastermind') {
    return { command: true, feed: true, hero: true, palette: true };
  }
  if (tier === 'core') {
    return { command: true, feed: true, hero: false, palette: true };
  }
  if (tier === 'shift') {
    const onShift = isAgentOnShift(agent);
    return { command: true, feed: onShift || hasRecentVoice(slug, 4 * 3600), hero: false, palette: true };
  }
  if (tier === 'event') {
    return { command: true, feed: hasRecentVoice(slug, 6 * 3600), hero: false, palette: true };
  }
  return { command: false, feed: false, hero: false, palette: false };
}

function isAgentOnShift(agent: { shift_start_hour_et: number, shift_end_hour_et: number }): boolean {
  const etHour = getETHour();           // returns 0..24, decimal
  const [s, e] = [agent.shift_start_hour_et, agent.shift_end_hour_et];
  if (s <= e) return etHour >= s && etHour < e;
  return etHour >= s || etHour < e;     // wrap (NightShift 20-04)
}
```

The 14 active personas:

```
mastermind (1): TheAnalyst       voice #d4a843   24h
core (8):      Anchor            #b8902a   24h
               BearGuard         #c4423f   24h
               TheOptimist       #5a9a4a   24h
               Burry             #c9b89e   24h
               Cathie            #67e8f9   24h
               TheDegen          #f472b6   24h
               Sentinel          #94a3b8   24h
               TheFloor          #e6b84a   9-16 ET
shift (3):     NightShift        #6366f1   20-04 ET
               DawnPatrol        #fbbf24   04-09:30 ET
               TheMacro          #fb7185   07-17 ET
event (2):     EarningsHawk      #d4a843   fires on earnings
               CloseBell         #4a9b8e   15:55 + 16:05 ET
```

Voice color is **sacred**. Never override. Render every agent name, monogram, left-border stripe in their color.

---

## Token reconciliation (v3 → v4)

v3 used `<html data-theme="light">`. **v4 uses `<html data-yb-theme="light">`.** Stick with v4's attribute name in the React port — it matches the convention in `young-bull-site/`.

The v4 token superset is in `assets/yb-tokens.css`. Key shifts from v3:

| Token         | v3 dark     | v4 dark     | v4 light  | Why |
|---------------|-------------|-------------|-----------|-----|
| `--bg`        | `#0c0d10`   | `#0e0e0e`   | `#faf8f1` | v4 sits closer to NotebookLM ref |
| `--bg-elev`   | `#131419`   | `#1a1a1a`   | `#ffffff` | Card body matches NotebookLM hero panels |
| `--ink`       | `#ffffff`   | `#f4f1e8`   | `#15181d` | Warmer cream beats stark white |
| `--gold`      | `#d4a843`   | `#d4a843`   | `#a8801a` | Sacred. Deepened on light for AA contrast. |
| `--gain`      | `#22c55e`   | `#5a9a4a`   | `#1f6b3a` | Less neon, more banking-paper |
| `--loss`      | `#ef4444`   | `#c4423f`   | `#a3341f` | Same |

When porting, **emit v4 tokens at the app root** and let v3 pages inherit. The v3 token names are a strict subset.

---

## Hard rules (must enforce in the React port)

### Voice
- **Zero em-dashes.** Every `—` in copy is a bug. Use commas, periods, parens, or " to ". Search-and-replace before commit.
- **Banned words.** Reject in PR review: `delve`, `robust`, `leverage`, `comprehensive`, `moreover`, `furthermore`, `additionally` (sentence-start), `in conclusion`, `embark`, `tapestry`, `myriad`, `vibrant`, `dynamic`, `seamless`, `groundbreaking`, `innovative`, `showcase`.
- **"I'm 17" is not the tagline.** It's a fact in body copy ("Quinn, 17, Portland"). Never the headline.
- **No last name "Byars"** on the public site, anywhere.
- **Pro positioning:** *"Free is the book. Paid is the notebook."* Not a signal service. The product is the process. Founding 50 = lifetime, no expiry.

### Visual
- **Voice colors per agent are sacred.** Mastermind `#d4a843` gold. The 13 others are listed above. Do not invent new ones.
- **Lightning bolt Y logo stays.** Don't redesign.
- **JetBrains Mono + `font-variant-numeric: tabular-nums` for every financial number.** No exceptions. Wrap with `.num` or `data-num`.
- **Hash deep-links must work.** `/command#agent-burry` opens the Burry spotlight on load. `/research/MU#hero` scrolls to hero. `/watchlist#MU` opens the MU drawer. Wire the router.
- **⌘K everywhere.** Mount `<CommandPalette />` at the app root.

### Privacy
- **Cost basis and share counts never leave the server** except on `/research/:ticker` anchor hero cards and `/pro`. The 16-field whitelist in `api/portfolio-live.js` is the boundary.
- **Test it:** `curl https://youngbullinvests.com/api/portfolio-live | grep -i 'cost_basis\|shares'` must return zero.
- **Anchor hero cards on Substack OG ARE allowed to show cost basis.** They're intentional receipt panels. Same on `/pro`.

---

## Motion presets

Lift these into Tailwind config or `globals.css`:

```css
:root {
  --ease:        cubic-bezier(0.22, 1, 0.36, 1);
  --ease-stage:  cubic-bezier(0.2, 0, 0, 1);
  --t-fast:      160ms;
  --t-med:       240ms;
  --t-slow:      420ms;
  --t-stage:     680ms;
}

/* Per-agent voice entry — sacred */
@keyframes voice-base       { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
@keyframes voice-flash      { 0% { opacity: 0; transform: scale(1.02); filter: brightness(2); } 20% { opacity: 1; } 100% { opacity: 1; filter: brightness(1); transform: scale(1); } }
@keyframes voice-unfold     { 0% { opacity: 0; transform: rotateX(-30deg); transform-origin: top center; } 100% { opacity: 1; transform: rotateX(0); } }
@keyframes voice-swoop      { from { opacity: 0; transform: translateX(-12px) skewX(2deg); } to { opacity: 1; transform: translateX(0) skewX(0); } }
@keyframes voice-authority  { 0% { opacity: 0; transform: translateY(-6px); box-shadow: 0 0 0 0 rgba(212,168,67,0.5); } 60% { box-shadow: 0 0 0 8px rgba(212,168,67,0); } 100% { opacity: 1; transform: translateY(0); } }
@keyframes voice-pulse-in   { 0% { opacity: 0; transform: scale(0.98); } 100% { opacity: 1; transform: scale(1); } }

/* Assign per agent */
.voice                              { animation: voice-base 360ms var(--ease-stage) both; }
.voice[data-agent="thedegen"]       { animation: voice-flash 380ms var(--ease-stage) both; }
.voice[data-agent="burry"]          { animation: voice-unfold 480ms var(--ease-stage) both; }
.voice[data-agent="cathie"]         { animation: voice-swoop 420ms var(--ease-stage) both; }
.voice[data-agent="theanalyst"]     { animation: voice-authority 520ms var(--ease-stage) both; }
.voice[data-agent="sentinel"]       { animation: voice-pulse-in 320ms var(--ease-stage) both; }
```

---

## Light mode

The reskin is real, not an inversion. Cream paper, deepened gold, deepened gain/loss. The `[data-yb-theme="light"]` block in `assets/yb-tokens.css` is the contract. Verify by:

```bash
# Click the LIGHT toggle on every page. Check:
#  - Receipts read clean on cream
#  - Gold has contrast ratio >= 4.5:1 against bg-elev (#ffffff)
#  - Layer hues are legible (not pastel)
#  - Schematic grid stays as a warm hairline, not a harsh black grid
```

The theme attribute is read pre-paint via the FOUC guard at the top of every page:

```html
<script>
  (function () {
    try {
      var t = localStorage.getItem('yb-theme');
      if (t === 'light' || t === 'dark') document.documentElement.setAttribute('data-yb-theme', t);
    } catch (_) {}
  })();
</script>
```

In Next.js, put this in `app/layout.tsx` as an inline `<Script strategy="beforeInteractive">`.

---

## ⌘K palette wiring (the global ritual)

The palette is the discovery surface. Every page reachable, every ticker openable, every agent dispatchable.

```tsx
// app/layout.tsx
import { CommandPalette } from '@/components/CommandPalette';

export default function RootLayout({ children }) {
  return (
    <html data-yb-theme="dark">
      <body>
        <CommandPalette />
        {children}
      </body>
    </html>
  );
}
```

The palette has 4 source lists:
1. **Pages** — every route in the app
2. **Tickers** — pulled from `/api/research-index` (105 names, cached 1h)
3. **Agents** — pulled from `/api/system` (14 active, filtered by tier)
4. **Actions** — toggle theme, jump to /command#ask, open dispatch

Hotkeys:
- `⌘K` or `Ctrl+K` toggles
- `/` opens (when not in an input)
- `↑`/`↓` navigate
- `Enter` execute
- `Esc` close
- `g h`, `g c`, `g w`, etc. — vim-style page jumps (already in `scripts/yb-palette.js`)

The palette also **owns the ticker drawer**. When a user picks a ticker, the drawer opens. Hash deep-link `#MU` opens the MU drawer on load.

---

## Hash deep-link map

| URL                            | Effect |
|--------------------------------|--------|
| `/command#agent-burry`         | Opens Burry spotlight modal |
| `/command#ask`                 | Scrolls to Ask the Room, focuses input |
| `/command#dispatch`            | Same, switches to Dispatch Mode |
| `/research/MU#hero`            | Scrolls to anchor hero |
| `/watchlist#NBIS`              | Opens NBIS ticker drawer |
| `/map#power`                   | Opens Power layer drill |
| `/compare#tickers=MU,NVDA,AVGO`| Pre-fills input + runs compare |
| `/brief#side=bull`             | Pre-votes bull side, opens take input |

Implement with the Next.js `useSearchParams` / `usePathname` hooks, or a small client-side router-level effect.

---

## Common pitfalls (don't do these)

1. **Don't override voice colors with Tailwind's default green/red.** Use the exact hex from `agent_personalities.voice_color`. The room is the brand.

2. **Don't wrap the room in a Suspense boundary that strips animations.** The voice entry animations are diagnostic of agent personality. Disable only with `prefers-reduced-motion`.

3. **Don't fetch `/api/command-voices` on the server.** Poll from the client every 15s. The endpoint is cached at the edge (`s-maxage=15`). Server fetch = stale immediately.

4. **Don't render the floor as a bunch of `<div>` desks with absolute positioning.** Use one SVG with `<g class="floor-desk">` per desk. The schematic grid backdrop only reads if the floor is on one canvas. See `assets/yb-command.js` `renderFloor()`.

5. **Don't generate 81 ticker card components.** ONE `<TickerCard>` that consumes `data-cluster` and `data-layer` from the metadata. The cluster maps live in `assets/yb-cards.css`.

6. **Don't put cost basis in the wire response.** The 16-field whitelist is in `api/portfolio-live.js`. Strip everything else. Anchor heros are SSR'd server-side from `ticker_metadata` + position data — never client-fetched.

7. **Don't ship without checking light mode on every page.** Each surface needs an honest light-mode pass. Cream paper aesthetic, not white-bg inversion.

8. **Don't add a new agent without adding a voice color.** Every agent needs a `voice_color` in `agent_personalities`. Without it, the room is generic.

9. **Don't break hash deep-links during a router upgrade.** The contract is documented above. If you migrate to App Router from Pages Router, write a regression test for each hash route.

10. **Don't generate new content with banned words.** Wire the banned list into a CI lint that scans `.tsx` and `.md` files.

---

## Don't ship until

- [ ] Light mode renders cleanly on `/command`, `/style`, `/compare`, `/brief`, every anchor hero.
- [ ] Hash deep-link works: `command.html#agent-burry` opens the Burry spotlight on load.
- [ ] Hash deep-link works: `compare.html#MU` opens the MU drawer.
- [ ] ⌘K palette finds every page, every ticker, every agent.
- [ ] Mobile `/command` newsroom floor scales (SVG `viewBox` does it; verify on 390px).
- [ ] Ticker drawer slides from bottom on mobile (`<720px`).
- [ ] The 16-field whitelist is enforced on the wire: `curl /api/portfolio-live | grep -i cost_basis` returns nothing.
- [ ] Em-dash audit: `grep -rE '—' src/` returns zero in non-comment code.
- [ ] Banned word audit: same, for the banned list above.
- [ ] Every voice has a `voice_color`. Every agent has a `tier`. Verify with a Supabase query.
- [ ] Spotlight modal closes on Esc, overlay click, AND the close button.
- [ ] Dispatch flow round-trips: pick desks, pick task, pick target, submit. Voice_router fans out replies.
- [ ] Audio overview player on `/brief` plays the right NotebookLM file for the day's brief.

---

## Out of scope this session (next iteration)

- Real-data plug-in for `/command` voice feed (today it uses `window.YB.MOCK_VOICES`; swap for `/api/command-voices` with 15s polling).
- Map page anchor-strip refresh (the v3 `/map` works; v4 just locks the 5-anchor visual treatment with hidden cost basis).
- Vestor chat embed (parked per the v3 handoff; the design is in `pages/vestor.html` but the API surface is gated).
- Per-ticker hero generator (auto-generate the 56 remaining cards from `ticker_metadata.cluster` + `.canonical_layer`).
- Substack OG endpoint Puppeteer wiring (the HTML hero cards are ready; needs a 30-line `/api/og` renderer to convert HTML → PNG).
- Mobile responsive pass on every v3 page.

---

## Reference materials

In this project:
- `assets/refs/nblm-mu-hbm3e.png` — MU NotebookLM Studio hero (the brand)
- `assets/refs/nblm-vst-behind-meter.png` — VST NotebookLM hero
- `assets/refs/nblm-nbis-capex.png` — NBIS NotebookLM hero
- `assets/refs/nblm-xndu-quantum.png` — XNDU NotebookLM (moonshot reference, not anchor)

External:
- Linear.app — technical density done right
- Hindenburg Research — raw authority, no slop
- Bloomberg Terminal — data-first hierarchy

---

## Final note from Quinn

> "Free is the book. Paid is the notebook. The product is the process. Real money, real positions, real receipts. Build the room so it reads while I sleep."

Real $58K book, +185% lifetime, three years compounding, 239 Substack subscribers, six founding members. Pro launches July 22, 2026. The room is on the record.

Built by Quinn, 17, Portland. Drawn by the room.
