This site
A portfolio built as a product: a static build, a look of its own for every page, an open log of decisions and checks before every commit.
- What it is
- A site in two languages. Home, Flow and Projects are done, Pora na Paru is still a stub. It is plain files with no application server, served by Caddy.
- My role
- Design, front end, motion, infrastructure and copy.
- What is open
- The code, the log of decisions and the plan are on GitHub. Each section links to its files.
- Period
- since 30.10.2026
- Address
- finchaf.com
- Stack
- Next.js, TypeScript, Tailwind, GSAP, Caddy
- 135commits
- 14decisions
- 14checks
- 557tests
- 4themes
- 2languages
On 03.11.2026, branch main and a check.py run
Form
Every page has a form of its own
A portfolio usually describes the work in words. Here a page is built like the work itself: home is a stage that scrolls down and sideways, Flow looks like Obsidian because that case was a base of notes, and Projects looks like an editor because I am a designer.
The price of that form is an engine per page. To keep them from growing together, pages are split by boundaries: a feature does not import another, and what they share moves down to ui/ and lib/. It is an ESLint rule rather than an agreement: an import across a boundary fails the check.




Files and decisions
Themes
One component, four themes
Each page declares a theme, and the theme changes colours, typefaces and the look of controls. There are two modes, so eight combinations. A component knows nothing of themes: it reads tokens, and the values live in the theme's file. A new theme is one file.
A colour outside the contract does not compile: the Tailwind palette is off. A test measures contrast, and that is why a theme has two accents. accent is the exact brand colour for fills, accent-strong is the one for text. White on the product's blue gives 3.7:1, too little for text.
The theme is declared in the markup and read through :has(), so it is in the first HTML and the page does not flash. \Get in touch on home and [[Get in touch]] on Flow are one component, and the capsule changes its skin on the fly when a chapter of another theme arrives on home. The values below are read from the sample at every switch.
A page heading
The same paragraph, the same markup, the same components.
Lens
Design and code on one screen
The first screen is two layers: the design and its blueprint, with sizes and real lines of code. A test finds those lines in the sources, so the blueprint cannot drift from the code.
The layers match to the pixel: every block of the blueprint stands where the real one does, because the block itself lies inside it, hidden, and sets its size. The lens only opens the second layer with clip-path. It reaches the capsule too, though that lies outside the first screen: a script writes it the same variables in its own coordinates.
The first screen never waits for animation. The name is in the HTML and drawn from the first frame, and the assembly only wipes an outline off it. The real controls lie under the code layer and take the clicks, and keyboard focus lifts them above it.




Move the lens over the frame
Motion
A page change along the slash of the logo
Each tool has its zone: CSS does hover and focus, ViewTransition the page change, GSAP the scenes on scroll, canvas the graphs and lines. Durations and curves live in tokens, so an effect's code holds no numbers.
The first page change looked bad. A ViewTransition around the page makes one layer as tall as the page, 34,678 px on home on 03.11.2026, and the browser squeezes it to the screen. Now only the screen is captured, and the new page arrives along a 20.2° line, the lean of the slash in the logo. A step back in the capsule runs the other way.
In Firefox the shape stood at its end for the whole change: it does not interpolate a registered @property variable on these pseudo-elements. So the shapes are written out in polygon keyframes. WebDriver's screenshot does not show this, so that frame comes from a system capture of a real browser window.


Only the screen is captured, and the new page arrives along the slash.
Checks
14 checks, one command
python3 scripts/check.py runs before every commit, and CI runs the same command on a PR. Below is a real run of 03.11.2026: 300 unit tests and 257 browser tests.
The browser checks run behind a real Caddy, the same image digest as the server: addresses, headers, CSP, accessibility in both modes, scenes with reduced motion and without JavaScript. A check must be able to fail: the scaffold was tested with a deliberate import across a boundary and with text in the markup.
python3 scripts/check.py
Docsok0.1 s
Publicok0.2 s
Typesok2.3 s
Lintok13.7 s
Formatok2.2 s
Contentok0.0 s
Unitok1.0 s
Buildok11.3 s
Bundleok0.3 s
Addressesok15.5 s
SEOok9.1 s
A11yok35.8 s
Motionok210.2 s
Themesok36.8 s
14 of 14, 5 min 39 s
limit: 280 KB
What turned up along the way
- 664494f
On the Flow card the graph's simulation ends on different coordinates on arm64 and x64. It was green on the laptop and red in CI. Now the hash counts what the graph is made of, not where it lands.
- da37183
The test config differed from production, so the gate stayed green while the server's 404 would have named itself and lost HSTS. The header block now matches line for line, and the gate looks at the 404's headers.
- 3e2a2bf
Gecko loses a thin diagonal gradient at random, and the hatching fell apart in Firefox. It is now an SVG mask, and a test keeps such gradients out of the stylesheets.
- bbdeab3
Shadows took the colour of the text, which on a dark page is a glow. In the dark mode a shadow is now transparent.
Files and decisions
Decisions
The decision first, the code next
Whatever is expensive to undo is written down before the code: the context, the options, the choice and when to revisit it. An accepted record is never rewritten, a different decision is a new record.
Static was not chosen to save money: the server had room. It is faster, more reliable and safer for the neighbour on that server, and the animation lives in the browser anyway. The repository has been open since the first commit, because what enters a history stays in old commits. The code is MIT, the texts and images are reserved, and GSAP stays a dependency that is never copied in.
- 0001A knowledge base in docs/
- 0002A static build, served by Caddy
- 0003One app, no monorepo
- 0004CSS, Motion, GSAP and WebGL, each with its zone
- 0005An atomic release by switching a link
- 0006The domain, a guard .ru and mail
- 0007Gates from one command
- 0008The repository is open
- 0009A page is a feature, with import boundaries
- 0010A theme per page, two modes, one contract
- 0011Control skins are theme tokens
- 0012Link previews from the site's tokens
- 0013Flow as a vault of notes
- 0014Projects as an editor
Files and decisions
Release
A release is one switch
The site is a folder of files on a shared server, beside another product. A release touches neither Caddy nor the containers: a new build lands in a folder of its own, then one operation switches the current link. There is no downtime.
The last three releases are kept, and --rollback returns the previous one in a second. GitHub holds no key to the server: CI only checks, and a release runs from the laptop. That is a price paid on purpose: there is one developer, and the site can be rebuilt from any commit.
- Refuses unless on main, with a clean tree, level with GitHub
- Every check and the build
- A copy in a folder of the commit under releases/, equal files as hard links
- An atomic switch of the current link
- A check of the real domain: codes, CSP, redirects
- If it fails, the link returns to the last release
ls portfolio/releases
664494f84cee← current
252bf73f2ffc
1a4994f45e0d
Files and decisions
Stage
What is not done yet
The site is a work in progress, and the plan in the repository shows it.
- The Pora na Paru page is still a stub.
- The story's photographs are over budget: 2.12 MB in all against a limit of 1.5 MB, measured 01.11.2026.
- The page change and the Flow chapter have not been tested on a real phone or in real Safari yet.
Files and decisions