# Portfolio Site
`06 · portfolio-site · Open source`
The site you're reading. React · Vite · TypeScript with token-first CSS modules, typed EN/RU/AR content, text mirrors, privacy-first telemetry and route-aware navigation.
**Scope:** Solo · ongoing
**Role:** Engineering portfolio · 2025–2026
---
## Context
> Six cases the recruiter cannot click into. One site they can.
Of the six showcased projects, five sit behind closed/NDA scope — client work, internal tooling, R&D. Recruiters cannot open them, cannot pull the repo, cannot diff the commit history. The site itself is the only public artifact a senior reviewer can audit on the spot: open DevTools, watch the network, read the CSS, view-source the dot-grid. Every detail visible to the user is also a decision visible to the engineer.
The constraint is dual. The page must scan for a recruiter in five seconds — fixed-shape cards, mono spec-sheets, ASCII diagrams that read like terminal output. It must also hold under a senior eye who scrolls slow — token-first design, hand-rolled i18n with compile-time parity, view transitions for the home → case morph, and a Hero ember field that does not cook iPhones. The site has to be its own case study, because nothing else here can be opened.
## Facts
| | |
|---|---|
| **Scope** | Solo · ongoing |
| **Surfaces** | Home + 6 cases · EN/RU/AR · dark/light · 3 palettes |
| **Source** | Public GitHub repository · CV PDF · copy-as-Markdown actions |
| **Stack** | React 18 · Vite · TypeScript · CSS variables |
| **Content** | Typed EN/RU/AR dictionaries · public/private content trees · generated text mirrors |
| **Telemetry** | Cookieless same-origin events · admin-only analytics |
| **Motion** | View transitions (card → case hero FLIP-morph) · prefers-reduced-motion respected |
| **Status** | Open source · live |
## Architecture
### Provider tree + routing
```text
// src/theme/ThemeContext.tsx
// src/i18n/LangContext.tsx
→
```
**Provider order matters.** ThemeProvider outermost so the palette is set on `` before any child reads color variables. LangProvider next so `useT()` is available everywhere. Loader, scroll restoration and analytics all sit inside the router because they need routed content or `useLocation()`.
**Two functional routes + 404 fallback.** `/` renders the home page; `/cases/:slug` renders the case-study page; `*` falls back to home for unknown URLs. Slug whitelist (`CASE_SLUGS = [ai-crm, roblox-game, ai-video-editor, ai-warehouse, macos-vpn, portfolio-site]`) lives in `src/config/cases.ts` — `Nav.tsx` and `ProjectDetailPage.tsx` consume it; server analytics keeps its own allowlists.
**No code-splitting yet.** Both routes ship in one bundle. Lighthouse stays green; revisit when a third route lands.
### Token resolution chain
```text
1 ThemeContext theme = 'dark' → palette = 'ochre'
│
▼ writes data-attrs on
2
│
▼ tokens.css matches via attribute selectors
3 [data-theme="dark"] → --bg --fg --line
[data-palette="ochre"][data-theme="dark"] → --accent --accent-soft
│
▼ CSS modules imported by styles.css consume via var()
4 .hero h1 { color: var(--fg) }
.chip { background: color-mix(in oklab, var(--bg) 10%, …) }
```
**Theme determines palette.** Derived deterministically — `dark → ochre` (warm gold accent), `light → electric` (cool blue accent). Both `data-theme` and `data-palette` written to ``. CSS handles the swap via attribute selectors; zero JS branching per element.
**Token file + CSS modules, no Tailwind.** `tokens.css` (153 LOC) is the single source of truth for colors / spacing / type / radii. `styles.css` is now a tiny entry file importing `base`, `home`, `media`, `case-study` and `runtime` modules. The token system does what Tailwind config would; component selectors do what utility classes would.
**oklch + color-mix for backdrops.** Heavy use of `color-mix(in oklab, var(--bg) X%, transparent)` for tinted overlays — the frosted Hero chips, the case-study glows, the diagram backdrops. Avoids opacity tricks on the wrong color.
### Loader + interface entrance phases
```text
T=0 T=400 ms T=900 ms
│ │ │
▼ ▼ ▼
pulse → rush → gone
hairline two whiteish blurred loader unmounted
opacity pulses run from 15% / 85% hero/about reveal
0.22-0.7 toward center (600 ms) (staggered)
at T+400 ms of rush:
html.is-loading removed (was set synchronously by inline