Generative Assets · Industry demo 268 · Upholstery

How Second Sitting was built

Second Sitting Upholstery is a fictional East Victoria Park re-upholstery workshop, built as a single conversion-focused landing page. Its audience is the owner of a good frame in tired fabric; the page's single job is a quote request — and the Recover Studio exists to make that request warmer, by letting the visitor watch their own piece recovered before they ask what it costs.

Palette — argued from the bench

Calico#F6F2EAThe ground. The fitting cloth every piece wears first — the workshop's neutral.
Walnut frame#3E3229Structure and primary ink — the show-wood under the fabric. 11.1:1 on calico.
Velvet teal#1F6E63Line/mark accent — the recover fabric on the bench. Seams, marks, CTA. Never body text.
Brass tack#A9803CSecondary — the nailhead trim. Chips, focus rings, marks. 3.3:1, so never type.
Brass ink#7A5A24Derived text twin of the tack — 5.8:1 on calico for links, eyebrows, labels.

A calico ground with a warm accent sits one step from the banned cream-and-terracotta default, so the line was held deliberately: the accent duty is carried by velvet teal, not terracotta; the display face is a soft plump serif chosen for upholstered warmth, not a high-contrast editorial serif; and brass stays secondary — tacks and focus rings, with a darker derived ink (#7A5A24) doing any actual reading work. Secondary body copy uses a derived walnut-soft #665649 (6.3:1).

Type

Fraunces (display) — soft-shouldered, plump, slightly wonky letterforms that read like upholstered furniture; its italic carries the brand line "every good chair deserves a second sitting". Albert Sans (body, UI) — a warm workhorse that stays out of the way at 16.5px. Fragment Mono — metres, counts and prices; every figure on the page is set in it so the numbers read as measured, not decorated.

The Recover Studio — technique

The signature is a procedural SVG workshop bench (viewBox 920×560, zero raster) on which the chosen piece is drawn in walnut line-work and then re-covered live, panel by panel. Each piece is generated from rounded-rect path geometry (armchair 6 panels, sofas 8–10, dining 1 + one mini-seat per chair in the set, banquette one segment per linear metre). Every panel exists twice: a calico-fitted outline path, and a fabric path filled with a procedural <pattern> per grade — fine cross-hatch weave for house linen, tight 7px grain for performance weave, directional 52px sheen bands for velvet. Wrapping is one scalar W (panels-done + fraction): each panel's fabric sits under a clipPath rect that a rAF loop grows bottom-up, so the cloth pulls over the calico exactly the way a cover goes on. Velvet additionally takes a light sweep — a shared userSpaceOnUse linear gradient whose gradientTransform is translated each frame, clipped to the wrapped panels only. Brass tacks seat along the arm rails after their panel wraps, each with a 220ms overshoot "tap". All animation is JS attribute work — no CSS rule ever animates a property an element also sets as a presentation attribute (the W16/W17 trap).

Dual-speed redraw: the first render earns the entrance — line-work, then W tweens to two-thirds over 2.1s and an ambient drift finishes the wrap slowly, so the 7-second thumbnail catches the armchair mid-recover with the sheen mid-pass (measured wrap 0.84 at settle). Every subsequent chip or stepper tap rebuilds and re-wraps completely in 550ms. Reduced motion renders everything settled and fully wrapped, sweep parked. The loop pauses on document.hidden; state is exposed at window.__upholsterer.

The pricing model — shape and derivation

PieceAnchorPerformance weaveVelvetCount / notes
Armchairfrom $1,250+$180+$320fixed piece — stepper disabled with on-page reason
2-seat sofafrom $1,950+$260+$450fixed piece
3-seat sofafrom $2,450+$260+$450fixed piece
Dining chairs$95/chairgrade chips disabled — commercial range only4–12 chairs, min 4 = $380
Banquette$480/lmrestricted to performance weave / vinyl2–6 lm by 0.5, min 2 lm = $960
New seat foam+$120/cushiontoggle, armchair (×1) and sofas (×2 / ×3) only — foam is part of the rate for dining and banquette

The labour / fabric split

The card splits every fixed-piece figure into labour and fabric lines. The fabric line is a house-grade allowance derived at roughly $45/m across the piece's stated metres — armchair (5–6 m) $250, 2-seat (8–10 m) $400, 3-seat (11–13 m) $540 — and labour is the anchor minus that allowance, so the split always re-adds to the pinned anchor exactly. Grade surcharges land on the fabric line, where they belong.

Deliberately flat dimensions (stated, not invented)

Measured from the running page against an independently re-implemented oracle: rest state armchair + velvet = $1,570; armchair + performance + foam = $1,550; 2-seat linen $1,950, + foam ×2 $2,190; 3-seat velvet $2,900; dining ×6 $570; banquette 3.5 lm $1,680; cheapest possible output $380 = the services grid's dining floor. Every worked example in the CONCEPTS spec matched the implemented model with no correction needed.

Conversion decisions

Local SEO we'd implement for the real business

This demonstration page ships only WebPage schema naming it a demo — fictional structured data in a real index would be dishonest markup. For a real upholsterer we'd ship LocalBusiness + Review schema like this:

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "LocalBusiness",
  "name": "Second Sitting Upholstery",
  "description": "Re-upholstery and recovering — armchairs, sofas,
    dining seats and banquettes. East Victoria Park workshop,
    Perth metro pickup and delivery.",
  "telephone": "+61 8 9000 0268",
  "address": {
    "@type": "PostalAddress",
    "addressLocality": "East Victoria Park",
    "addressRegion": "WA",
    "postalCode": "6101",
    "addressCountry": "AU"
  },
  "areaServed": ["East Victoria Park", "Victoria Park", "Como",
    "South Perth", "Bentley", "Cannington", "Perth"],
  "openingHoursSpecification": [{
    "@type": "OpeningHoursSpecification",
    "dayOfWeek": ["Tuesday","Wednesday","Thursday","Friday","Saturday"],
    "opens": "08:00", "closes": "16:00"
  }],
  "priceRange": "$$",
  "review": {
    "@type": "Review",
    "reviewRating": { "@type": "Rating", "ratingValue": "5" },
    "author": { "@type": "Person", "name": "— real customer —" },
    "reviewBody": "— pulled live from the review platform —"
  },
  "aggregateRating": {
    "@type": "AggregateRating",
    "ratingValue": "— live —", "reviewCount": "— live —"
  }
}
</script>

