Guide · how it was made
A lawn you price by stepping it out
Fairway Synthetic Turf is a fictional Canning Vale synthetic-lawn installer, built as a conversion-first landing page demonstration. The audience is a Perth household staring at dead grass under sprinkler restrictions; the page's single job is a lawn-install enquiry with the lawn's dimensions already on the meter.
Palette — the pile and the sand infill
Two greens because a real lawn has two: the pile colour and the band the mower leaves. Everything warm around them is the sand the installer actually broadcasts into the pile. Small text never sits in either green — a separate muted ink (#46504A, 6.7:1) does that work.
Type
Outfit (600/700) is the display face — geometric and landscape-brochure modern, the voice of a company that draws presentation sheets for backyards. Source Sans 3 carries body copy at 16.5px. The typographic moment is the ledger's square-metre readout: the area set huge in Outfit with "never watered / never mowed" stacked quietly beside it — the whole pitch in one glance.
The Lawn Planner — signature & implementation
A top-down SVG yard plan drawn like a landscape architect's sheet: paver-grid ground, the house wall with its sliding door and cast shade, a dashed boundary fence, and the lawn itself drawn live at 26 px per metre. Width (2–12 m) and length (2–20 m) steppers resize the drawn lawn; pile chips swap the fill grammar — 30 mm draws wide mow bands, 40 mm draws twice-dense bands, the 13 mm putting green swaps to sand-dressed rings with a cup, flag, and a ball mid-putt on a dotted roll line (the second read). The strip toggle rolls the old patchy lawn back at one corner — a pattern-filled region terminated by a drawn turf roll — and tight access opens a side gate with a hand-barrow path.
Everything is rebuilt into three SVG groups per state change. Mow bands are registered with a single persistent requestAnimationFrame loop that owns a shimmer clock (dt = min(max(now − last, 0)/1000, 0.05) — clamped at zero and capped) and drives band opacity as 0.75 + 0.2·sin(t·1.15 + i·0.65): a floor of 0.55, so the signature is visibly alive at any capture instant. The first draw earns entrance choreography (26 ms stagger, 1.5 s cap); every later redraw is snappy (8 ms, 500 ms cap) behind a firstDraw flag. Every control handler bumps a generation token before touching state, and rebuilds replace the loop's scene object atomically — so a rapid-fire burst of clicks can never leave a stale closure animating a dead lawn. State is exposed at window.__turf (scenario, area, rates, raw, billed, minApplied, settled) so QA gates on state, never on seconds.
The pricing model (all figures measured from the running page)
| Line | Rate | Worked example | Measured |
|---|---|---|---|
| 30 mm family lawn | $92/m² | 20 m² (4 × 5 m), no toggles | $1,840 · no minimum note |
| 40 mm premium lawn | $105/m² | 40 m² (5 × 8 m) + strip | $4,760 (rest state) |
| 13 mm putting green | $135/m² | 20 m² (4 × 5 m), no toggles | $2,700 |
| Strip & cart old lawn | +$14/m² | 40 m² on 30 mm | $4,240 |
| Tight access, hand-barrow | +$9/m² | 40 m² on 40 mm + strip | $5,120 |
| Minimum job | $1,840 billed | 16 m² (4 × 4 m) on 30 mm = $1,472 raw | billed $1,840 · note shown |
| Concrete/paving removal | No number — quoted on inspection; thickness and access drive it | price-less by design | |
The minimum is a total-level clamp with its note shown only while it binds: at exactly 20 m² of 30 mm ($1,840 on the nose) the note stays hidden — the boundary is reachable from the integer stepper grid and was asserted there. Every figure above was verified against an independently reimplemented oracle in the QA harness, plus a rapid-fire drive (14 clicks across two no-settle bursts) whose settled state still matched the oracle ($4,050 at 6 × 5 m putting green).
Conversion decisions
The planner lives in the hero. It's the signature and the qualifier — a visitor who steps out 5 × 8 m has already told us the job size, so its bounding box shares the first 750 px with the headline instead of sitting below the fold.
Planner → form handoff. Dimensions, pile and the strip toggle pre-fill the enquiry form (and flow back the other way), so the enquiry arrives warm: name, phone, suburb is all we still ask. Phone validation is a JS digit-count check rather than a pattern attribute — Chrome now compiles patterns as v-flag regexes and a dead pattern fails silently.
Call first, form second. Trade traffic is a phone in one hand: tel: in the masthead, a sticky call/quote bar under 720 px, and the form's confirmation is a designed local panel — hidden on load and on empty submit, shown only after a valid submit, with a plain line saying no enquiry was sent anywhere.
Honest edges. The minimum-job note appears only while it binds; concrete removal prints no number at all ("quoted on inspection — thickness and access drive it"); the FAQ concedes the 40° underfoot heat and the enzyme rinse for small dog yards. A demo that argues honestly is the sales evidence.
Local SEO we'd implement for the real business
This demo page ships only WebPage/WebSite/BreadcrumbList structured data naming it a demonstration — fictional business schema in a real index is dishonest markup. For a real installer we'd ship HomeAndConstructionBusiness (schema.org has no landscaping/turf subtype — that's the correct family type a real SEO build would choose) plus a review feed:
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "HomeAndConstructionBusiness",
"name": "Fairway Synthetic Turf",
"telephone": "+61 8 9000 0307",
"url": "https://fairwayturf.example.com.au",
"address": {
"@type": "PostalAddress",
"addressLocality": "Canning Vale",
"addressRegion": "WA",
"postalCode": "6155",
"addressCountry": "AU"
},
"geo": { "@type": "GeoCoordinates", "latitude": -32.058, "longitude": 115.918 },
"areaServed": "Perth metropolitan area",
"openingHours": ["Mo-Fr 07:00-17:00", "Sa 08:00-12:00"],
"priceRange": "$$",
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "4.9",
"reviewCount": "87"
},
"review": {
"@type": "Review",
"author": { "@type": "Person", "name": "A real customer" },
"reviewRating": { "@type": "Rating", "ratingValue": "5" },
"reviewBody": "Pulled from the live Google Business Profile feed, never hand-written."
}
}
</script>
Ratings and reviews would be fed from the client's real Google Business Profile — never fabricated. Add suburb-level service pages, a Google Business Profile with photo cadence, and review-request automation off job completion.
What changes for a real client
Their install photography would replace nothing structural — the procedural plan stays as the quoting tool — but their finished-lawn gallery earns a strip above the reviews. The planner's anchors become their real rates, loaded from one place. Job management wires into what they already run: ServiceM8 or Tradify for the enquiry-to-job-card handoff (the form posts the planner scenario straight into the job description), AroFlo for the bigger multi-crew operators, a Google Business Profile reviews feed for live social proof, and Xero + Stripe deposits so the measured quote can be accepted and paid from the phone it was read on.
Iteration log
- Pass 1 — Craft. The plan's title block overflowed its box: the single data line ("lawn 5.0 × 8.0 m · 40 m² · 40 mm premium lawn") clipped against the SVG edge at every pile — split into two short dynamic lines (dims/area, pile/scale). Planner header allowed to wrap at 375 instead of squeezing the sub-line. Measured the 375 masthead explicitly: wordmark right edge 130 px vs call-button left edge 142 px — 12 px clear, no collision. Contrast audit: body ink 13.2:1, button text 6.3:1, muted ink 6.7:1.
- Pass 2 — Depth. Added the house wall's cast shade (a soft gradient below the wall line — the plan stopped floating), the putting-mode ball with its dotted roll line to the cup (second-read detail), a 320 ms token-invalidated count-up on the installed total, and a scale-pop on stepper outputs. The old-lawn roll gained its end-cap ellipses so it reads as a rolled cylinder, not a bar.
- Pass 3 — Hardening. Full 42-assert harness re-run green: every worked example measured equal to the spec's arithmetic (zero corrections), boundary semantics asserted at exactly 20 m² ($1,840, no note) and 16 m² (note shown), rapid-fire 14-click burst matched the independent oracle, empty/filled/short-phone submits drove the confirmation panel's visibility transitions, reduced-motion renders the settled plan with bands at fixed opacity, 375 scrollWidth clean on both pages, zero console errors. Chanel rule: removed the second corner garden bed — one bed says "yard", two said "clip-art".