How Pilotlight was made: a changeover priced before the phone rings

Design and build notes for a fictional Perth hot water business — the household-band pricing model, the SVG system elevations, and the conversion decisions underneath the warmth.

Concept

The urgency trade

Pilotlight Hot Water is the wave's urgency demo: the customer is a household whose system died overnight, phone in hand, comparing two or three tabs at 7am. The page has one job — a changeover enquiry with the system and size already chosen — so everything serves the promise booked by 11am, installed the same day. The planner comparing gas, electric and heat pump before theirs dies is the second reader, and the running-cost column is for them.

The signature, the Changeover Board, converts because it removes the two questions that stall every hot water call: what size do I need and what will it cost. A household stepper (1–6 people) maps to three size bands, four system chips redraw an annotated SVG elevation of the actual unit, and the ledger prices it live — supplied & installed, plus an estimated annual running cost declared as an estimate, not a bill.

Palette

Flame and copper against porcelain

The warmth is the product, so the warm hues are spent only where warmth means something: copper on the pipework and the CTAs, pilot-flame blue on every rule, dimension line and the burner itself. The ground stays porcelain — the calm of a clean bathroom wall — and the ink is the dark of a steel flue. No panic red anywhere: urgency is carried by copy and the same-day strip, not by alarm colours.

#F5F3EF porcelainpage ground — bathroom-wall calm
#2A2724 flue inkbody text, 12.7:1 on porcelain
#B96A34 copper pipemarks, pipework, spec plates — never small text
#3D7A9E pilot-flame bluerules, dimensions, the burner flame

The ink/line split (LEARNINGS): copper CTAs sit on the deeper fired-copper #A6591F (5.1:1 under white label text), and links use #2E6284 flame-deep (5.9:1 on porcelain) — the brighter accents stay lines and marks.

Type

An appliance badge, not a brochure

Sora 600/700 — "Cold shower this morning."
Inter 400/500/600 — body, forms, annotations.

Sora's rounded-industrial letterforms read like the moulded badge on a modern appliance — right for a brand named after a pilot flame. The display moment is the same-day promise plus the three litreage spec plates (26L · 250L · 270L) set as dark plates with Sora numerals, echoed by the badge on every SVG elevation. Inter carries forms and small annotation text where legibility beats character.

Techniques

The Changeover Board, under the lid

Everything is procedural SVG + vanilla JS in one file — no build, no rasters. Each system is a builder function returning an elevation drawing: the wall-mounted continuous-flow box with its burner cutaway, the storage cylinders with flue or element, the heat pump with fan module and refrigerant loop. Pilot-blue dimension callouts (height, width, litreage badge) are generated per size band, so stepping the household from 2 to 5 physically grows the tank on the wall.

  • Dual-speed redraw (W21): the first draw earns entrance choreography (110 ms/element stagger, 990 ms cap); every later chip/stepper change redraws snappy (26 ms/element, 340 ms cap) via a firstDraw flag.
  • Token invalidation (W25): every control handler bumps boardToken and drops parts.ready before the state swap, so the persistent rAF loop can never touch a stale scene mid-rebuild. Verified with a twice-over rapid-fire drive — nine dispatched clicks in one evaluate, no settle between.
  • Animation with a floor (W18): the burner flame breathes at opacity 0.775 ± 0.225 — never below 0.55, so any capture instant shows a lit flame. The element glow pulses the same way; the refrigerant loop marches its dash offset; the fan spins. All in one rAF loop that pauses on document.hidden and never starts under reduced motion (a settled static frame instead).
  • State probe: window.__hotwater exposes household, band, system, size, price, running cost and a settled flag — QA gates on state, never on seconds.
  • Handoff (W12): "Book this changeover" pre-fills the enquiry form's system select and job-details field with the configured scenario, so the demo interaction becomes a warmer lead.

Conversion decisions

Why the page is shaped like this

  • Call first, form second. A dead system is a phone job: the copper call CTA leads the fold and the mobile sticky bar; the form is for after-hours and planners. The callback promise ("within 30 minutes in business hours") is printed as static copy beside the button — the designed confirmation panel is separate, hidden until a valid submit.
  • The leak toggle. "Is it leaking?" isn't decoration — a yes routes real triage into the confirmation: turn off the cold inlet valve, mind the switchboard. Same honesty rule as the FAQ's gas-smell answer, which routes to ATCO's real 13 13 52 emergency line rather than the fictional business.
  • Price-less states are designed, not missing. Restricted-access and rooftop installs say "quoted on photos — access drives the labour". The heat-pump STC rebate prints no number anywhere: it's an authority-set, market-priced figure, so the declaration ("applies at the day's certificate price, deducted on your quote") appears on the service card and beside every heat-pump total.
  • Flatness as a statement (W15): same-day carries no premium — "the price on the board is the price on the invoice" is the trust line.
  • Internal consistency (W12): the services grid floor "from $1,590" equals the board's cheapest output (125L electric, 1–2 band), and each grid "from" price is its system's band floor.

Honest numbers

The pinned model, measured from the running page

Supplied-&-installed changeover prices by household band, with estimated annual running cost for a typical Perth household (3–4 band shown; 1–2 band scales ×0.7, 5–6 band ×1.3 — every product an integer by construction, totals whole-dollar).

Measured rest state: gas continuous, 3–4 band — $2,150 installed, $340/yr estimated running.
System1–2 people3–4 people5–6 peopleEst. running (3–4)
Gas continuous flow16L — $1,99026L — $2,15032L — $2,390$340/yr
Gas storage135L — $1,790170L — $1,990170L — $1,990*$420/yr
Electric storage125L — $1,590250L — $1,890315L — $2,190$620/yr
Heat pump270L — $3,990 less STCs270L — $3,990 less STCs270L — $3,990 less STCs$190/yr
Repairsfrom $180, callout included — valves, thermostats, elements, igniters
Restricted access / rooftopno number by design — quoted on photos, access drives the labour

