Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/content/staticPages/uses.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ date: "2026-08-26"

A snapshot of what I build with. Inspired by [uses.tech](https://uses.tech). The bias throughout is toward things that work offline, keep data local, and can be rebuilt from a file in a repository rather than a click in a dashboard.

The list is deliberately broad. I would rather know four languages and the infrastructure under them than one language very well, and the same goes for tools: I keep several in rotation instead of settling on one, because a stack you cannot leave is a stack that eventually picks your problems for you.

## Hardware

- **Apple Silicon Mac**: main development machine, and where I run local LLMs.
Expand All @@ -22,7 +24,7 @@ I live in the terminal, and most of the tooling is a replacement for something o
- **direnv** for per-project environment, with secrets resolved from the macOS Keychain rather than sitting in a `.envrc`.
- **chezmoi** for dotfiles, with **age** encrypting the parts that need it.
- **fnm** for Node versions, a single `.node-version` keeping local and CI in sync.
- **Claude Code** for day-to-day development.
- **AI coding harnesses**, more than one on purpose: **Claude Code** day to day, alongside **OpenCode**, **Codex** and whatever else is worth a look. They have genuinely different strengths, the field moves fast enough that committing to one is a bad bet, and running several is the only way to tell which is actually better at a given job rather than which one you are used to.

## Web stack

Expand Down
15 changes: 9 additions & 6 deletions src/pages/education/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,24 @@ const education = await Promise.all(
<Container>
<div class="space-y-10">
<h1 class="animate font-semibold text-black dark:text-white">
🎓 Education
Education
</h1>
<ul class="flex flex-col space-y-8">
{/* Same treatment as /work: education entries share `entry-body`, so
leaving them on the old rhythm would have made the two pages diverge
for no reason. */}
<ul class="flex flex-col divide-y divide-black/10 dark:divide-white/10">
{
education.map((entry) => (
<li class="animate">
<div class="text-sm opacity-75">
<li class="animate py-9 first:pt-0 last:pb-0">
<div class="text-xs uppercase tracking-wider opacity-60">
{dateRange(entry.data.dateStart, entry.data.dateEnd)}
</div>
<div class="font-semibold text-black dark:text-white">
<div class="mt-1.5 font-semibold text-black dark:text-white">
{entry.data.place}
</div>
<div class="text-sm opacity-75">{entry.data.role}</div>
{entry.body?.trim() && (
<div class="entry-body mt-2">
<div class="entry-body mt-5">
<entry.Content />
</div>
)}
Expand Down
32 changes: 16 additions & 16 deletions src/pages/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,9 @@ const education = await Promise.all(
<section class="animate space-y-6">
<div class="flex flex-wrap gap-y-2 items-center justify-between">
<h2 class="font-semibold text-xl text-black dark:text-white">
📱 Apps I've built
Apps I've built
</h2>
<Link href="/projects">See all projects</Link>
<Link href="/projects">See all projects</Link>
</div>
<ul class="animate-stagger grid grid-cols-1 sm:grid-cols-2 gap-4">
{
Expand Down Expand Up @@ -105,7 +105,7 @@ const education = await Promise.all(

<section class="animate space-y-4">
<h2 class="font-semibold text-xl text-black dark:text-white">
👋 A little about me
A little about me
</h2>
<Image
class="animate rounded-md"
Expand All @@ -132,7 +132,7 @@ const education = await Promise.all(

<section class="animate space-y-4">
<h2 class="font-semibold text-xl text-black dark:text-white">
🤖 AI consulting
AI consulting
</h2>
<p>
The apps above are proof I can take an idea and ship it end to end. If
Expand All @@ -158,7 +158,7 @@ const education = await Promise.all(
<h2 class="font-semibold text-xl text-black dark:text-white">
Recent projects
</h2>
<Link href="/projects">See all projects</Link>
<Link href="/projects">See all projects</Link>
</div>
<ul class="animate-stagger flex flex-col gap-4">
{
Expand All @@ -176,20 +176,20 @@ const education = await Promise.all(
<h2 class="font-semibold text-xl text-black dark:text-white">
Work Experience
</h2>
<Link href="/work">🏢 See all work</Link>
<Link href="/work">See all work</Link>
</div>
<ul class="animate-stagger flex flex-col space-y-8">
<ul class="animate-stagger flex flex-col divide-y divide-black/10 dark:divide-white/10">
{
work.map((entry) => (
<li>
<div class="text-sm opacity-75">
<li class="py-9 first:pt-0 last:pb-0">
<div class="text-xs uppercase tracking-wider opacity-60">
{dateRange(entry.data.dateStart, entry.data.dateEnd)}
</div>
<div class="font-semibold text-black dark:text-white">
<div class="mt-1.5 font-semibold text-black dark:text-white">
{entry.data.company}
</div>
<div class="text-sm opacity-75">{entry.data.role}</div>
<div class="entry-body mt-2">
<div class="entry-body mt-5">
<entry.Content />
</div>
</li>
Expand All @@ -203,16 +203,16 @@ const education = await Promise.all(
<h2 class="font-semibold text-xl text-black dark:text-white">
Education
</h2>
<Link href="/education">🎓 See all education</Link>
<Link href="/education">See all education</Link>
</div>
<ul class="animate-stagger flex flex-col space-y-4">
<ul class="animate-stagger flex flex-col space-y-5">
{
education.map((entry) => (
<li>
<div class="text-sm opacity-75">
<div class="text-xs uppercase tracking-wider opacity-60">
{dateRange(entry.data.dateStart, entry.data.dateEnd)}
</div>
<div class="font-semibold text-black dark:text-white">
<div class="mt-1.5 font-semibold text-black dark:text-white">
{entry.data.place}
</div>
<div class="text-sm opacity-75">{entry.data.role}</div>
Expand All @@ -224,7 +224,7 @@ const education = await Promise.all(

<section class="animate space-y-4">
<h2 class="font-semibold text-xl text-black dark:text-white">
🤝 Let's connect
Let's connect
</h2>
<p>
Questions about an app, an invite to a beta, or a consulting project?
Expand Down
2 changes: 1 addition & 1 deletion src/pages/projects/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ const hackathons = all.filter((project) => HACKATHON_SLUGS.includes(project.id))
<div class="space-y-10">
<section class="space-y-4">
<h1 class="animate font-semibold text-black dark:text-white">
⭐️ Projects
Projects
</h1>
<ul class="animate flex flex-col gap-4">
{
Expand Down
19 changes: 13 additions & 6 deletions src/pages/work/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,27 @@ const work = await Promise.all(
<Container>
<div class="space-y-10">
<h1 class="animate font-semibold text-black dark:text-white">
🏢 Work
Work
</h1>
<ul class="flex flex-col space-y-8">
{/* A rule between entries, not just a gap. With space alone and every
block inside an entry sharing one rhythm, the boundary between two
jobs looked the same as the boundary between two paragraphs of one
job, so eight years read as a single column of text. */}
<ul class="flex flex-col divide-y divide-black/10 dark:divide-white/10">
{
work.map((entry) => (
<li class="animate">
<div class="text-sm opacity-75">
<li class="animate py-9 first:pt-0 last:pb-0">
{/* Date, company and role are one unit and are set tight, then a
deliberate gap before the body. Previously all three and the
body shared the same spacing, so the entry had no header. */}
<div class="text-xs uppercase tracking-wider opacity-60">
{dateRange(entry.data.dateStart, entry.data.dateEnd)}
</div>
<h2 class="font-semibold text-black dark:text-white">
<h2 class="mt-1.5 font-semibold text-black dark:text-white">
{entry.data.company}
</h2>
<div class="text-sm opacity-75">{entry.data.role}</div>
<div class="entry-body mt-2">
<div class="entry-body mt-5">
<entry.Content />
</div>
</li>
Expand Down
38 changes: 37 additions & 1 deletion src/styles/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -115,16 +115,52 @@ html.dark article.longform {
company and role around it rather than the long-form serif style. */
.entry-body {
@apply text-sm;
/* A measure, not the full container. At the 840px container width these
paragraphs ran to roughly 95 characters, well past the 60-75 the eye
tracks comfortably, which is half of why the section read as a wall. */
@apply max-w-[68ch];
}

.entry-body > * + * {
@apply mt-2;
@apply mt-3;
}

.entry-body p {
@apply leading-relaxed;
}

.entry-body strong {
@apply font-semibold text-black dark:text-white;
}

/* The section labels. In these entries a subheading is written as `**label**`
alone on a line, because a real heading here would collide with the page's
h1/h2 order. Markdown renders that as a paragraph containing nothing but a
bold run, which is exactly what this matches.

Before this, every block in the entry carried the same 8px gap and the same
weight, so the labels did not separate anything: four paragraphs of prose
with four bold words between them read as one undifferentiated wall. The
fix is asymmetric spacing, a lot above and very little below, so each label
binds to the paragraph it introduces instead of floating between two. */
.entry-body p:has(> strong:only-child) {
@apply mt-7 mb-1 text-xs uppercase tracking-[0.14em];
@apply text-black/55 dark:text-white/45;
}

/* Overrides the general `strong` rule above, which would otherwise make the
label the highest-contrast thing in the entry and pull the eye to the
signpost rather than the content. */
.entry-body p:has(> strong:only-child) strong {
@apply font-medium text-black/55 dark:text-white/45;
}

/* The first label in an entry follows the intro paragraph and link, so it
does not need the full separation that the ones between prose blocks do. */
.entry-body > p:has(> strong:only-child):first-child {
@apply mt-0;
}

.entry-body ul {
@apply list-disc pl-5;
}
Expand Down