loading...
A portfolio is a strange product: its content is the designer, but its craft is the argument. A static site can show finished work; it can't show how decisions actually get made.
I wanted the site to do both. Visitors should be able to ask questions the way they would in a first conversation — about experience, process, specific projects — and get grounded answers instead of navigating a page hierarchy.
And the site itself should demonstrate the process I'd bring to a team: decisions made in the working artifact, checked by measurement, and written down where the next person can find them.
This site has a design system, but it didn't start in a design tool. The source of truth is a markdown file checked into the repo — tokens, type scale, spacing rules, motion tiers, and a running record of which decisions are settled and which are still open. The token layer is implemented three times in lockstep: CSS custom properties, the Tailwind theme, and a small JS constants file, commented as a set so drift is visible.
Figma exists in this workflow — but it mirrors the code. The design-system file carries variable collections with the same token names as the CSS, and its baseline pages hold captures of the live site. The intent going forward: explore there when it's useful, and pull decisions back as one-file token diffs.
The rule I held myself to: wherever a decision could be measured, it ships with its measurement.
Some decisions ran the loop more than once. The per-study tile colors shipped as a fully validated palette — and then I removed them: reordering the grid forced hue swaps to keep the color-blindness checks passing, which proved the colors were decoration, not identity. The grid is monochrome now, red reserved for interaction. Both the decision and its reversal are recorded with their reasoning.
The same happened with page width: a unification pass narrowed case studies to a 768-character-theory measure, and a look at the rendered pages reversed it the same day — longer pages and double-wrapped bullets cost more than the line-length theory saved. The heuristic lost to the artifact. That's the point of working code-first: the real page is always available to overrule the abstraction.
Motion went through the same discipline. One near-symmetric ease had been driving everything, which read as mechanical; it became a small set of tokens — expo-out for entrances, sharp-in for exits, springs for direct manipulation — with a 160ms floor on feedback because the old 100ms was below the threshold where a transition reads as a response.
Case studies run about ten viewports, and the original mount-everything animation played 90% of its entrance off-screen. Content now scroll-reveals, with a separate slower cascade reserved for the blocks actually on screen at load. Parallax was considered and rejected: the void stays still; the type moves.
The landing page is a centered composer. Type a question and it rides into the chat as a single continuous element — the bar travels to its docked position, widens, and the message sends from there. The entrance runs as a ladder of beats: bar widens, rule draws in, controls fill, conversation fades up, suggestion chips arrive last. Every other entry point into the chat gets a quieter version that resolves in place — a nav link isn't a composer, so nothing pretends to travel.
Details that took real iteration: the suggested prompts are a recruiter-ordered pool that backfills as chips are consumed; the chip rail collapses by animating its height with the contents sliding, so the conversation above never jumps; a returning visitor's stored thread gets a "reconnected" seam line instead of silently reappearing. The chat itself stays mounted across every route, so navigating away mid-answer doesn't lose the response.
The persona answers from my actual content: the system prompt, resume, and case-study summaries are injected directly into every request, with an optional vector-store search on top. The response is a strict JSON contract enforced with structured output — including a field where the model cites which case study an answer draws on. Those citations are validated server-side against the real content directory, so a hallucinated slug can never become a dead link.
The unglamorous edges got design attention too: the static prompt prefix is hashed once into a stable cache key so requests share the provider's prompt cache; timeouts, length caps, and an abuse guard bound the public endpoint; upstream errors never reach the visitor — failures answer in persona, with contact info. When someone asks for access to the password-gated studies, the chat concierges them through: the gate is a soft filter by design, and the password is injected from an environment variable.
A chatbot that speaks as me can fail in ways a UI can't: wrong self-labels, leaked instructions, broken formatting, out-of-scope helpfulness. Any prompt edit, content regeneration, or model change can silently regress it.
So the persona has a regression harness. Twenty cases across four categories — recruiter questions, out-of-scope requests, prompt-injection attacks, and ambiguous input — each scored by ten programmatic checks (JSON contract, formatting rules, forbidden labels, system-prompt leaks) plus LLM-judge checks for response pattern and staying in persona. Prompt changes ship with before-and-after runs; the persona rewrite and the link-citation contract both landed at 20/20.
The harness earns its keep on the boring failures. One formatting case started failing after a recent change — sampling the same question repeatedly showed the old prompt failed it just as often, so the "regression" was pre-existing flakiness, now documented in the eval's readme instead of triggering a rollback.
The eval builds its prompt through a mirror of the production code path and prints the same cache-key hash, so drift between what's tested and what ships is visible immediately.
The site's content had the same failure mode as its prompt: claims drifting from ground truth. The fix was the same shape — build a pipeline.
Case studies are rewritten from their source design files through a process that collects provenance-tagged facts first, drafts only against those facts, then runs two independent gates: a fresh-context audit agent that classifies every claim as grounded or not, and a blind panel of three recruiter personas who score the old and new versions without knowing which is which. A rewrite only ships if it survives the audit and wins the panel. Six client studies went through it.
The chatbot reads generated summaries of the same case-study files the site renders — one source of truth feeding both the persona and the pages. This rewrite is itself running through that pipeline — provenance-tagged facts, an independent audit, and a blind panel.
The site is live on Vercel and does the two jobs it was designed for: the work is readable, and the conversation is real — grounded answers, cited sources, honest failure states.
The process left artifacts a hiring panel can inspect: a checked-in design-system document with settled-and-open decisions, an eval harness with its dataset and rubric, a content pipeline with its audit gates, and a commit history where visual decisions ship alongside their measurements.
As a personal project the outcomes stay qualitative by choice — there are no analytics yet — but the claims on this page trace to repo artifacts — commits, the design-system file, the eval harness, the pipeline.