Tuart & Stone

Guide · Industry demo 218 · Landscaping

How Tuart & Stone was made.

Tuart & Stone Landscapes is a fictional design-and-build landscaper based in Wembley Downs, serving Perth's western suburbs. The audience is a homeowner with a sand-patch backyard and a Pinterest board; the page has one job — book a $220 design consult. Everything on it, including the signature before/after reveal, is procedural SVG, CSS and vanilla JS in a single file.

Conversion decisions

Palette — argued from the coastal plain

#F1ECDF limestone creamground — the coastal plain's stone, quarried and dressed
#2E4A38 eucalypt deep greenstructure and body ink — 8.2:1 on the cream
#C9922E banksia goldLINE/MARK only — rules, icon strokes, the reveal's tag spines. Never small text on cream (2.3:1)
#6B4A32 soil umbersecondary ink and earth blocks — 6.6:1 on cream

The ink/line split is enforced throughout: gold draws, green and umber speak. On the green review and footer grounds, small cream text runs at ≥.72 alpha to hold 4.5:1.

Type

Bricolage Grotesque (display, 700–800) has the organic irregularity the brand needs — grotesque bones with warm, slightly hand-cut terminals, set tight at −.02em. Public Sans carries body, forms and UI: plain-spoken, legible at 16px on a phone, appropriately governmental about prices. The one typographic flourish is the hero's gold underline swash on "western suburbs" — a mark, not text.

Techniques — the before/after reveal

The signature is two hand-drawn SVG scenes of the same backyard — identical fence geometry, identical 1200×750 viewBox — stacked in one stage. The "before" (sand, dead kikuyu patches, rusting hills hoist, cracked concrete, abandoned hose) sits on top, clipped with clip-path: inset(0 calc(100% − --pos) 0 0); the "after" (limestone retaining and terrace, kangaroo paw and grevillea beds, cedar screen, striped kikuyu lawn, pop-up retic) renders full underneath. A single CSS custom property --pos drives the clip, the divider line and the handle, so pointer, touch and keyboard all share one setPos() path. The handle is a real role="slider" button — arrows step 2, PageUp/Down 10, Home/End jump — with aria-valuetext reporting "N% of the finished garden revealed".

Five scope tags are positioned over the after-scene in viewBox coordinates, each with a trigger x; when the divider crosses a trigger the tag surfaces with its honest price anchor. At full reveal (≤12%) a total chip surfaces the whole-yard figure. The page parks at 42% so the static thumbnail already reads as a transformation with three tags up. Everything else is restrained: one entrance stagger gated on document.fonts.ready with a timeout fallback, CSS-only plant sway and sprinkler shimmer (killed under prefers-reduced-motion), no rAF loops anywhere, no libraries.

Live state is exposed at window.__landscaper — slider position, visible tags, total-chip state, form status and errors, open FAQ panels — so QA asserts on state, never on wall-clock.

Local SEO we'd implement for the real business

This demo deliberately ships no LocalBusiness, Review or AggregateRating structured data — fictional entities in a real search index would be dishonest markup. The page carries only a WebPage schema naming it a demonstration. For a real client, this is the block we'd ship, fed by their actual NAP, licence numbers and review feed:

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "LandscapeArchitect",
  "name": "«Real Business» Landscapes",
  "telephone": "«real phone»",
  "url": "https://«realdomain».com.au",
  "address": {
    "@type": "PostalAddress",
    "streetAddress": "«street»",
    "addressLocality": "Wembley Downs",
    "addressRegion": "WA",
    "postalCode": "6019",
    "addressCountry": "AU"
  },
  "geo": { "@type": "GeoCoordinates", "latitude": -31.9, "longitude": 115.77 },
  "areaServed": ["Wembley Downs","City Beach","Floreat","Scarborough",
                 "Doubleview","Churchlands","Wembley","Subiaco","Claremont",
                 "Swanbourne","Cottesloe","Nedlands","Karrinyup"],
  "openingHoursSpecification": [{
    "@type": "OpeningHoursSpecification",
    "dayOfWeek": ["Monday","Tuesday","Wednesday","Thursday","Friday"],
    "opens": "07:00", "closes": "16:00"
  }],
  "priceRange": "$$$",
  "aggregateRating": { "@type": "AggregateRating",
    "ratingValue": "«from review feed»", "reviewCount": "«count»" },
  "review": [{
    "@type": "Review",
    "author": { "@type": "Person", "name": "«real reviewer»" },
    "reviewRating": { "@type": "Rating", "ratingValue": "5" },
    "reviewBody": "«pulled from Google Business Profile via API»"
  }]
}
</script>

Plus: Google Business Profile optimisation, suburb-level service pages, and review markup fed live from the client's actual Google reviews — never hand-typed.

What would change for the real client

Iteration log

Pass 1 — Craft

Pass 2 — Depth

Pass 3 — Hardening

Verified headlessly with puppeteer-core against Chrome — 7-second settle, console-error collection, state-probe assertions, drive-then-diff, 375px and reduced-motion passes.