Case study
Eleven Barberstudio
A design system and a three-language website for a barbershop in Burgas, built so the whole site ships as one 8.5kB script and a visitor can book a chair without picking up the phone.
- Client
- Eleven Barberstudio, Burgas
- Role
- Design system, website
- Scope
- Tokens, motion, booking, performance
- Year
- 2026

01The shop
A barber, four services, no website
Nikola Atanasov has been cutting since 2022. Fades, classic cuts, beard shaping, in ж.к. Изгрев in Burgas. Four services, 10 to 25 euro, cash only, because there is no card terminal and pretending otherwise would waste somebody a trip.
The shop had a logo and an Instagram account. It did not have a website, and every booking arrived as a phone call in the middle of a haircut.
02The brief
One job, stated plainly
Somebody in Burgas finds the shop on a phone and books a chair without calling. Everything else on the page is in service of that.
Two things followed. The site had to load on a phone on a bad connection in a seaside town in August, so weight was a constraint from the first commit rather than an audit at the end. And it had to run in Bulgarian, English and Russian, because the customers here do.
03The system
The brief came before the CSS
We wrote the design system as a document before it existed as a stylesheet. Colours, type scale, spacing, radii, all of it, worked up from a Cron Calendar style reference into a brief the build then followed literally.
That sounds like process theatre until you read the first six lines of the stylesheet, which say the tokens are taken verbatim from the brief. Nothing in there is a value somebody eyeballed at two in the morning.
The wordmark was the only thing that already existed. The dark canvas, the ember accent and the whole token set are ours.
Color
Seven colors, one of them chromatic
Bone, and the two derived values
Nothing else gets in
Headings are bone, plain #ffffff, the seventh colour. Below that there are exactly two derived values: a hairline at rgba(204,204,204,0.16) and a scrim of the canvas at 0.92. Both are neutrals at low alpha, so no new hue can enter the system through the back door.
Ember is reserved. It paints the booking button and the mark, and nothing else on the site is allowed to be orange. One accent used once is what makes a single button read as the thing to press.
Typography
Inter, and a display scale that fights back
Inter
Everything
400, 500, 700
Latin and Cyrillic out of one family, because the site runs in three languages and the headline has to set in all of them.
ТВОЯТ БРЪСНАР В БУРГАС
ABCDEFGHIJKLM NOPQRSTUVWXYZ abcdefghijklm nopqrstuvwxyz 0123456789
Type
The 140px headline, and what actually ships
The brief asks for 140px. What ships is clamp(44px, min(9.4vw, 13.5vh), 118px), and the vh term is the whole point. Sized off width alone the headline grows to the full height of a 16:9 laptop and walks into the header, which is how it shipped once. Test at 1920x841, not just 1440x900.
Section headings sit on a separate, smaller clamp, because the brief says the display scale belongs to the hero and nothing else.
Two smaller decisions keep paying. Headings are stored as arrays of lines rather than strings, and rendered with a break between them, because where a display line breaks is a design decision and the browser makes a worse one. It is also why no heading carries a ch width cap: a cap on top of explicit breaks re-broke the Bulgarian hero into three lines. And headings are stored in sentence case and set uppercase in CSS, because capitals in the source get spelled out letter by letter by some screen readers.
Design system
The parts, live
Rendered here in the client’s own colours, in the browser, not photographed. Cards at 0px, buttons at 4px, pills at 9999px, everything on a 4px unit, page capped at 1200px.
The booking dock
Ghost link, section nav
Price row
4+ години опит
The one eyebrow on the site, and it counts itself.
Caption, 13px, tracked
1px dots, 22px apart, at hairline alpha. It reads as grain on the surface rather than as pattern, which is the only reason it earns a place in the system.
Dot field, the one texture
Graphite on obsidian
Cards keep square corners. Only buttons round.
Surface and hairline
The opening reveal
Looking through the wordmark
The site scrolled top to bottom on a phone.
The mark is the aperture
The letterforms are an SVG mask over a photograph of the shop. Nothing is layered on top of anything. You are looking through the wordmark.
Zoom alone goes black
The centre of the wordmark is the gap between two letters, so past a certain scale the frame is looking through empty space. A rect fades up under the mask while the last letterform is still sweeping across, and hands the frame over.
It resolves, it does not just end
The scale curve is pow(q, 2.2) rather than linear. That holds the wordmark legible through the first third of the track, then accelerates hard, so the trip reads as a reveal instead of a zoom.
It lands as the nav
The flight to the header is measured, not guessed, and re-measured on resize, on orientation change, and on fonts.ready. The header mark is held at zero opacity and crossfades in over the last 6% of the flight, so there is never a second wordmark on screen.
Motion
The animation chases the scroll, it is not bound to it
Three tall tracks with sticky stages: the opening, the barber slice, the closing film. Scroll progress inside a track maps onto the animation, and everything runs from one damped requestAnimationFrame loop.
The loop moves the animation toward the scroll position instead of onto it, cur += (target - cur) * 0.16. Bound one to one, a flick on a phone snaps the reveal to its end frame and reads like dragging a slider. Chased, the same flick carries weight and settles. That is the entire feel of the page and it is one line.
Motion also finishes before the track does. The progress curve compresses the animation into the first 82% of the track, so the last stretch of scroll is a beat on a finished frame instead of dead travel. It is what stops a long reveal feeling long. The easing is in and out rather than ease out, which front loads the trip: two thirds of the distance goes in the first quarter and the wordmark is off the top of the frame before you have read it.
The barber slice is one photograph, clipped open from the top while it settles out of a slow push in. The version before it shredded the picture into fourteen sliding bands, and because each band carried its own residual offset the seams never landed flush. The photograph read as torn. A single element cannot tear.
Reduced motion
With the motion turned off
prefers-reduced-motion skips the loop entirely and calls rest() on every track, which paints the same end state. Copy inside a track is faded in by the script with inline styles; copy outside a track uses a CSS attribute scoped to a js class, so nothing is trapped invisible when JavaScript does not run.
The menu uses the hidden attribute as its real state, so a shut panel is out of the accessibility tree, and the language list is only hidden once the script runs, so a visit without JavaScript keeps all three languages inline and reachable. The gallery page has no opening track at all, so the script hands the header and the booking dock over immediately. Without that line the page scrolls under a transparent header and never shows the dock.
The site
Section by section, in three languages
Booking
Ten seconds down to 335 milliseconds
Booking is a Cal.com panel that opens over the page, not a new tab. Losing the visitor mid decision is the expensive part of a booking flow, and people mostly do not come back from a stray tab.

