Why this converts
The page answers the visitor’s real question — does this apply to me — before it mentions a price, because a manager who has just been asked whether the building is compliant needs a defensible answer more than they need a quote. The primary call to action, the phone number and one trust line all sit above the fold on a 375-wide phone with 115 px of clearance above a sticky call bar that owns a real tel: link, so the call that starts in a plant room never needs a scroll. The estimator asks three things the manager already knows — building class, what is on the roof, how many of it — and returns the inspection frequency, the action threshold and a fixed monthly figure, which is more than any competing page in this trade puts on a screen at all. Every regulatory sentence is quoted verbatim from the Department of Health with a link beside it, so a cautious buyer can verify the page instead of trusting the business, and the business never has to write the words “trust us”. And the page refuses work twice — a designed $0 answer for excluded building classes, and a full-size card telling the reader to ask their existing contractor for three documents before ringing us — which is the strongest asset on the page precisely because it is the only thing on the first screen of a search result that costs the seller money.
Palette
Two colours argued from the subject: the teal of standing water in a tower basin, and the rust that blooms on galvanised plant nobody has opened in a year. Rust marks the action threshold and overdue states only — it is never decoration. Every ratio below is read from the wave’s contrast sweep, not calculated by hand.
Three colours minted during the build, and what they measure
The sweep declares tokens against grounds; a build still needs hairlines and off-states. These three are reported with their measured ratios so the next wave’s log can carry them, and none of them ever carries text.
| Hex | Role | On #FFFFFF | On #F1F5F6 |
|---|---|---|---|
| #CFDADC | Hairline separators, table rules, leader lines in the drawing | 1.43:1 | 1.30:1 |
| #5E6E72 | Input and select borders (a UI component, needs 3:1) | 5.32:1 | 4.84:1 |
| #9FB1B5 | Unlit sample pins and the empty-basin dash | 2.23:1 | 2.03:1 |
The unlit pin is deliberately quiet: the on/off distinction is carried by fill — a hollow ring against a solid teal disc at 7.60:1 — not by a contrast difference, so a reader with low vision reads the shape rather than the tone.
Type
Plant-room clinical, not corporate. Two faces, both variable, both loaded in one request.
Manrope 700/800 — headings, figures, controls
Manrope’s flat terminals and near-closed apertures read like equipment labelling rather than like a brochure, and at 800 with −0.022em tracking the h1 holds its weight at 48 px without shouting. It also carries the one moment of typographic bravery on the page: the monthly figure at 2.5 rem in tower-water teal, set solid, with the qualifier “+ GST a month” dropped to a third of its size underneath so the number is the object and the tax treatment is the caption.
IBM Plex Sans 400/500 — body, quotations, forms
Plex was drawn for technical documentation and it is the right voice for quoting a regulator: it reads as a specification rather than as marketing, and its lining figures keep the price ladder aligned. Body is 16 px everywhere including mobile, and every quoted regulatory sentence stays in Plex so it visually separates from the brand’s own claims set in Manrope.
The proof element
“What your building owes” — three inputs, four answers, in the order the buyer needs them.
What it does
Building class, system on site, and scale. It answers does it apply, then how often, then what triggers action, then what it costs. Frequency before fee is deliberate: this is a service that runs on a clock, and the manager’s first problem is the schedule, not the invoice.
Classes 1, 2 and 10 return a designed $0 answer rather than an error — the Department lists those three as excluded, so the card says the Regulations do not apply, prints nothing due, and points the reader at their certificate of classification and their building surveyor. The state probe carries isSubstitute: true on that branch.
The arithmetic
Monthly = towers × $395 + (sample points > 0 ? $780 + max(0, points − 5) × $62 : 0), ex GST. A clean event is towers × $1,450 and there are two of them a year, so the first twelve months is monthly × 12 + towers × $1,450 × 2. Line items print to the cent, the figure the customer acts on is rounded once, and GST is applied to the total rather than line by line.
The default state is fixed constants — Class 3–9, two cooling towers, no warm-water system — so the thumbnail is deterministic: $790 + GST a month, $869.00 inc, $2,900 + GST per clean, $15,280 + GST for the first year ($16,808.00 inc). Warm-water only at twelve points is $1,214 + GST ($1,335.40 inc); one tower plus eight points is $1,361 + GST. Every reachable cell of the grid — 146 of them — was driven in a headless browser and compared against the wave’s figures script, not against a reimplementation of it.
The drawing
An inline SVG cooling-tower cross-section: fan and cowl, drift eliminator, spray header, fill pack, and a basin filled in tower-water teal, with the basin sample point pinned and a bank of tower glyphs beneath that appears and disappears with the count. Beside it, a warm-water riser with a calorifier and twenty outlets that light in order as the sample-point count changes. The whole thing is literal markup, not generated at runtime, because a service-area map that never drew and a toggle that never rendered both passed a fully green harness in the previous wave — a drawing that exists in the HTML cannot fail to exist in the browser.
The second-read detail: five droplets above the fan, drawn only while a tower is live. That is the drift — the aerosol the drift eliminator exists to catch, and the actual reason any of this is regulated. Nobody needs to notice it; the people who do, understand the page differently.
Conversion decisions
The proof card sits beside the headline, not below the services grid
The mobile reading order is the conventional one — masthead, hero, services, proof, why-us, reviews, service area, FAQ, form, footer. At 1024 px and up the card moves into the hero’s second column, because the 1200×750 thumbnail is the asset that sells this page in a gallery, and a thumbnail showing a headline and a menu of tiles sells nothing. The answer, the frequency table, the threshold and the fee all land inside the first 750 px; the drawing runs 381–628.
Six form fields, and two of them are the estimator’s own questions
Name, phone, suburb, building class, system on site, plus an optional building name and an optional note. Building class and system are there because they change the reply: a Class 1 submission gets a confirmation that says we would ring to confirm and then leave them alone, and a “not sure what is up there” submission gets an offer to walk the roof for nothing. Phone carries required, inputmode="tel" and a pattern escaped for the v flag, so the browser’s own constraint API refuses junk as well as the JavaScript does — which matters for assistive technology and for anyone auditing the page.
The refusal sits beside the number, never in the FAQ
The cheaper alternative renders in every state of the estimator, directly under the price, with an honest reason when it is not a substitute: on a cooling-tower site it is $0 because the treatment contractor is probably already sampling; on a warm-water site it says plainly that there is no cheaper version, because nobody is attending that system monthly. Below the fold, “When you don’t need us” gets a full card and names the three documents to demand from the incumbent before ringing us.
Prices are published, and labelled as ours
No operator in Western Australia publishes a Legionella monitoring rate. The services grid publishes six, and the section standfirst says in one line that these are ours and are not a market benchmark — the contrast is what converts, not the figure.
The three passes
Each pass drove the estimator to all four worked examples including the $0 branch, and drove the form empty, with junk in the phone field, and filled.
Pass 1 — Craft
- The thumbnail was selling nothing. Driven measurements put the price answer at 758 px and the entire cooling-tower drawing at 1177–1429 px — wholly outside the 1200×750 frame. The card body was rebuilt as a two-column grid at ≥1024: verdict, frequency table, threshold and fee on the left, the drawing on the right, so the drawing begins on the same line as the verdict rather than after it. Answer now at 642–710 depending on the branch, drawing at 381–628.
- The trust line was underneath the call bar at 375×667 — 62 px below it, invisible to a phone user who never scrolls. The h1 went from four lines to three, the subline was cut, and both CTA labels were shortened so they share one row. Clearance is now 115 px against a 12 px floor.
- Opening the thumbnail found two SVG labels running past the viewBox edge — “2 cooling towers · sampled mo…”. The static markup fit; the JavaScript strings that replace it on render did not, so nothing in the DOM was wrong and no assert could see it. Both strings shortened.
- A label was rendering in the wrong style because
.ctrl > .labdoes not match a label nested one level deeper inside.ctrl-row > div. A selector that matches nothing fails silently; this one was caught by eye in the same screenshot. - “Warm-water” wrapped inside its segmented button and made the control row taller than its neighbour.
white-space: nowrapand 0.87 rem.
Pass 2 — Depth
- Added the drift. Five droplets above the fan, drawn only while a tower is live, toggled by the
hiddenATTRIBUTE rather than thehiddenIDL property — the latter is anHTMLElementproperty and does nothing at all on an SVG element. - Gave a number away. When a warm-water plan is at or under the five included sample points, the card volunteers “Adding a sample point later — $62.00 each” below the monthly total, so the buyer can price their own expansion without ringing to ask. It sits below the total, not above it, so the visible ledger still adds up.
- Rebuilt the $0 branch as an answer rather than three zero rows. One line — “Nothing due, this month or any month” — and one action that is not a call to us: read the certificate of classification, or ask the building surveyor. A refusal that ends the conversation is a lost lead dressed as honesty.
- Micro-interaction. The sample pins and the basin water transition their fill over 280 ms on a crafted ease, so changing the point count reads as points lighting along the riser rather than as a redraw.
- Re-drove all four worked examples and the full 146-cell reachable grid after the changes; every cell still agrees with the wave’s figures script.
Pass 3 — Hardening
- Junk in the format-constrained field. “ring me maybe” typed into the phone box is refused by the page and by
checkValidity(), and thepatternattribute is compiled with thevflag in the checker rather than eyeballed — av-invalid character class is silently discarded by the browser and the field then accepts anything. - Removed one ornament. The teal dot bullets in the hero trust line wrapped onto their own line at 375 and read as a stray mark; they are gone, replaced by a single primary rule down the left edge of the sentence.
- Verified the computed layout, not the look.
getComputedStyle(proof).gridColumnis read directly, because an id rule can silently lose to a descendant selector on the same property and every state assert stays green while the card sits in the wrong column. - Scanned both files for curly quotes inside HTML attributes, for street-type nouns in prose, for banned
LocalBusiness/Review/AggregateRatingschema, and for a nestedOrganizationundercreator— Sapience is credited withcreditTextonly. - Motion. No animation loop ships, so there is nothing to pause on
document.hiddenand nothing for a seven-second thumbnail to catch mid-flight. The only motion is one staggered entrance and the CSS transitions, both settled inside 900 ms and both removed underprefers-reduced-motionin CSS and in thematchMediacheck before the stagger starts. - Re-shot the 1200×750 thumbnail and the 375×667 phone view and looked at both before writing this pass.
Local SEO we’d implement for the real business
The live demo emits only a WebPage block naming itself a demonstration, because fictional structured data in a real index is dishonest markup and the reviews on the page are demo copy. For a real Osborne Park operator we would ship the following, with their real address, their real ABN, and reviews pulled from their verified Google Business Profile rather than typed into the page.
{
"@context": "https://schema.org",
"@type": "LocalBusiness",
"@id": "https://graylinewater.example/#business",
"name": "Grayline Water Hygiene",
"description": "Independent Legionella monitoring, sampling and cleaning schedules for cooling towers and warm-water systems across Perth.",
"url": "https://graylinewater.example/",
"telephone": "+61 8 9000 0701",
"priceRange": "$$",
"address": {
"@type": "PostalAddress",
"streetAddress": "Unit 0, 000 Example Way",
"addressLocality": "Osborne Park",
"addressRegion": "WA",
"postalCode": "6017",
"addressCountry": "AU"
},
"geo": { "@type": "GeoCoordinates", "latitude": -31.900, "longitude": 115.820 },
"areaServed": [
{ "@type": "City", "name": "Perth" },
{ "@type": "City", "name": "Joondalup" },
{ "@type": "City", "name": "Midland" },
{ "@type": "City", "name": "Kwinana" },
{ "@type": "City", "name": "Rockingham" }
],
"openingHoursSpecification": [{
"@type": "OpeningHoursSpecification",
"dayOfWeek": ["Monday","Tuesday","Wednesday","Thursday","Friday"],
"opens": "06:30", "closes": "16:00"
}],
"hasOfferCatalog": {
"@type": "OfferCatalog",
"name": "Legionella monitoring services",
"itemListElement": [
{ "@type": "Offer",
"itemOffered": { "@type": "Service", "name": "Monthly cooling-tower service and sample" },
"priceSpecification": { "@type": "PriceSpecification",
"price": "395.00", "priceCurrency": "AUD", "valueAddedTaxIncluded": false } },
{ "@type": "Offer",
"itemOffered": { "@type": "Service", "name": "Six-monthly clean and disinfect, per tower" },
"priceSpecification": { "@type": "PriceSpecification",
"price": "1450.00", "priceCurrency": "AUD", "valueAddedTaxIncluded": false } }
]
},
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "4.9", "reviewCount": "38"
},
"review": [{
"@type": "Review",
"author": { "@type": "Person", "name": "Verified reviewer name" },
"datePublished": "2026-08-14",
"reviewRating": { "@type": "Rating", "ratingValue": "5", "bestRating": "5" },
"reviewBody": "Real review text, synced from the verified Google Business Profile."
}]
}
Alongside it: one FAQPage block built from the six questions already on the page (they are the questions the trade is actually asked, which is what makes them worth marking up), a Service page per suburb only where the operator genuinely attends that suburb, and the Google Business Profile category set to the service the buyer searches for rather than the one the operator calls itself.
What would change for a real client
Their asset register becomes the source of truth
A water-hygiene operator at this size runs recurring maintenance out of simPRO or AroFlo — asset-level records, recurring job templates, technician forms on a phone in a plant room with no signal. We would read the tower and sample-point assets straight out of it so the estimator quotes from the register rather than from a select, and so an existing customer’s renewal quote is generated rather than typed. A smaller operator on ServiceM8 or Tradify gets the same integration against a flatter data model.
Laboratory results stop being PDFs in an inbox
Legionella and HPC results arrive from the laboratory’s portal as emailed reports. We would run those through an inbox pipeline — Make.com, n8n or Power Automate, whichever the client already pays for — parsing the count, the sample point and the date into the register, and firing the above-threshold alert the hour it lands rather than the day someone opens the attachment. The client’s compliance register then reports itself in Power BI: every tower, every result, every clean date, exportable for an auditor.
The page’s own plumbing
Form submissions into Freshsales or HubSpot with the building class and system type mapped to fields, so a Class 1 enquiry is closed politely instead of chased. The plant walk books against a real calendar — Microsoft Bookings or Calendly — rather than promising a call back. Invoicing and the GST lines go to Xero. Reviews come from the verified Google Business Profile through its API, so the testimonial block is live and the schema above is truthful. Photography replaces nothing: the tower cross-section stays, because a diagram of the reader’s own plant explains the service better than a photograph of somebody else’s roof.
What we would not change
The refusal branches. They are the reason the page is worth building, and they are the first thing a nervous client asks to remove.
Build facts
| Item | Detail |
|---|---|
| Libraries | None. No framework, no chart library, no animation library. |
| External requests | Google Fonts only (Manrope, IBM Plex Sans), one stylesheet request. |
| Images | None. Every visual is inline SVG or CSS — the tower cross-section, the riser, the service-area map, the favicon. |
| Page weight | Both pages self-contained, all CSS and JavaScript inline, each comfortably under 120 KB. |
| Structured data | One WebPage block naming the page a demonstration, crediting Sapience Analytics with creditText. No LocalBusiness, no Review, no AggregateRating. |
| State probe | window.__legionella exposes the three inputs and every derived figure, including the cheaper alternative and an isSubstitute boolean. |
| Sources quoted | WA Department of Health (Legislation and guidelines for cooling towers and water systems) and the Office of the Auditor General WA (Regulation of Air Handling and Water Systems, Report 20: 2022-23). Both linked at every quotation. |