*The 5–6 gas-storage card carries the honest nudge: at that household size, continuous flow recovers faster — worth the extra $400 ($2,390 − $1,990). Band edges land on integer stepper values, so both boundaries (2→3 and 4→5) are asserted exactly: stepper 2 = 16L $1,990/$238, stepper 3 = 26L $2,150/$340, stepper 5 = 32L $2,390/$442. All 24 household×system scenarios were verified against an independently reimplemented oracle of this table.

Process

Three passes, driven not skimmed

Every pass drove the interactive surface headlessly — empty and filled form submits, every chip and the stepper across both band boundaries, the rapid-fire burst, screenshots at 1200 and 375 — and asserted state, DOM and pixels. 60+ assertions green plus zero console errors at the end.

Pass 1 Craft

  • The thumbnail lied by omission: at 1200×750 the ledger's $2,150/$340 sat below the fold — the rest state read as a drawing with no price. Capped the elevation at 296px and tightened board spacing so the capture now shows elevation, price and running cost together (the W19 signature-in-thumbnail rule).
  • v-flag regex trap (W19): the phone field's pattern carried unescaped parentheses — legal in old regex, a parse error under Chrome's v-flag compilation that would have silently killed validation. Escaped to [0-9 \(\)+]{8,14}.
  • Removed a stray class on the leak fieldset's legend that was inheriting flex layout it never asked for.

Pass 2 Depth

  • Pipes floated in air beside the storage and heat-pump cylinders. Joined them into the units with elbow fittings, and recoloured the isolation valve pilot-blue so it reads as the cold side.
  • Added HOT / COLD annotations under the continuous-flow unit's risers — the trade detail a plumber would notice (second read), alongside the flickering wordmark pilot flame and the burner's ~7-second flame kick.
  • The electric element's glow read muddy inside its dark cutaway — brightened the coil stroke (#F2A75E at 3.8) and enlarged the halo so the pulse reads at board size.

Pass 3 Hardening

  • 375 masthead first (W18): wordmark right edge clears the tel chip's left edge measured explicitly; the "Hot Water" tail drops below 392px. No horizontal overflow on either page; sticky call bar fixed in the bottom 140px with a tel: link.
  • Reduced motion: emulated prefers-reduced-motion — the rAF loop never starts, every element renders at full opacity, settled flips true. Keyboard: chips and steppers are native buttons (Enter drives them, verified), focus-visible ring on tab.
  • Chanel rule: removed the middle wall line from the elevation backdrop — two horizontals carry the room; three were noise.
  • Rapid-fire burst (nine clicks in one evaluate, twice) landed on oracle-correct state with zero console errors — the token-invalidation pattern held.

Local SEO

What we'd implement for the real business

This demo page ships only honest WebPage/CreativeWork structured data, because fictional business schema in a real search index is dishonest markup. For a real hot water operator we'd ship the full local stack — schema.org's exact trade subtype is Plumber (hot water changeover is plumbing and gas work):

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Plumber",
  "name": "Pilotlight Hot Water",
  "description": "Same-day hot water system changeovers, repairs
    and heat pump upgrades across the Perth metro area.",
  "url": "https://pilotlighthotwater.com.au",
  "telephone": "+61 8 XXXX XXXX",
  "address": {
    "@type": "PostalAddress",
    "addressLocality": "Balcatta",
    "addressRegion": "WA",
    "postalCode": "6021",
    "addressCountry": "AU"
  },
  "geo": { "@type": "GeoCoordinates", "latitude": -31.875, "longitude": 115.828 },
  "areaServed": { "@type": "City", "name": "Perth" },
  "openingHoursSpecification": {
    "@type": "OpeningHoursSpecification",
    "dayOfWeek": ["Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"],
    "opens": "07:00", "closes": "19:00"
  },
  "priceRange": "$1,590–$3,990",
  "hasOfferCatalog": {
    "@type": "OfferCatalog",
    "name": "Hot water services",
    "itemListElement": [
      { "@type": "Offer", "itemOffered": { "@type": "Service",
        "name": "Same-day hot water changeover" } },
      { "@type": "Offer", "itemOffered": { "@type": "Service",
        "name": "Heat pump upgrade with STC rebate handling" } }
    ]
  },
  "aggregateRating": { "@type": "AggregateRating",
    "ratingValue": "4.9", "reviewCount": "214" },
  "review": [{
    "@type": "Review",
    "author": { "@type": "Person", "name": "Real Customer" },
    "reviewRating": { "@type": "Rating", "ratingValue": "5" },
    "reviewBody": "Dead tank at 7am, hot showers by dinner."
  }]
}
</script>

Fed by real data only: the review feed from Google Business Profile, the rating from actual jobs, the phone from call tracking. Never fabricated.

For the real client

What changes when this stops being a demo

The enquiry form posts to the job platform the operator already runs — ServiceM8, Tradify, Simpro or AroFlo — so a 7am enquiry lands as a booked job with the board's scenario attached, not an email. The Changeover Board's price table becomes a config the owner edits when supplier costs move. Reviews pull live from Google Business Profile; the phone number gets call tracking; install photos replace nothing, because the procedural elevations stay — they're the explainer real photos can't be. STC rebate figures pull from the day's certificate price via the installer's registry feed, keeping the "deducted on your quote" promise automatic.