Concept
The audience is Swan River and Rockingham boat owners: due for antifoul, needing an outboard service before the season, or buying a boat and wanting the truth about it. The direction is the slipway at 7am — salt white, hull shadow, honest yard work — premium shipwright pride rather than a chandlery catalogue. Every visual is procedural: the hardstand scene, the service-area map and all ornament are inline SVG drawn in the site's palette; there is not a single raster image on the page.
Conversion decisions
- Above the fold at 375px: brand + promise h1, the $85/ft anchor in the lede, a red "Get a slipping quote" CTA and the insured-yard/EPA trust chips — all inside the first viewport. The dark rate card is desktop-only garnish; on mobile the lede carries the numbers.
- Estimator directly under the hero — it is the argument. Length slider, hull chips and service chips feed a live job card, and "Use this in my quote" hands the configured scenario to the form (calculator→form handoff), so the demo interaction becomes a warmer lead. The form is deliberately not pre-filled at rest, so its validation stays honest on first load.
- Form fields: name, phone, suburb/pen, timing, and one free-text "boat & the job" field. Boat owners describe their boat fluently — one textarea beats six dropdowns. Confirmation is local and demo-honest: "we'd call you back within one business day", plus a note that nothing was sent.
- Sticky mobile bar with Call + Quote, both ≥44px targets; header carries a permanent tel: link.
- Honesty as positioning: prop/shaft and gelcoat work are listed without prices ("quoted after inspection / moisture readings") — a yard that prices osmosis off a webpage reads as a yard to avoid, and the FAQ says no to hardstand sleepovers and refers insurance surveys to accredited surveyors.
Palette — the hardstand at 7am
Ink/line split (LEARNINGS W2/W5): antifoul red measures ~5.2:1 on salt white — fine for 40px display and thick rules, marginal for small text — so captions use #933431 (5.7:1+) or the derived inks. Dark panels carry their own light body-ink #C9D6DA (9.0:1).
Type
- Barlow Condensed 600/700 — display, all-caps with tracking: stencilled hull-name confidence, the way a boat wears its name on the transom.
- IBM Plex Sans — body and UI: clean, technical, unfussy.
- IBM Plex Mono — lengths, prices, hours, the job card: every figure set like a line on a written quote.
The Slipway Estimator — technique
The hardstand scene is a single inline SVG rebuilt from parameters on every interaction. A hull-path generator takes the slider length (16–45 ft mapped at 15px/ft) and the hull type and produces the sheer, stem and run as cubic Béziers; monos get a keel subpath and poppet stands with keel blocks, catamarans get a second hull drawn offset (+16px x, −7px y) behind the first so a twin bow reads in side view, and trailer boats sit on a procedural trailer — frame, guards, two wheels, drawbar and jockey wheel — with an outboard on the transom.
The antifoul band is the same hull path duplicated, filled antifoul red and clipped by a
rect starting at the hull's waterline height, so the "paint" always follows the hull exactly
(keel and second hull included). The waterline itself is a stroke-dash draw-in with
butt caps, kept visibility:hidden until
its dash length is set — the LEARNINGS W15 round-cap dot never gets a frame to render.
A dimension line above the boat mirrors the slider ("26 ft"), the estimate figure counts up
over 320ms with a cubic ease-out, and everything settles instantly under
prefers-reduced-motion. State probe:
window.__marine, updated on every interaction.
Pricing model — source of truth
The spec pins these anchors; the harness's independent oracle re-implements exactly this table and asserts the page's displayed figures against it.
| Service | Rate | Model shape |
|---|---|---|
| Haul-out & antifoul — mono | $95/ft | Linear in length. 26 ft rest state = 26 × 95 = $2,470. |
| Antifoul — catamaran | $152/ft | Mono rate + 60% (95 × 1.6). 32 ft = 32 × 152 = $4,864. |
| Antifoul — trailer boat | $85/ft | On the owner's trailer, no haul-out — this is the services grid's "from $85/ft" floor. 20 ft = $1,700. |
| Outboard 100-hr service | from $440 | Deliberately flat in hull length — service cost scales with the engine, not the boat, so the line is shown as "from" and never multiplied by feet. |
| Shaft-drive diesel service | from $590 | Flat, as above; twins and larger engines are quoted per engine. |
| Pre-purchase inspection | $30/ft | Linear in length. 26 ft = $780; the review's "best $900" buyer is a 30-footer — 30 × 30, cross-surface consistent. |
Services are additive: multiple chips sum their lines on the job card (26 ft mono, antifoul + outboard + inspection = 2,470 + 440 + 780 = $3,690), and the total is labelled "from" whenever any flat-rate line is included. The card, the services grid, the hero lede and the reviews all quote from this one table.
Local SEO we'd implement for the real business
This demo page ships only WebPage schema naming it a demonstration — fictional structured data in a real index would be dishonest markup. For a real yard we would ship:
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "LocalBusiness",
"name": "Southerly Marine Services",
"description": "Boat servicing, antifouling and pre-purchase
inspections on the O'Connor hardstand, Fremantle.",
"telephone": "+61-8-XXXX-XXXX",
"address": {
"@type": "PostalAddress",
"streetAddress": "[real street address]",
"addressLocality": "O'Connor",
"addressRegion": "WA", "postalCode": "6163",
"addressCountry": "AU"
},
"geo": { "@type": "GeoCoordinates",
"latitude": -32.06, "longitude": 115.79 },
"openingHoursSpecification": [
{ "@type": "OpeningHoursSpecification",
"dayOfWeek": ["Monday","Tuesday","Wednesday","Thursday","Friday"],
"opens": "07:00", "closes": "16:00" },
{ "@type": "OpeningHoursSpecification",
"dayOfWeek": "Saturday", "opens": "07:00", "closes": "12:00" }
],
"priceRange": "$$",
"areaServed": ["Fremantle","East Fremantle","Bicton","Applecross",
"Mosman Park","Coogee","Henderson","Rockingham"],
"aggregateRating": { "@type": "AggregateRating",
"ratingValue": "[from live review feed]",
"reviewCount": "[from live review feed]" },
"review": [ /* synced from Google Business Profile —
never hand-written */ ]
}
</script>
What would change for the real client
- Job cards & quoting: the estimator posts into Simpro or Tradify as a draft job with length/hull/services structured, not free text; quote acceptance flows back.
- Accounts: Xero or MYOB for invoicing, deposits on slipping bookings.
- Pre-purchase workflow: Boat History Check and DoT WA registration lookups wired into the inspection report template.
- Operations: Deputy rostering for the yard crew; SMS service reminders via Podium keyed to each hull's last antifoul date ("the Swan says 12–18 months — you're at 14").
- Their assets: real before/after hull photos replacing the procedural scene's supporting role, a live Google reviews feed, real licence and insurance numbers.
Iteration log
Pass 1 — Craft
Built a 43-assert puppeteer harness (headless Chrome, port 6203) with an independently re-implemented pricing oracle, then read the real screenshots. Found and fixed:
- The quote form was pre-filled by the estimator at rest, which would have masked the empty-submit validation probe (LEARNINGS W15) — pre-fill now happens only on interaction; a fresh load submits empty and all 4 required fields flag with aria-invalid.
- The mono's keel was excluded from the antifoul band — a yard that leaves the keel bare gets laughed off the river. Band path now includes keel and, for cats, the second hull.
- A deck rail line drawn straight through the mono's cabin; removed.
- The cat's second hull (vertical offset) read as a sheer stripe, not a twin hull — re-drawn with a horizontal +16px offset so a second bow visibly peeks past the first.
- The waterline stroke overshot the bow by ~30px at 38+ ft — end point now derived from the stem geometry per hull type.
- Map suburb labels drowned on the dark river stroke (Bicton's dot was in the water) — dots moved to the banks, labels given a salt-white paint-order halo.
Pass 2 — Depth
- Estimate figure now counts up over 320ms with a cubic ease-out (skipped under reduced motion; the probe's displayed field tracks the animation target so QA never reads a mid-tween value).
- Second-read detail: boats of 40 ft and over get a gull perched on the pulpit.
- Chip press micro-interaction (scale .96 on :active, motion-guarded).
- Re-drove every scenario: 32 ft cat $4,864, 20 ft trailer $1,700, multi-service 26 ft mono $3,690, inspection-only $780 — all displayed figures matched the oracle; the last-service guard held (can't deselect the final chip).
Pass 3 — Hardening
- Contrast audit caught three small-text failures: slider tick labels and the map caption in #8FA6AD on light ground (~2.2:1 → ink-soft #4A5D64, 6.3:1) and the scene's WATERLINE badge in #B0413E on the hardstand grey (4.3:1 → #933431, 5.7:1).
- Chanel rule: removed a paint-pot-on-a-post prop that read as a thermometer.
- Honesty sweeps: positive phone sweep (all 4 occurrences of (08) 9xxx use the reserved 9000 0238; tel: hrefs identical), JSON-LD types are WebPage/CreativeWork only, exactly one h1 per page, banned-word scan clean.
- node --check on the 18.4KB extracted inline script; zero console errors on desktop and 375px runs.
- Final full-harness run: 43/43 — 375px wordmark/nav/CTA in-viewport checks, sticky-bar tap scrolls to the form, FAQ driven open by dispatched MouseEvents and by keyboard, drive-then-diff liveness, reduced-motion settled state with the waterline fully drawn. Screenshots of mono/cat/trailer/gull states reviewed by eye.
Techniques: inline SVG (parametric hull generator, clip-path paint band, dash draw-in, procedural map), CSS grid/flex layout, vanilla JS. No libraries, no build step, no raster images — Google Fonts is the only external resource.