Living Document
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.
Display
Page Title
Section Heading
Subsection
Hero title / Now statement
I'm a product designer who works at the intersection of emerging technology and human experience.
Default body text. Comfortable for long-form reading. Max-width is capped at 65ch to maintain an ideal line length.
Navigation links, footer links, secondary labels.
Section 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.
Selected Work
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.
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.
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.
import { Badge } from '@/components/ui/badge';
<Badge>Default</Badge>
<Badge variant="secondary">Secondary</Badge>
<Badge variant="destructive">Destructive</Badge>
<Badge variant="outline">Outline</Badge>