The panel opens over the page in the site’s own colours, already warm.
The problem
The panel took about ten seconds to open on a phone. The embed is 22kB and the thing behind it is a whole calendar application.
The fix that did nothing
We warmed the embed on pointerenter and touchstart, which is fine on a desktop and worth nothing on a phone, because touchstart lands a few milliseconds ahead of the click. The visitor pressed the button and then watched an empty panel while the embed, the calendar and a month of availability downloaded from scratch.
What worked
A timing change, not a size one. The embed warms on a MutationObserver watching the booking dock appear. The dock arrives when the opening reveal resolves, which is a scroll and a read away from anyone reaching for it, and that gap is the head start. Anyone who leaves during the opening never pays for any of it. A preconnect to app.cal.com takes DNS, TCP and TLS off the critical path.
Measured
On Fast 3G with 4x CPU throttling, the panel is on screen at 335ms and the calendar is usable at 1.6s, with the embed’s requests already finished before the tap.
Three smaller calls
Cal is handed the site’s own accent, so the panel does not arrive as a white sheet with somebody else’s blue in it. The button stays a real link to the booking page throughout, so if the script never runs, or Cal is blocked, or anything throws, the click is an ordinary navigation and the booking still happens. And there is one booking button on the page, the floating dock, and one only. A second one went into the closing section once and came straight back out.
Performance
Small on purpose, from the first commit
8.5kB
JavaScript, whole site
335ms
to the booking panel on Fast 3G
1.6s
to a usable calendar
3
languages, one content file
0
framework runtime
20kB
gallery tiles, down from 190kB
3 days
brief to live, eleven commits
0
layout shift, mobile and desktop
Four hundreds, twice
| Lighthouse 12, live site | Mobile | Desktop |
|---|---|---|
| Performance | 100 | 100 |
| Accessibility | 100 | 100 |
| Best practices | 100 | 100 |
| SEO | 100 | 100 |
| Largest contentful paint | 3.0s | 0.9s |
| Total blocking time | 0ms | 0ms |
| Cumulative layout shift | 0 | 0 |
One JavaScript file, 8,511 bytes, for the entire site. No framework runtime, no hydration, no client side router. One stylesheet at 22kB. The built Bulgarian home page is 50kB of HTML.
Every image is AVIF, quality 62 on the grids and 82 on the two full bleed photographs, generated at build into 75 variants. The JPG masters stay in the repo. That took the gallery tiles from around 190kB to under 20kB each.
Blocking time is zero and layout shift is zero on both form factors, which is what 8.5kB and fixed aspect ratios buy you. Nothing on the page moves after it paints, and there is no framework waiting to hydrate before it responds.
Performance, accessibility, best practices and SEO all come back at 100, on mobile and on desktop. That is not a tuning pass at the end. It is what is left when the site ships one script, sets an aspect ratio on every image, and renders its text as text.
The other honest number: the closing video is 2.8MB, the single largest thing in a 6.1MB build. It sits below the fold behind an IntersectionObserver, so it costs nothing on first paint. It is still 2.8MB.
The photographs
The grade is in the file, not in a filter
Every photograph on the site is black and white, and not with a CSS filter. The grade is baked into the file before it is committed, so it is the same picture everywhere it is used and there is no property left for anyone to miss.
A small sharp script does it: Rec.709 luma, then a straight levels move at a gain of 1.15625 and a lift of -34.75, which closes blacks to 30 and opens whites to 250. Those numbers were measured back off the first set rather than picked twice, so a new batch lands within half a level of what is already there. It is what keeps a phone snapshot taken in the shop from arriving as a bright rectangle in the middle of the grid.
Five photographs on the landing page as a selection. The whole set lives on the gallery page. They are Nikola’s work, not stock.
Three languages
One file holds every fact about the shop
A single content module, 624 lines, is the only place anything factual is written down: all three languages, the address, the phone, the hours, the prices, the external links. The pages, the structured data, the llms.txt and the footer all read from it, so a price changes once.
The eyebrow above Nikola’s name counts up from 2022 at build time instead of storing “four years”, so it stays right without anyone remembering to edit it in January. With the correct plural per language, including the Russian one that switches at one, again from two to four, and back through the teens.
The phone button is a tel: link and it is hidden from 900px up. A desktop has nowhere to send it, and the number is written out in full in the contact section anyway.
Search
Six URLs, and every one of them points somewhere true
Six indexable URLs: the landing page and the gallery page, each in three languages. Canonical and hreflang are both built off one path value, so the three gallery pages point at each other rather than at three copies of the home page, and every sub page URL keeps its trailing slash, because the host redirects the other form and a canonical aimed at a redirect is a self inflicted wound.
The business JSON-LD always points its id and url at that language’s home page, so a sub page adds to one entity instead of announcing a second barbershop. The FAQ block is opt in and only the three landing pages pass it, because a FAQPage block on a page with no questions on it is exactly what a manual review looks for. The Google Business Profile is tied in by knowledge graph id, which survives the listing being renamed or re-verified. Share links expire, so they are not used.
Under the hood
Astro 5, static, three days
Astro 5, static output, deployed on Vercel from main. The dependency list is astro, the sitemap integration and sharp. There is no framework runtime on the page because there is no framework on the page. Bulgarian sits at the root, English and Russian under /en/ and /ru/, on Astro’s own routing rather than a middleware layer.
Caching is a year and immutable on the hashed assets, a day on the video, the share image and the logo, with nosniff and a referrer policy site wide. The same headers are written a second time in the format Netlify and Cloudflare Pages read, so the site can change hosts without quietly losing them.
Eleven commits, three days, 28 to 30 August 2026. Checked on every pass, in all three languages: exactly one h1, the heading line counts, the booking button count, no horizontal overflow from 360px through 1920px, console errors, and both JSON-LD blocks.
In the end
A barbershop that books itself.
One accent on near black, a wordmark that opens the page and then becomes the navigation, and a booking panel that is already warm by the time anyone reaches for it. The shop stopped taking bookings mid haircut, which was the whole reason to build it.