Plus a Google Business Profile wired to the same NAP, suburb landing content for the pickup run, and review schema fed from a live platform — never hand-typed.

What would change for the real client

Iteration log

Pass 1 — Craft

Drove the whole page headlessly (Chrome + puppeteer-core, port 6803): 42 asserts green, zero console errors. Rest state measured $1,570 against an independently re-implemented oracle; wrap 0.842 at the 7s settle; empty submit on a fresh load raised 8 validation signals; filled submit confirmed in the lexicon; every chip, stepper and the FAQ driven with dispatched MouseEvents.

The screenshot review (thumbnail, 375 fold, reduced-motion) found what the asserts couldn't: the webbing roll read as a lollipop on a stick — redrawn as a flat roll with a cross hub; seat-cushion seams vanished into the fabric — panel strokes darkened to #124A42 at 2px; armchair arms widened 84→92 with a seventh tack per rail so the piece reads as furniture, not diagram.

Pass 2 — Depth

Added the second-read details: brass tacks now "tap" in with a 220ms overshoot as their rail wraps; the parked shears snip once every ~14 seconds; the price total pops 6% on change. Measured sofa scenarios against the oracle: 2-seat velvet $2,400, linen $1,950, linen + foam ×2 $2,190; banquette 4.0 lm $1,920.

The eyeball review caught a real honesty defect no assert modelled: switching to dining chairs kept rendering the previously chosen velvet pattern while the card said "commercial-grade range" — the dining scene is now locked to the commercial weave regardless of prior selection (and can never carry the velvet sheen). One harness expectation was corrected the other way: foam honestly persists between fixed pieces, so armchair + performance + foam = $1,550 was the right figure — fix the harness, never the site.

Pass 3 — Hardening

Masthead first at 375: screenshot + measured edges (wordmark right 153.6px, call chip 293–353px — 59.9px clear). Above-fold at 375×720: h1 at 143px, primary CTA 55.6px tall at 468px, trust row 550px — all inside the first screen. Sticky bar fixed with 48px tel target in the bottom 140px. scrollWidth 375 and 768 both clean; an offender sweep found zero non-SVG elements past the viewport edge. Keyboard drive: chips, stepper and FAQ all operate on Enter with visible brass focus rings (screenshot reviewed). Reduced motion renders wrap = 1, sheen parked, entrance skipped. Chanel rule: removed the outermost dashed radius ring from the service-area map — two rings say "near ring", three said clutter.