Build Log
You weren’t supposed to find this page. It is linked from exactly one place: a footer icon that looks like decoration. But since you’re here, this is the full record of how the site was built. Not the marketing version. The engineering version: what was tried, what failed, what survived, and why.
The first version had everything. Dark mode. Animated gradients. A custom cursor. Purple-to-pink hero text. Three different button styles. It looked like every other developer portfolio on the internet, but louder.
So it was scrapped. All of it. And rebuilt around a single question: what would a site look like if every element had to justify its existence?
What follows is the answer, chapter by chapter. Every claim on this page is backed by code that is running right now. The figures are live CSS, not screenshots. If you inspect them, you will find the same classes the rest of the site uses.
Built
Deleted
Rebuilt
Refined
Shipped
Part I · The Story
Chapter 01
The site had four jobs. Show the work. Explain the offer. Prove competence. Get the visitor to a contact form. Everything else was negotiable.
That list sounds obvious. It took three failed versions to write it down. The early drafts confused “impressive” with “effective.” A portfolio does not need to impress other developers. It needs to convince a clinic owner, a startup founder, or a thesis adviser that the person behind it ships working systems.
Once the brief was honest, the design constraints wrote themselves. Fast on a mid-range Android over mobile data. Readable in a bright room. Navigable with one thumb. Every claim verifiable on the same page it is made.
Audience
Clients, collaborators, employers. Not other developers’ Dribbble feeds.
Constraint
Mid-range Android, mobile data, one thumb, bright daylight.
Goal
Work, offer, proof, contact. In that order.
Test
If removing an element changes nothing, it never ships.
Chapter 02
The site went through four distinct versions. Each one taught something the next one needed. None of them were wasted, but three of them were deleted.
v1 · The Everything Kitchen
Dark mode, gradients, custom cursor
Three button styles. Purple-to-pink hero text. A trailing cursor dot. Every animation library that existed. It looked impressive in a screenshot and exhausting to use. The lesson: more is not better. More is only more.
v2 · The Reduction
Light mode, three colors, no cursor
Stripped everything. Dark mode gone. Gradients gone. Cursor gone. Three colors: white, near-black, blue. The site felt calm, maybe too calm. The hero was flat, the cards were flat, everything was flat. The lesson: restraint without personality is emptiness.
v3 · The Personality
Inter, gradient borders, motion
Added back the things that were missing, but only the ones that served a purpose. A display font gave the headings a voice. Gradient borders gave cards a reason to be hovered. Spring-based entrance animations gave scroll a sense of arrival. The lesson: personality is not the enemy of restraint. Bad taste is.
v4 · Current
Accessibility, SEO, the details
Keyboard navigation on every interactive element. ARIA roles on the lightbox, the mobile menu, the project cards. Canonical URLs on every page. A real 404 for slugs that do not exist. A sitemap that includes this page at priority 0.1. The lesson: the last 10% is 90% of the work, and it is where craft lives.
Part II · The System
Chapter 03
The first decision was the hardest: three colors. Not four. Not “three plus accents.” Three.
White for the background. Near-black for text. Blue for everything that matters: links, active states, the logo, the “jbpa” in jbpa.dev. No purple. No indigo. No cyan. If a design needed a fourth color, the design was wrong.
The blue sits at oklch(0.55 0.18 255), a perceptually uniform color space where equal numeric changes produce equal visual changes. Same lightness as the mid-gray, with chroma. It makes mixing color and grayscale predictable, and it makes dark mode a lightness inversion instead of a second palette.
Primary
oklch(0.55 0.18 255)
One blue. Everywhere. No variations.
Foreground
oklch(0.145 0 0)
Near-black. Pure black causes eye strain on light backgrounds.
Background
oklch(1 0 0)
Pure white. Maximizes contrast. Makes the blue pop.
Muted
oklch(0.97 0 0)
Subtle gray for section backgrounds and secondary surfaces.
Muted Foreground
oklch(0.45 0 0)
Mid-gray for secondary text. Readable, not competing.
Border
oklch(0.92 0 0)
Visible enough to define structure. Invisible enough to not distract.
fig. 01 · the entire palette, live
Chapter 04
Inter handles the display work: page titles, section headings, the wordmark. Its geometric construction reads as engineered rather than decorated, and its slightly condensed rhythm keeps long headings from sprawling.
Inter handles everything else: body text, buttons, navigation, forms. Large x-height, open counters, optimized for screens. It disappears, which is exactly what body text should do.
The monospace slot is the system mono stack. It shows up for metadata, figure captions, code, and labels. It creates visual rhythm against the sans-serif body and signals “technical” without a single word of copy.
Inter
--font-display
Titles, headings, wordmark. Geometric. Distinctive.
Inter
--font-sans
Body, UI, buttons. Neutral. Readable. Invisible.
System Mono
font-mono
Labels, metadata, code. Technical rhythm.
Type Scale
Chapter 05
Most sites cram content edge-to-edge on mobile and leave dead space on desktop. The fix was simple: one container, consistent padding, generous section spacing.
Sections breathe at py-14 on mobile and py-28 on desktop. Cards stay compact at p-6. The container caps at 72rem, wide enough for three-column grids, narrow enough to keep reading comfortable.
Section Padding
py-20 md:py-32
56px mobile, 112px desktop.
Container
max-w-6xl px-5 sm:px-6 lg:px-8
72rem max. Responsive horizontal padding.
Card Padding
p-6
20px mobile, 24px desktop.
Grid Gap
gap-4 / gap-6
16px tight, 24px spacious.
Border Radius
--radius: 0.625rem (10px)
sm (6px)
md (8px)
lg (10px)
xl (14px)
2xl (18px)
full
Cards use rounded-xl. Buttons and pills use rounded-full. Each step is roughly 1.4x the previous.
Part III · The Engineering
Chapter 06
The header went through more revisions than any other element on the site. The goal was a frosted-glass bar: content should blur as it slides under the nav. Simple in theory. In practice it fought the browser for a week.
The first attempt put backdrop-filter on the header itself. It worked until horizontal overflow was introduced anywhere on the page. The moment overflow-x: hidden landed on body, the backdrop stopped sampling page content and the glass turned to flat gray. The fix was obscure: overflow-x: clip does not create a backdrop root, so the blur keeps working.
The final version is deliberately boring. The header is opaque, the same color as the page background, with a hairline bottom border. Content can never show through because there is nothing to show through. The blur exists only as progressive enhancement where it costs nothing.
header · bg-background/90 + backdrop-blur-xl
.header-blur · sibling layer, masked fade
page content · scrolls beneath both
fig. 02 · the header layer stack, top to bottom
/* the fix that took a week to find */
html, body {
overflow-x: clip; /* clip does not create a backdrop root */
}Chapter 07
Toggling light and dark mode does not swap a class and repaint. It takes a snapshot of the page, expands a circle from the theme button, and resolves a motion blur as the new theme covers the screen. The whole thing runs on the View Transitions API, so the browser does the compositing.
Two details matter. The origin is the button’s center, not the click point, so the ripple always starts from the same place. And the blur is not decoration: it hides the mid-transition state where half the page is light and half is dark, which looks broken at any speed.
circle(r at button.x, button.y)
fig. 03 · the clip-path ripple expanding from the toggle
document.documentElement.animate(
{
clipPath: [
"circle(0px at x y)",
"circle(radius at x y)",
],
filter: ["blur(10px)", "blur(0px)"],
},
{
duration: 650,
easing: "cubic-bezier(0.22, 1, 0.36, 1)",
pseudoElement: "::view-transition-new(root)",
}
);Chapter 08
On a phone, this site behaves like a native app. A bottom tab bar replaces the header nav. Secondary pages live in a sheet that slides up instead of a menu that drops down. The footer does not exist on mobile. These are not responsive adjustments. They are a different interface for a different device.
The tab bar's active indicator was the fiddly part. The first version put a shared layoutId pill inside each tab, which meant the indicator unmounted and remounted on every change, animating vertically from wherever the old tab was. The fix was one nav-level indicator that only ever moves horizontally, positioned by measuring the active tab's offset.
fig. 04 · one indicator, horizontal travel only
Chapter 09
Flat white felt sterile. The fix was texture, but texture is usually expensive: big PNG overlays, live SVG filters, canvas noise. All of them cost paint time on every frame.
The site’s grain is a 256px SVG turbulence tile, pre-rendered once and repeated at 5% opacity. The hero adds two more layers: an ordered-dither gradient that rises from the bottom like newsprint, and a giant outlined wordmark that bleeds off the right edge. All three are pure CSS. None of them animate. Total cost: a few kilobytes and zero JavaScript.
noise tile · 5%
dither mask · 4px
wordmark · stroke only
fig. 05 · the three texture layers, isolated
/* the dither: a 4px dot tile intersected with a fade */
mask-image:
url("data:image/svg+xml,...4px dots..."),
linear-gradient(to bottom, transparent 20%, black);
mask-size: 4px 4px, 100% 100%;
mask-composite: intersect;Chapter 10
Every sentence on this site, the copy, the case studies, the pricing, the navigation labels, lives in one content file. Components contain zero hardcoded strings. When the words change, the whole site follows.
The notes section works the same way. Fifty-six articles in a single data file, statically generated at build time, paginated nine per page, each with its own metadata, OpenGraph image, and canonical URL. Unknown slugs return a real 404, not a 200 with a sad message.
Centralizing copy early makes a sitewide rewrite possible from one file, one diff, every page updated.
Words
Every sentence, one source
Components
Zero hardcoded strings
Pages
Compose, never repeat
fig. content flows one direction: words, components, pages
Chapter 11
Every page is statically generated. There is no server round-trip for content, no loading spinner for data that could have been baked at build time. Images sit behind skeleton preloaders that crossfade on load, so nothing pops in unstyled.
The SEO layer is mechanical and complete: canonical URLs on every page, unique titles and descriptions, OpenGraph and Twitter cards, JSON-LD for the person schema, a sitemap covering all 56 articles, and a robots file that points to it. The QA pass that produced this list also fixed a soft 404, missing h1 elements on six pages, and a search bar that floated where it should not have.
Rendering
SSG · generateStaticParams
Every route is HTML at build time.
Images
next/image + skeleton
Pulsing placeholder, 500ms crossfade, always.
Metadata
per-page + OG + JSON-LD
Unique titles, canonicals, article schema.
Errors
dynamicParams = false
Bad slugs return real 404s, not soft ones.
Chapter 12
The hero was clean but static. A headline, a grid, some texture. It needed motion that felt alive without stealing attention from the words.
The answer is a field of floating fragments: miniature pieces of real work. A terminal mid-deploy, a database table, a code snippet, a SQL query, a build log, an uptime chart, a git log, a cron entry, a passing test run, and more. Twenty-two sit in a pool; six are dealt to strategic edge slots at random on every page load, each with its own drift timing. The randomization runs in useEffect, after mount, so server and client markup never disagree.
Then physics. Each icon gets a random speed between 0.08 and 0.42, wired to page scroll through useScroll and useTransform. Every icon drifts against the scroll direction at its own pace, and a useSpring (stiffness 55, damping 18) lets them settle softly when scrolling stops instead of freezing mid-drift.
Two guards keep it tasteful. A radial mask fades icons out where the headline and description sit, so they never compete with the text. And the whole layer is aria-hidden with pointer-events-none: pure atmosphere.
The headline itself got a sheen: a soft gradient band that sweeps across “digital systems.” once every nine seconds, then rests. Low contrast, wide feather, eased. It reads as light catching the type, not a streak.
Randomness
6 of 22 work fragments dealt to 13 edge slots at random per load. No two visits identical.
Parallax
Per-icon speed from 0.08 to 0.42, all drifting against the scroll, spring-settled.
Restraint
Radial mask near the text, 20-30% opacity, reduced-motion safe.
Sheen
One soft sweep every 9s on the second headline line. Then it rests.
Chapter 13
The last stretch was not new features. It was removing friction that only shows up when you watch the site load.
The notes index heading used to animate twice: once in the loading fallback, again when the real page mounted. The fix is an invisible copy of the heading in the loading state. It reserves the exact space, so there is no layout shift, but it never animates. The entrance plays once, when it matters.
The contact form stopped interrogating people. Six fields became three required ones. Inquiry type is a row of tap-to-pick chips. Budget and timeline are optional and say so. And because a blank box is the hardest part of any form, picking a topic offers a “Use a draft” link: two starting messages per topic, inserted only on an explicit click. Help offered, never imposed.
The about stats got a count-up: numbers climb from zero when scrolled into view, rendered large in the gradient text style. Below them, a stack row of grayscale tool icons that color on hover. Proof, then playfulness.
One removal mattered more than any addition: the phone number is gone from the entire codebase. Not hidden. Removed. A wa.me link exposes the number in its URL, so the channel went with it. Email and LinkedIn carry the direct-contact weight now.
Loading
Invisible heading placeholders: zero layout shift, zero double animation.
Contact
Three required fields, chip picker, opt-in draft messages.
About
Count-up stats, grayscale stack icons that color on hover.
Privacy
The phone number was deleted from source, not just hidden.
Part IV · The Principles
Chapter 14
These were not planned. They showed up after enough things were removed.
1. Three colors only
Light, dark gray, blue. If a design needs a fourth color, the design is wrong, not the constraint.
2. Subtle over flashy
Gradient borders that only appear on hover. Glow shadows at 8% opacity. The site should feel engineered, not decorated.
3. Content is the interface
No stock photos. No decorative illustrations. Typography and spacing carry the design.
4. Mobile is not an afterthought
A bottom tab bar, sheets instead of menus, safe-area spacing. A different interface, not a shrunken one.
5. Motion with purpose
Animations reveal content, provide feedback, or guide attention. All respect reduced-motion.
6. Boring where it counts
Opaque headers, static HTML, system fonts where they suffice. Save the cleverness for the parts users feel.
Appendix
Next.js with the App Router. React. Tailwind CSS with OKLCH tokens. Motion for spring-based animation. Lucide for icons. Self-hosted fonts. No UI kit; every component is hand-built.
Framework
Next.js · React
App Router, static generation, server components where possible.
Styling
Tailwind CSS
Utility-first with OKLCH tokens and a small layer of custom CSS.
Animation
Motion
Spring physics, layout animations, AnimatePresence for exits.
Fonts
Inter · Inter
Self-hosted via next/font. Display swap for fast LCP.
Pages
Composition only
Sections
Hero, work, about, contact
Primitives
Containers, skeletons, motion
Content
Every word, one source
fig. the architecture, top to bottom. Internal paths intentionally omitted.
You found the easter egg.
This is how jbpa.dev is built.
Every pixel, every color, every animation, deliberate. If you read this far, you care about craft. So does the person who built it.
Next step
Tell me what you're building. I'll get back to you with a free initial assessment.
Start a project