TanosoLeather Works Bespoke

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 #8C4A23 and dark chocolate #2C1A10 carry the hide; stitch cream #EFE3CE is the thread; brass #B08D3F is 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 scrollWidth matches 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 ?success handling 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.