Playground

Design System

The foundation of this site — tokens, typography, and components built for AI-ready design.

Prototypes

Explorations and experiments as they take shape.

Color

All colors are defined as CSS custom properties in tokens.css. Nothing in this project uses a hardcoded hex value — every color reference points back to one of these tokens.

Brand palette

Navy

--color-navy

Headings, footer background

Royal Blue

--color-royal-blue

Links, hover states, hero title

Lemon

--color-lemon

Section labels, footer accents

Citrine

--color-citrine

Secondary accent, available

Slate

--color-slate

Borders, dividers, placeholders

Charcoal

--color-charcoal

Body text

Surfaces

White

--color-white

Primary page background

Surface

--color-surface

Alternate section backgrounds

Typography

All type is set in Fraunces — a variable serif with optical sizes and a warm, editorial character. Sizes come from a base-16 scale defined in global.css.

--text-6xl · 80px · Bold

Display

--text-5xl · 56px · Bold · h1

Page Title

--text-4xl · 40px · Bold · h2

Section Heading

--text-3xl · 32px · Bold · h3

Subsection

--text-2xl · 24px · Medium

Hero title / Now statement

--text-lg · 18px · Regular · Body lead

I'm a product designer who works at the intersection of emerging technology and human experience.

--text-base · 16px · Regular · Body

Default body text. Comfortable for long-form reading. Max-width is capped at 65ch to maintain an ideal line length.

--text-sm · 14px · Regular · UI text

Navigation links, footer links, secondary labels.

--text-xs · 12px · Bold · Label

Astro Components

Every component below uses the same HTML and class names as the live portfolio. There is no separate "demo" version — what you see here is the real component.

Button

Two variants: primary (navy fill) and secondary (surface + outline). Both use a pill shape and share the same hover behavior — the background shifts and the arrow icon rotates from ↗ (rest) to → (hover) over a smooth 0.2s ease transition. When an href prop is passed, the component renders as an <a> tag (correct for navigation). Without href, it renders as a <button> (correct for actions). The download prop triggers a file save instead of navigating. The optional icon prop accepts an SVG string — when provided, the icon renders on the left of the label and the right-side arrow is hidden.

import Button from '../components/Button.astro';

<!-- Primary (default) -->
<Button label="LinkedIn" href="https://linkedin.com/in/..." variant="primary" />

<!-- Secondary -->
<Button label="Download Resume" href="/resume.pdf" variant="secondary" download={true} />

<!-- Disabled -->
<Button label="Coming Soon" href="#" variant="primary" disabled={true} />

<!-- Icon left (no href = renders as <button>, not <a>) -->
<Button label="Reset" variant="secondary" icon='<svg ...>...</svg>' />

Text Link

Inline hyperlinks for body copy and navigation lists. Royal blue at rest — no underline, to keep prose clean. On hover: color shifts to --color-heading (navy in light mode, near-white in dark mode) and an underline appears as a clear interactive signal. Apply using the .text-link class on any <a> tag. Do not use for buttons or CTAs — use the Button component for those instead.

The thread across my work is keeping people in their flow. The best interfaces never pull you out of what you are doing — they blend into the task itself.

<!-- Apply .text-link to any <a> tag -->
<a href="/some-page" class="text-link">Link label</a>

<!-- In a navigation list -->
<ul>
  <li><a href="#color" class="text-link">Color</a></li>
  <li><a href="#typography" class="text-link">Typography</a></li>
</ul>

Section Label

A small lemon pill used to introduce each major section. Built with display: inline-block and background-color: var(--color-lemon). Lemon is used nowhere else on the page at full saturation — this constraint keeps it feeling like a signal, not decoration.

import SectionLabel from '../components/SectionLabel.astro';

<SectionLabel label="Selected Work" />

Navigation

Top bar with name on the left and anchor links on the right. On screens 768px and smaller, the links collapse into a hamburger menu that toggles open and closed. Links use /#section anchors so they work correctly from any page, not just the homepage. aria-current="page" is applied automatically to the active link.

