Build Guide — How this site was made
Sewing a workshop into pixels
Tanoso Leather Works is the collection's texture study: convincing leather grain, an embossed wordmark, and product cards whose stitching sews itself — all in static HTML, CSS and vanilla JS, no images anywhere. This page explains the techniques generously enough to rebuild them.
Concept & creative direction
The brief was texture as proof: a heritage leather studio has to feel tactile on a screen that has none. Every decision serves that one problem.
- Workbench layout. A tool-rail nav standing in for pegs on a wall, sections separated by a "burnished edge" shadow line, product cards styled as swatches rather than a catalogue grid.
- Four leathers of colour. Cognac
#8C4A23and dark chocolate#2C1A10carry the hide; stitch cream#EFE3CEis the thread; brass#B08D3Fis the hardware. Nothing else appears. - Two voices. Zilla Slab — a tools-and-leather slab serif — speaks for the house and its products; Work Sans, wide-tracked in small caps, is the workshop's label-maker voice for captions and UI.
The signature commitment: no photograph of leather would ever look as convincing as leather that is computed — grain built from layered noise, an emboss lit by a simulated light, stitches that are drawn, not stock-photographed.
Toolchain
Designed and engineered by Fable 5 (Anthropic's model) as designer-engineer, directed by Hannah Kwakye — art direction, taste and final cut are hers. The stack stays deliberately small:
- Hand-authored static HTML, CSS and JavaScript. No framework, no build step, no bundler.
- Zero libraries. The leather grain and the emboss are native SVG filters; the stitching and tool-icon draw-ins are IntersectionObserver plus
stroke-dashoffset— about 140 lines of vanilla JS. - Three self-hosted static fonts (Zilla Slab regular and bold, Work Sans variable) as woff2, preloaded,
font-display: swap. No runtime request leaves the origin. - Every product — satchel, belt, sandals, tote, wallet, watch strap — is a hand-plotted SVG line drawing on a shared 200×160 stage, so the whole goods wall ships inside the HTML document itself.
Leather grain, in three SVG filter primitives
Real leather reads as texture at two scales at once: fine pore-grain and the coarser, wandering undulation of the hide itself. One feTurbulence can't do both convincingly, so the grain filter layers two:
<filter id="leatherGrain">
<feTurbulence type="fractalNoise" baseFrequency="0.85"
numOctaves="2" seed="4" result="fine"/>
<feTurbulence type="fractalNoise" baseFrequency="0.03"
numOctaves="4" seed="11" result="coarse"/>
<feColorMatrix in="fine" ... result="fineA"/>
<feColorMatrix in="coarse" ... result="coarseA"/>
<feComposite in="fineA" in2="coarseA" operator="arithmetic"
k2="1" k3="1" result="grainA"/>
<feColorMatrix in="grainA" ... result="grainTint"/>
<feBlend in="SourceGraphic" in2="grainTint" mode="multiply"/>
</filter>
The two feColorMatrix steps convert each turbulence field into a translucent alpha mask at a different strength, feComposite sums them, a final feColorMatrix tints the result a warm near-black, and feBlend's multiply mode presses that tint into whatever sits underneath — the hero's radial gradient, the process strip's cognac panel, the care section's chocolate ground. One filter, referenced by filter: url(#leatherGrain), textures every dark surface on the site identically, the way one tannery's hide texture would.
The embossed wordmark
"TANOSO" in the hero is not a styled heading — it's an SVG <text> run through a lighting filter that treats the letterforms as a raised surface on the hide:
<filter id="emboss">
<feTurbulence type="fractalNoise" baseFrequency="0.9"
numOctaves="2" seed="7" result="noise"/>
<feDiffuseLighting in="noise" surfaceScale="2.4"
lighting-color="#F6ECDA" result="light">
<feDistantLight id="embossLight" azimuth="235" elevation="50"/>
</feDiffuseLighting>
<feComposite in="light" in2="SourceAlpha" operator="in" result="litText"/>
<feBlend in="SourceGraphic" in2="litText" mode="soft-light"/>
</filter>
feDiffuseLighting treats the turbulence field as a bump map and a feDistantLight as a light source, producing highlights and shadows exactly where the noise rises and falls. Clipping that lit field to the text's own alpha (feComposite operator="in") means only the letterforms catch the texture; soft-light-blending it back over the flat cognac fill keeps the type legible while giving every stroke a leather-grain sheen, as if the word were tooled into the hide rather than printed on it.
A small rAF loop drifts the light's azimuth and elevation on a slow sine while the hero is on screen, so the emboss catches the light slightly differently over time — a hide turned in a workshop window, not a static texture. It pauses off-screen and never runs under reduced motion, where the filter simply renders one well-lit, static frame.
Stitch marks that sew themselves
Each product swatch carries an SVG overlay of 44 short <line> "stitch marks" placed evenly around its rounded border, generated in JS rather than hand-plotted:
const perimeter = 2 * (W + H);
for (let i = 0; i < 44; i++) {
const d = (i / 44) * perimeter; // distance along the border
// ...walk the four sides, place a short tick centered at d
line.style.setProperty('--i', i % 12);
}
Each mark starts scaled down and transparent; CSS reveals it on hover or focus with a transition-delay keyed to its index, so the seam appears to travel around the card rather than simply fade in — the same "sequence, not simultaneity" trick used for the tool icons below, applied to dozens of tiny stitches instead of a handful of paths. Under prefers-reduced-motion, every mark is forced visible immediately (!important, transitions killed) — the seam is pre-sewn, since a hover-triggered reveal has no reduced-motion equivalent worth keeping.
From hide to handle: tools that draw themselves
The process strip's six tool icons use the classic self-drawing SVG technique — measure each path's real length, then transition its stroke-dashoffset from that length to zero:
svg.querySelectorAll('path').forEach((p, i) => {
const len = Math.ceil(p.getTotalLength()) + 2;
p.style.strokeDasharray = len;
p.style.strokeDashoffset = len;
p.style.transitionDelay = `${i * 140}ms`;
});
An IntersectionObserver watches each station and sets strokeDashoffset = 0 once it's a third visible, so the knife, the awl, the needle and thread draw themselves in as the visitor scrolls past — a single knife stroke, a single needle pass, never more than the tool needs. Under reduced motion the dash properties are never set, so every icon is simply, instantly drawn.
Iteration log
The collection's protocol: three passes, screenshots read like a creative director, findings acted on. What actually changed:
Pass 1 — Design critique
- The Ahenema Sandals swatch read as a vague rounded blob with no strap logic. Redrawn as a proper elongated sole with a crossing X-strap over the forefoot and a heel strap arc — it now reads as a sandal at a glance.
- The care guide's "clean gently" icon had rendered as an accidental eyebrow shape. Replaced with a clear circular buff/polish glyph; "store with shape" became a hanger, matching the copy about hanging belts.
- Section reveal timing needed a longer settle window in review screenshots — real visitors scroll continuously, so transitions complete naturally, but it flagged that a couple of `--d` delay chains ran longer than necessary and were trimmed.
Pass 2 — Elevation
- Added the raking-light animation to the embossed wordmark (§04) — a static emboss looked flat next to the rest of the site's motion; a slow, screen-gated light sweep gave the hero its own signature moment beyond the texture itself.
- Rebuilt the stitch border from a single dashed rectangle (which could only slide, not "grow") into 44 individually-staggered stitch marks, so the seam genuinely appears to sew itself round the card rather than just fade in as a block.
- Tuned the leather-grain filter's two noise layers after side-by-side screenshots — the first pass read as static/TV-snow; slowing the coarse layer's frequency and tinting warmer made it read as hide.
Pass 3 — Ship quality
- Caught a real responsive bug: the workshop and commissions sections used a two-column grid with no narrow-viewport override, so below ~900px the bespoke form ran off the right edge of the screen. Added the missing breakpoints (both collapse to a single column, the workshop illustration reordering above its copy) and verified
scrollWidthmatches viewport width at 390px. - All routes screenshotted at mobile/tablet/desktop with zero console errors; every nav, footer and in-page anchor resolves; the commissions Netlify form posts with honeypot and inline
?successhandling verified. - Reduced-motion pass verified in emulation: stitch marks render pre-sewn, tool icons render fully drawn, the emboss light holds a single frame, and all reveals appear without travel.
- Fonts confirmed self-hosted and loading (three woff2 files, ~103 KB total); every word proofread against the four-colour, two-voice system; contrast checked on cream-on-chocolate and chocolate-on-cream body text.
Deploy
The site is static files on Netlify, deployed CI-style from the collection's repository. netlify.toml publishes the site root, sets immutable year-long caching on /assets/* (fonts and CSS are content-stable), and adds standard hardening headers. The commissions form is Netlify Forms — a plain HTML <form data-netlify="true"> with a honeypot field, no JavaScript required to submit.
This is one of 26 sites in Hannah Kwakye's collection — the shared thesis being that AI doesn't replace taste; it amplifies the designer who has it. See the design process for the thinking, or return to the workshop.