Playground
Design System
Design System
Everything I built to make this site. Colors, type, and components — shown as-is, no abstractions.
Color Palette
The full palette — semantic tokens for UI, and the brand ramps they're built from.
All values are CSS custom properties in global.css using HSL.
Theme colors
Each card shows a background color with its paired foreground color as text — so you can immediately judge contrast. These are the tokens Basis UI components read directly.
UI colors
Single-use tokens for structural UI elements — not surfaces or text, but borders, inputs, and focus rings.
Border
--border Card edges, dividers
Input
--input Form field borders
Ring
--ring Focus ring — Navy 700
Brand ramp — Navy
Brand ramp — Lemon
Neutral scale
Component aliases
The --color-* tokens are backward-compatible aliases used
by the handbuilt components (Button, Nav, Footer, Carousel,
SectionLabel). They sit on top of the semantic tokens above.
Fixed aliases always resolve to the same value
regardless of theme. Theme-aware aliases flip
automatically — toggle dark mode above to see them change.
Fixed — same in light and dark
Navy
--color-navy Footer bg, code blocks, button hover
#081F5D → Navy 900
Royal Blue
--color-royal-blue Links, nav hover, hero title
#1040A8 → Navy 700
Lemon
--color-lemon SectionLabel bg, Footer name
#F1F11C → Lemon 500
Citrine (unused)
--color-citrine Defined but not used in any component
#BFBF12 → Lemon 700
Slate
--color-slate Borders, placeholders, Footer links
#808080 → Neutral 500
Theme-aware — flips with dark mode
Heading
--color-heading h1–h6, nav name, carousel title
→ --primary
Charcoal
--color-charcoal Body text, nav links, button text
→ --foreground
Surface
--color-surface Section backgrounds, preview areas
→ --muted
White
--color-white Card backgrounds, slide backgrounds
→ --background
Intentionally excluded
Shadcn/ui defines additional token groups that this site does not use. They are absent by design — not an oversight.
Chart 1–5
--chart-1…5 Data visualization palette. Not needed — no charts in this portfolio.
Sidebar tokens
--sidebar-* Sidebar navigation system. Not needed — this site uses a simple top nav.
Typography
The type scale used across the site. Fraunces for display headings, Geist for everything else. Sizes are defined as CSS variables 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
Basis UI Components
Astro components from Basis UI, customized to match this palette. Each one is a copy-paste component — source lives in src/components/ui/. Toggle the preview to see dark mode.
The token pipeline
Every color decision starts in Figma as a variable and ends up in a live component through a four-layer chain. A brand update touches one value and cascades everywhere.
Step 1 — Figma variable (designer's source of truth)
color/navy = #081F5D
Step 2 — Brand ramp token in global.css
--navy-900: 223.8 84.2% 19.8%; /* stored as HSL, no hsl() wrapper */
Step 3 — Semantic mapping in global.css (the bridge layer)
--primary: 223.8 84.2% 19.8%; /* "primary" maps to Deep Navy */
--ring: 221.1 82.6% 36.1%; /* focus rings use Navy 700 */
Step 4 — Basis UI component in Button.astro
bg-primary → resolves --primary → #081F5D in light / #C0D4F4 in dark
hover:bg-ring → shifts to Navy 700 on hover / focus Component gallery
Button — src/components/ui/forms/Button.astro
Variants
Sizes
States
Note: ghost and outline buttons show --accent (Lemon) on hover
Text — src/components/ui/display/Text.astro
Variant picks the correct HTML element automatically (h1, h2, p, span, etc.) and applies the matching token. All colors adapt to dark mode.
Hero Title
Page Title
Section Title
Card Title
Card Subtitle
The quick brown fox jumps over the lazy dog.
Card — src/components/ui/display/card/
Divider — src/components/ui/layout/Divider.astro
Always uses --border — flips automatically in dark mode.
No accent variant in the component; use a wrapper with a colored background for accent rules.
thickness="1" (default)
Content above Content belowthickness="4"
Content above Content belowdirection="vertical" (needs a flex-row parent)
Left content
Right content
Badge — src/components/ui/feedback/Badge.astro
Variant → token reference
Each variant name maps to a specific semantic token pair. Changing
the token in global.css updates every component that
uses that variant instantly.
Button / Badge variants → token used
─────────────────────────────────────────────
default → bg-primary text-primary-foreground
secondary → bg-secondary text-secondary-foreground
destructive → bg-destructive text-destructive-foreground
outline → border-border bg-background hover:bg-accent
ghost → bg-transparent text-foreground hover:bg-accent
link → text-primary (underline on hover)
Card variants → token used
─────────────────────────────────────────────
default → bg-card border-border
muted → bg-muted border-border
outline → bg-transparent border-2 border-border Astro Components
Hand-built components specific to this site. These predate the Basis UI migration and may be refactored over time. What you see here is the real component — no separate demo version.
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" /> 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 />