import Nav from '../components/Nav.astro';

<Nav />

Project Card

Used in the Work section to display a project. Image placeholder uses aspect-ratio: 4 / 3 to stay proportional at any width. Cards sit in a flex row with flex: 1 so they share space equally regardless of content length.

[ image ]

Project title goes here

import ProjectCard from '../components/ProjectCard.astro';

<ProjectCard title="Project title goes here" />

Theme Toggle

Sun/moon icon button that lives in the navigation. Clicking it switches between light and dark mode and saves the choice to localStorage so it persists on future visits. On first load, the site reads the user's OS preference (prefers-color-scheme) as the default — the toggle lets them override it. The moon icon is shown in light mode (click to go dark), the sun icon in dark mode (click to go light).

import ThemeToggle from '../components/ThemeToggle.astro';

<ThemeToggle />

Carousel

A manually controlled image carousel. Navigate with the arrow buttons or click any pagination dot to jump directly to a slide. Keyboard users can also use the left/right arrow keys. No auto-scroll — the user is always in control (WCAG 2.2 guideline 2.2.2). Each slide shows an image with a title and subtitle caption below it. To update content, edit the slides array at the top of Carousel.astro.

import Carousel from '../components/Carousel.astro';

<Carousel />

Footer

Navy panel at the bottom of the page — the strongest use of the brand color. Lemon is used as the name accent, creating a bookend with the lemon section labels above. On mobile, name and links stack vertically and center-align. The component itself has no margin-top — spacing is set at the page level so it can be dropped onto any page without side effects.

import Footer from '../components/Footer.astro';

<Footer />

shadcn Components

shadcn/ui is an open-source component library used by teams at Vercel, Linear, and Resend. Components are not installed as a dependency — the source code lives in src/components/ui/ so it can be edited directly. Styling is driven entirely by CSS variables, which means a single token change propagates to every component at once.

The token pipeline

Every color decision starts in Figma as a variable and ends up in a live component through a four-layer chain. Nothing is hardcoded at any step — a brand update touches one value and cascades everywhere.

Step 1 — Figma variable (designer's source of truth)
         color/navy = #081F5D

Step 2 — Brand token in tokens.css
         --color-navy: #081F5D;

Step 3 — Semantic mapping in global.css (the bridge layer)
         --primary: var(--color-navy);    /* "primary" is shadcn's name for it */
         --ring:    var(--color-royal-blue);

Step 4 — shadcn component in button.tsx
         bg-primary   → reads --color-primary → reads --primary → reads --color-navy
         hover:bg-ring → shifts to royal-blue on hover

What each variant controls

Each shadcn variant maps to a different semantic token. Changing the token value updates the variant everywhere it is used — no per-component overrides needed.

Default   → --primary         (navy fill, white text)
Outline   → --border          (border stroke color, currently slate)
Secondary → --secondary       (subtle surface background)
Ghost     → --muted           (hover-only background, invisible at rest)

Button — custom vs. shadcn

Left: Button.astro — hand-built with custom CSS classes, no dependencies. Right: components/ui/button.tsx — the shadcn version whose appearance comes entirely from the token mapping above. Both render identically because they read from the same brand tokens. The shadcn version adds four variants and scales to a full component library without writing new CSS.

Custom — Button.astro

Primary Secondary

shadcn — Button.tsx

import { Button } from '@/components/ui/button';

<Button>Default</Button>
<Button variant="outline">Outline</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="ghost">Ghost</Button>

Badge — imported via Figma MCP

This component was read directly from a Figma file using the Figma MCP server. The MCP detected that the Figma variables (--primary, --border, etc.) match the semantic tokens already mapped in global.css — so no color changes were needed. The badge automatically uses your brand colors.

Default
Secondary
Destructive
Outline
import { Badge } from '@/components/ui/badge';

<Badge>Default</Badge>
<Badge variant="secondary">Secondary</Badge>
<Badge variant="destructive">Destructive</Badge>
<Badge variant="outline">Outline</Badge>