How this site was built
This site is part of the portfolio. It's built the way I build at work: a small design system, content kept as data, and guardrails in the system instead of in review. This page shows how it's put together, including the parts AI helped with.
The stack
A static site generated by Eleventy from Nunjucks templates. There's no client-side framework and no build pipeline beyond Eleventy itself. The CSS is written by hand, and the little JavaScript there is (the mobile menu, and a scroll trigger for the number animations) is inlined where it's used. Every page is readable with JavaScript turned off.
Layout runs on one named-column CSS grid. A section picks its
width by name (breakout, two-thirds,
left-half) instead of nesting wrapper divs, so the
markup describes intent rather than geometry.
Content as data
Every number on this site lives in one file,
_data/impact.json. Pages never type a metric. They
reference it by name, so the homepage, the case studies, and the
work index can't disagree. This is the real entry behind the
28x on the homepage, pulled in at build time:
"iconOutput": {
"outcome": "creativeTime",
"value": "28",
"unit": "x",
"label": "Designer output multiplier: one icon becomes every brand color, theme, and format",
"project": "self-service-tooling"
}
The data files carry rules, and the templates enforce them:
- A metric with no value renders nothing. Not a dash, not "TBD". An unconfirmed number can't reach the page, so nobody can fill the gap with an estimate.
- A quote with no attribution never renders. Testimonials are credited by title and company, and an uncredited one stays out.
- Projects are a list, not markup.
_data/projects.jsondrives every project card and the "Next up" link at the bottom of each case study. Reordering the list rewires the whole path through the work. - Drafts can't ship by accident. Working notes go in template comments that are removed at build time, and raw source material sits in a folder the build ignores.
It's the same idea as my self-service tools: when the rule is built into the system, nobody has to remember to check it.
Design tokens
These tables are generated from the stylesheet when the site
builds, and each swatch is painted with the live CSS variable.
Contrast is calculated against the page background
(#29353d), the only surface text sits
on. WCAG AA needs 4.5:1 for body text, and AAA needs 7:1.
| Swatch | Token | Value | Contrast | Text use |
|---|---|---|---|---|
--zero-color |
#000000 | 1.67:1 | Not for text | |
--first-color |
#020622 | 1.59:1 | Not for text | |
--second-color |
#29353d | 1.00:1 | Not for text | |
--third-color |
#325167 | 1.50:1 | Not for text | |
--forth-color |
hsl(192, 35%, 46%) | 3.39:1 | Large text only | |
--fifth-color |
oklch(80.5% 0.062 211.9) | 7.01:1 | AAA | |
--sixth-color |
#dae8f3 | 10.07:1 | AAA | |
--seveth-color |
#ffffff | 12.57:1 | AAA | |
--emphasis-one-color |
oklch(82.3% 0.096 307.3) | 7.06:1 | AAA |
| Swatch | Token | Points to | Contrast | Text use |
|---|---|---|---|---|
--primary-color |
--fifth-color |
7.01:1 | AAA | |
--background-color |
--second-color |
1.00:1 | Not for text | |
--text-color |
--sixth-color |
10.07:1 | AAA |
The shifting accent on headings and numbers is one animated
color, cycling through six stops every ten seconds. Because it's
always moving, every stop has to pass on its own, and so does the
color it rests on when motion is turned off
(#90cbd7, 7.01:1).
The lowest of them is 7.01:1.
-
0%
#FFA8CE7.03:1 · AAA -
20%
#FFBE987.82:1 · AAA -
40%
#D9C7BF7.70:1 · AAA -
60%
#B3E5CD8.98:1 · AAA -
80%
#B7DAE18.46:1 · AAA -
100%
#CFBAE47.06:1 · AAA
Fluid type scale
Each step scales smoothly with the viewport between a minimum and
maximum size, using clamp(). Resize the window and
the samples below change size; nothing jumps at a breakpoint.
AI, with guardrails
A tool like the others
AI is another self-service tool. It multiplies what one person can make, and the guardrails decide what it's allowed to ship.
The tools I build let one person do the work of many without going off-brand, because the rules live in the tool instead of in a review step. I use AI the same way. It takes the repetitive work, and written rules plus a human read stand between it and anything that ships.
- What it multiplies
- Extracting data, refactoring, translating code between frameworks, testing, troubleshooting, and realistic placeholder data.
- Guardrails
- Written standards the agent reads before it writes anything, data rules that keep an invented number from ever reaching a page, and a human review of every change.
- What stays human
- What to build, which numbers are true, and what ships.
On this site
-
Research the target
While tailoring resumes in Claude against about 30 job descriptions, I collected the requirements that kept coming up: system-level work, tools for non-designers, design-to-code fluency, accessibility, and AI in the workflow. That became a written brief of what this portfolio has to prove, and the current structure follows it.
-
Write the guardrails down
The repo holds the same briefing a new teammate would get:
CLAUDE.mdfor architecture,PRODUCT.mdfor audience, positioning, and the no-invented-numbers rule, andDESIGN.mdfor the design system, documented from the shipped CSS rather than from intentions. -
Let the tool do the volume
Claude Code works in the repo with me: restructuring pages, writing templates, and building pieces like the token tables above. A design skill (Impeccable) holds it to the documented system and an accessibility floor.
-
Review everything
Every change is a diff I read before it ships. Every number comes from me. The data rules above apply to the assistant too: if it doesn't have a real figure, the template shows nothing.
At work
At F&G I serve as the front-end and UX expert in the Developer
Community of Practice, and I wrote the front-end guides F&G's AI
coding agents follow: a design.md and an
accessibility.md. They do for those agents what
DESIGN.md does for this site
(more in the UX practice case study).
In my own project work, AI has handled:
-
Self-Service Tooling
- Icon generator: Extracting icons that had been inlined as SVG into separate, standalone files.
- Icon generator: Testing that the generator's logic produced exactly what it was supposed to.
-
Annual Statement Digital Transition
- Generating placeholder data for design mockups that followed the patterns of real customer data.
-
Digital Underwriting Guide
- Extracting the underwriting tables from a legacy PDF into JSON arrays.
- Refactoring the first working version into configuration arrays and smaller functions, so the rules live in data instead of logic.
- Translating components from React to vanilla JavaScript, which cut dependencies.
- Troubleshooting integrations.
Where I draw the line: I haven't shipped AI features in a product. My AI work is in how things get made: the workflow, and the written standards agents follow. And my creative work doesn't use generative AI at all. It's made by hand.
Accessibility
I lead accessibility work professionally, so this site has to hold up to an audit. These are the mechanisms, not just intentions:
- Type chosen for legibility. Atkinson Hyperlegible, developed by the Braille Institute for low-vision readers, sets every heading and paragraph.
- Contrast is calculated, not eyeballed. Every text color passes AA against the background, as the tables above show. Body text is 10.07:1, well past AAA.
- Motion is opt-in. The number count-ups and the hand-inked reveals only run when your system allows motion. Otherwise, and without JavaScript, you see the final state immediately.
- Numbers are real text. Each animated figure has a plain-text twin for screen readers, so assistive tech never hears a counter mid-count.
- Visible focus everywhere. Keyboard focus gets a themed outline, and links in running text are underlined rather than relying on color alone.
View source is part of the portfolio.
The markup is meant to be read. If you'd like to talk through how any of it works, or how I'd bring the same approach to your team, I'd like to hear from you.