Atlas, built in a single Claude Code session.
A field guide to YC's AI generation, designed and shipped in dialogue with Claude Code. Every screen drawn in code, every easing curve felt. Below is a curated summary of how it came together, in the order it happened. Linked SHAs are the actual commits.
The work · chronological
- 01
Brief
Single-page AI-native field guide to YC's AI generation. Apple × OpenAI aesthetic, off-white #FAFAF7 / charcoal #0A0A0A, one accent (YC orange #FF6600, used ≤7 times). Newsreader serif + Inter Tight body + JetBrains Mono numerals scoped to /yc only. Spring physics 200-400 ms. Reduced-motion respected. Cloudflare-only deploy.
- 02
Data pipeline
Three scripts. atlas:fetch pulls 5,925 YC companies from yc-oss, narrows to 2,017 AI rows W21→X26. atlas:enrich classifies into 9 sub-verticals via a Cloudflare Workers AI → Claude Haiku 4.5 cascade with caching. atlas:trends rolls up per-batch + global aggregates and emits trends.json / seeds.json / companies.json. A follow-on atlas:founders scraper enriches with founder names, photos, LinkedIn/X links (99 % coverage, 3,384 photos mirrored locally).
commit · b9f81c68 - 03
Route + design system
Atlas fonts isolated in lib/atlas-fonts.ts (don't share with site fonts, would leak the chunk into every other route). [data-atlas] CSS-var scope for tokens. Dark mode flips bg/fg; accent + brand colors are theme-independent. Build-time OG image via app/yc/opengraph-image.tsx + next/og ImageResponse (1200×630 PNG, ~51 KB). JSON-LD WebApplication + Dataset schema for GEO.
- 04
Hero with constellation
2,017 dots positioned via Halton low-discrepancy sequence, so the same company lands on the same dot across deploys. Sinusoidal drift + spring magnetism within 120 px of the cursor on desktop; static compact path on mobile for perf. prefers-reduced-motion → single paint, no RAF. Word-by-word headline reveal via CSS keyframes (framer-motion stalled inside Radix Dialog earlier; same fix here, plain CSS).
- 05
Command bar + SSE endpoint
POST /api/atlas-query → text/event-stream. Two Claude Sonnet 4.6 calls: tool_use for forced JSON filter spec, then streaming synthesis with [[slug]] citations. companies.fn.json imported at build time, no runtime fetch. ⌘K hotkey, single YC-orange focus border, suggestions hoisted above the bar. Followed by a five-stage streaming UX in the response panel: parsing → searching → reading N matches → writing → done.
- 06
Grid + filters + URL sync
Lazy-fetched cohort, module-cached. Filter pills as a state-of-industry snapshot: per-pill cohort counts (W25 · 135, LLM apps · 404, Active · 1831). Three filter dimensions + sort. URL sync via window.history.replaceState (no history pollution). Selected card uses YC orange border + soft wash instead of an accent rail.
- 07
Profile + methodology panels
Shared floating-panel widget. Desktop: slides in from the right at 640 px max-width with edge gap matching the command bar's outer padding (28 px). Mobile: slides up as a bottom sheet with a drag handle, sits above the command bar (which floats at z-50). Single butter-smooth cubic-bezier(0.22, 0.61, 0.36, 1) curve in both directions. Profile shows lede → meta → founders row → related companies; methodology is the same widget reused.
commit · 7bad1675 - 08
Infographic block
Atlas at a glance: 4 KPI tiles, companies-per-batch bar chart with motion.g + scaleY (animating SVG y/height attributes directly stalled in framer, so a wrapper element + transform was the fix), Apple Watch ActivityRings-style top-verticals wheel. Each ring + legend dot uses a distinct YC orange shade or white accent. Numbers count up via RAF. Hover any ring or bar → highlight; click → pin and light up matching dots on the constellation backdrop, auto-cleared via IntersectionObserver when the section scrolls out of view.
commit · d262eed1 - 09
Top header + slide-deck scroll
Sticky glass header with Charts · Filters · About nav, appearing past scrollY 480 px. Each nav button auto-scrolls to its anchored section. scroll-snap-type: y mandatory + atlas-slide class on hero + charts so the page reads as discrete full-screen slides. Hero parallax tied to its own scrollYProgress (not window scrollY) so the headline stays full-opacity until the section is actively leaving. Section-relative is the only way parallax + snap coexist cleanly.
commit · 83df6a49 - 10
Founders pipeline
yc-oss doesn't ship founder data. Built a scraper for the ycombinator.com per-company Inertia data-page JSON: extracts full_name + title + avatar_thumb_url (presigned S3, 1-hour TTL → mirror locally so deploy survives expiry) + LinkedIn + Twitter. Throttled 1 req/s out of respect; SIGINT-safe checkpoint every 25 companies. First pass got 29 % (run hit a timeout); second pass to 99 %. 3,384 photos in public/atlas/founders/, ~55 MB.
commit · f0609436 - 11
Bug hunt + production
Three parallel Explore agents audited responsiveness, parallax integrity, interaction states. Punch list returned: header pointer-events race (per-frame useTransform string could turn a button non-clickable mid-click; fix: stable React boolean state from a passive scroll listener); scrollIntoView to #atlas-charts failing in query mode (section unmounted; fix: flip browse mode first, then rAF the scroll); grid card aria-pressed always-boolean for screen readers. Then promoted to production: removed robots:noindex, added googleBot directives, verified the apex domain serves the build.
commit · f6f62339
What I learned about working with Claude Code
- Vibe coding is real, and it's specifically a craft skill, not a shortcut. You're still naming spacing scales, picking easing curves, weighing a 0.78 spring damping against a 0.85. The tools just compress the loop between intent and shipped pixel. After the third pass I noticed I'd stopped describing components in prose and started giving Claude the exact CSS-var token names and the framer transition props I wanted. Vocabulary mattered more than instruction count.
- Two animation regressions came from the same root cause: framer-motion stalling when asked to interpolate SVG attribute values (the bar chart, the panel slide). Both fixed by wrapping with a <g> or a CSS-keyframed parent and animating the transform on the wrapper. After the second one I knew to reach for it instinctively. Pattern recognition compounded. I started writing the prompt as if I'd already debugged it once.
- Tried scroll-snap twice. First attempt used `proximity` + `scroll-snap-stop: always`. Those two settings conflict (proximity allows in-between positions; stop:always forbids them). Removing snap entirely was the wrong overreaction; the cleaner pairing is `mandatory` alone. Naming the slide opt-out class `atlas-slide` made the intent obvious in the JSX.
- Parallax + snap can coexist, but only if the parallax is section-relative (`useScroll({ target: ref })`). Window-scrollY parallax interpolates from 0→400 px while the snap is jumping from 0→100vh; the math doesn't compose. Once both read from the same section's scroll progress, the parallax animates DURING the snap transition.
- The constellation drift was set to 1 px amplitude, invisible after the spring physics damped it. Bumping to 2.8 px gave the field the breathing motion the user noticed was missing. Small numbers compound through physics chains; pixel-perfect is a function of math, not just measurement.