The portfolio shipped in May with two things bothering me. The chat panel disappeared when you clicked it. And the whole site looked like every other AI-generated landing page — purple gradients on dark mesh, glass blur cards, the lot. I spent a weekend fixing both, and the two problems turned out to be tangled in the same root cause: I had stopped paying attention to my own defaults.

Why I tossed the glass

The original design leaned on a .glass utility that was a color-mix of foreground at 5% opacity. On a dark background that produced washed-out cards. On light it produced invisible cards. The mesh gradient behind it all was purple-and-cyan in the exact register every AI design tool ships by default. The headline used a font I never explicitly chose. The aesthetic was the absence of a decision.

I rebuilt it around three constraints: serif display, hairline rules, single accent. Fraunces for headlines, Geist for body, JetBrains Mono for numerals. One ember-orange punctuation accent. No blur, no shadow. The visual identity is now a posture — editorial, deliberate, dated by section number and volume — rather than a special effect.

Design is one. The discipline does not change.

— Massimo Vignelli

Switching chat to free

The chat had been running on OpenRouter, eating tokens on free-tier models that kept getting rate-limited. Cloudflare Workers AI gives you ~10,000 neurons a day on the free plan. For a personal site that’s effectively unlimited.

The integration was nine lines plus a wrangler binding:

import { createWorkersAI } from 'workers-ai-provider'

const workersAI = createWorkersAI({ binding: env.AI })

const result = streamText({
  model: workersAI('@cf/meta/llama-3.1-8b-instruct'),
  system,
  messages: modelMessages,
  maxOutputTokens: 512,
})
return result.toUIMessageStreamResponse()

The bug that made the chat panel “go gone” turned out to be unrelated to either provider. Two separate client:idle islands were each instantiating their own Zustand store at module load — clicking the button in island A never propagated to the panel in island B. Plus the bg-background Tailwind class I’d written didn’t exist (no matching --color-background in the @theme block). Two bugs hiding behind each other.

The fix was one merged ChatWidget island. Two islands collapsed to one, and the store became guaranteed shared.

What I’d change next

Honestly, the redesign was overdue. I’d been polishing copy on a layout that didn’t deserve it. The lesson worth keeping: when a project feels stuck on “details,” check whether the foundation is what’s actually stuck.

Three things I’d do differently:

The Workers AI swap was a much smaller win in scope but a larger one in cost: the chat now runs on free quota indefinitely. If a visitor hits the daily ceiling, the request 429s and the next day it’s clean again. Good enough for a portfolio. Not the kind of decision I’d make for a paying product, but the kind I’ll keep making for everything else.