Component Library

All UI components currently in use across the site.

Typography

h1 — text-4xl font-semibold tracking-tight

I ship product.
I also write the code.

h2 — text-3xl font-semibold tracking-tight

About

Section label — text-xs font-mono uppercase tracking-widest text-zinc-400

Work

Eyebrow — text-sm font-mono text-zinc-500

Senior PM · Builder

Body large — text-lg text-zinc-500 leading-relaxed

Nine years in product at HubSpot across AI, mobile, and growth. I use AI-native workflows to go from idea to working product — and I build my own tools in the gaps.

Body — text-sm text-zinc-500 leading-relaxed

Built mobile reporting from scratch, targeting the sales leader persona. Grew to 55k weekly users. Led the React Native migration that cut feature delivery time by 3×.

Label / meta — text-xs font-mono text-zinc-400

2021 – 2024

Hero

Senior PM · Builder

I ship product.
I also write the code.

Nine years in product at HubSpot across AI, mobile, and growth. I use AI-native workflows to go from idea to working product — and I build my own tools in the gaps.

GitHub ↗

Built with Next.js and Claude Code.

Button

Variants

Sizes

States

Badge

shadcn variants

default
secondary
destructive
outline

As used in the site — tech stack labels

Next.js
Supabase
TypeScript
Claude Code
SQL

Input

Default

Password (as used in CaseStudyGate)

Disabled

ProjectCard

Pocket Planner

A personal finance tracker that turns bank statements into weekly insights. No manual entry, no syncing — just upload a CSV and get a clean summary.

Next.js
Supabase
Read

Interview Prep Tool

AI-powered mock interview sessions with structured feedback. Generates questions from a job description and scores answers against the rubric.

Claude API
TypeScript
Lovable

ProjectGrid — section header pattern

Work

↳ followed by the grid of ProjectCards above

SkillCard — used on /skills

Skill name in mono (it's invoked as a command), description, optional external source link. Used in a 2-column grid per tier.

slack-er

Triages my Slack and drafts replies in my voice for messages that genuinely need one. Drafts only, never sends.

deslop

Detects and removes AI writing tropes from prose. (No source link — internal tier.)

Experience row

Senior PM, AI Personalization & Ecosystem

HubSpot

2025 – present

Building Breeze Projects — the personalization layer for Breeze Assistant — and the MCP ecosystem strategy that connects third-party apps to Breeze.

Senior PM, Growth & Onboarding

HubSpot

2024 – 2025

Led HubSpot's #1 2025 product priority: product-led onboarding for Pro customers. Shipped an AI conversational onboarding agent that doubled plan adoption.

CaseStudyGate

This case study is private.

Incorrect password.

Loading state

Screenshot — ChromeBar

ChromeBar with URL

app.hubspot.com/reports/123456

screenshot image here

Mobile reporting dashboard — 55k weekly active users

ChromeBar without URL

screenshot image here

BeforeAfter

Before/after pair, side by side. Each image is click-to-expand (full-screen lightbox, Escape or click to close). Stacks on mobile.

Before

app.hubspot.com/onboarding

before.png

Static checklist — 12% completion rate

After

app.hubspot.com/onboarding

after.png

AI conversational agent — 2× plan adoption

MediaPair

Two media side by side, top-aligned, equal-width columns (stacks on mobile). Pass any two children — typically a Screenshot and a Video. Use each child's mediaClassName to lock both to the same height, e.g. h-64 sm:h-72 with object-contain / object-cover.

screenshot.png

object-contain

demo.mp4

object-cover

MobileScreenshots

2-column grid

mobile-1.png

Home screen

mobile-2.png

Report detail

3-column grid

1

2

3

Single (max-w-xs centered)

single mobile screen

Business card scanner

Video

HTML5 player with optional caption — for screen recordings and demos. Chrome-free, controls on.

video player (controls, poster)

Unedited screen recording — caption goes here

Metrics

A row of hard numbers. Value + mono label. Supports 2, 3, or 4 items.

1:09
Time to trace
5
Clicks
0
Warnings

Card (shadcn — available, not used directly on site yet)

Card Title
Card description goes here.

Card content area. Any elements can go here.

ComparisonTable

Options laid side by side. First column is the row label (zinc-700), the rest is prose (zinc-500). Cells accept any ReactNode. Scrolls horizontally below ~680px.

ApproachHow it worksGlass box?Verdict
Learned baselineA model trained on past runs flags anomalies.No - opaque by constructionLeft out. Verdicts can’t be traced.
Explicit expectationDeclare what a step should return, check it.YesBuilt. Inspectable by construction.

Color tokens

zinc-900

Primary text

zinc-500

Secondary text

zinc-400

Labels / meta

zinc-300

Decorative (dots)

zinc-200

Borders (hover)

zinc-100

Borders (rest)

zinc-50

Badge bg / captions

white

Page / card bg

Favicon / App Icon

SVG source — src/app/icon.svg

vm

32×32

vm

64×64

vm

128×128

On dark background

vm

32×32

vm

64×64

Design tokens

Shaperounded rect, rx 4.5 (≈14% corner radius)
Fillwhite
Stroke#18181b (zinc-900), 1.5px
Lettermark"vm" — Georgia serif, 15px, weight 400
Letter-spacing0.5

AssuranceRailPrototype (case-study-only — Tines)

Bounded, low-fidelity interactive mock in Tines' light visual idiom, framed like a contained product surface. Toggle Autonomy vs Governed autonomy and run the agent. The point it dramatizes: in autonomy the agent certifies its own steps; in governed an independent rail enforces the agent's declared expectation and withholds its verdict on a divergence, then hands off to Workbench. Logic is unit-tested in src/lib/assurance-rail.ts.

◆ tinesAlert Investigation Agent
  1. Receive phishing reportWebhook
    expects: report has a URL· inferred from downstream use
  2. Extract domainTransform
    expects: domain is resolvable· inferred from downstream use
  3. Enrich IPipinfo
    expects: hosting_provider is non-empty· declared by agent · AI
  4. Check threat feedURLhaus
    expects: threat status returned· declared by agent · AI
  5. Score riskTransform
    expects: score derived from full enrichment· inferred from downstream use
  6. Open ticketTicketing
    expects: ticket created with enrichment· builder assertion
An agent investigates a reported phishing URL across ipinfo and a threat feed, then opens a ticket. In autonomy, the agent decides for itself whether each step succeeded.