Concept
The wave's dark site: the page lives inside the roof space at dusk. Amber heat presses down through the tiles, cool blue conditioned air pools in the lounge below, and the batt layer is drawn as the barrier between them. The argument is visible physics — a thicker batt visibly damps how many arrows get through — and the estimate rides on the same picture, so the commercial claim and the visual claim are one object.
The heat-gain claim stays hedged on purpose: “by most accounts, up to a third of summer heat gain comes through an uninsulated ceiling” — and the visualiser shows flow density only, never a fabricated temperature readout. The summer/winter toggle flips the arrow direction because both directions are honest: the same barrier works both ways.
Palette
The ink/line split holds throughout: amber carries marks and the two CTA weights, plaster white carries every sentence, and the muted #B8AE9F derived ink carries secondary copy at ≥7:1.
Type
Chivo 700/800 for display — solid and technical-warm, right for a trade that argues in R-values; the typographic moment is the R-value set at 58px inside the cross-section with the batt layer thickening beneath it. Hanken Grotesk as the workhorse: friendly counters, excellent at 14–16px on dark ground.
The Heat-Loss Visualiser
One inline SVG (720×420): tiled roof, roof space, ceiling line, lounge room. Two arrow populations are rebuilt from state on every change — six constant “load” arrows between roof and batt, and a through-ceiling population whose count and opacity fall as R rises (R4.0: 7 at .8 · R5.0: 5 at .68 · R6.0: 3 at .6, all riding a sine flicker floored at 0.55 so the signature is visibly alive at any thumbnail instant). A single rAF loop translates them along their spans with the delta clamped at zero and capped at 50ms; it pauses on document.hidden and never starts under reduced motion, where a spread static frame renders instead.
- R chips rebuild the batt layer — ten segments between joist ticks, 16/22/28px tall — with a staggered entrance on first draw only; later redraws run the snappy dual-speed path (11ms stagger, ~500ms settle).
- The removal toggle is the second-read detail: the old flattened grey layer lifts out on a dashed vacuum line and the new batts drop 8px onto bare joists. Its +$8/m² lands as its own ledger line.
- Token invalidation in the handler: every control bumps a token and swaps state synchronously before any deferred work; the settle timer checks its token before declaring
window.__insulation.settled. The rAF loop reads the live arrow array each frame, so a rapid-fire burst of clicks can never leave it closed over dead geometry. - The price badge sits on the artwork, top-right of the cross-section, inside the first 750px at 1200 wide — the thumbnail carries the commercial argument, not a ledger below the fold.
Pricing model — pure anchor arithmetic
| Scenario | Arithmetic | Billed | Minimum note |
|---|---|---|---|
| Rest — R5.0 · 180 m² | 180 × $13 | $2,340 | — |
| R6.0 · 180 m² | 180 × $15 | $2,700 | — |
| R6.0 · 180 m² + removal | 180 × ($15 + $8) | $4,140 | — |
| R4.0 · 100 m² | 100 × $11 | $1,100 | — |
| R6.0 · 60 m² (boundary) | 60 × $15 = $900 exactly | $900 | none — clamp not binding |
| R4.0 · 80 m² | 80 × $11 = $880 | $900 | shown — billed at minimum |
| R4.0 · 90 m² | 90 × $11 = $990 | $990 | — |
All figures were measured from the running page and cross-checked against an independently reimplemented oracle in the verification harness; every one landed exactly on the spec's pinned arithmetic. Rodent-damaged and asbestos-flagged ceilings print no number by design — “quoted after inspection” is the honest deliverable there, and the estimator card says so.
Conversion decisions
- Above the fold at 375px: name, the one-line promise (“the cheapest air-conditioner you'll ever buy”), a quote CTA, the call button, and the AS 3999 trust line — the visualiser follows immediately for anyone who scrolls.
- Calculator→form handoff: R-value and area flow into the enquiry form's plan line, and the form's own R chips and area field write back to the planner, so the two surfaces can never disagree.
- The minimum is copy, not a gotcha: $900 appears in the hero trust line, the services grid, the FAQ, and as a designed ledger note that appears only while it binds.
- Sticky call/quote bar on mobile; tap targets ≥44px; the confirmation panel is local, honest about being a demo, and appears only after a valid submit.
What would change for the real client
Their job photos in the reviews strip (every Warmline job is “photographed in the roof space” — a real feed beats drawn art here), a live Google reviews source, and booking wired into their field-service tool — ServiceM8, Tradify or AroFlo all take a lead-with-metadata webhook, so the R-value/area scenario lands on the job card before the callback. Quote follow-up automation (a day-3 nudge with the roof-space photos attached) is the single highest-yield add for this trade, and the estimator's scenario string is built to be that email's first line.
Local SEO we'd implement for the real business
This demo page ships only WebPage/BreadcrumbList schema naming it a demonstration — fictional structured data in a real index would be dishonest markup. For a real Warmline, we'd ship HomeAndConstructionBusiness (schema.org has no insulation-specific subtype, so the construction-family parent is the correct precise choice) with real NAP, geo, hours and a genuine review feed:
{
"@context": "https://schema.org",
"@type": "HomeAndConstructionBusiness",
"name": "Warmline Insulation",
"description": "Ceiling insulation supply, install and top-up across the Perth metropolitan area.",
"telephone": "+61 8 XXXX XXXX",
"url": "https://warmlineinsulation.com.au",
"address": {
"@type": "PostalAddress",
"addressLocality": "Malaga",
"addressRegion": "WA",
"addressCountry": "AU"
},
"geo": { "@type": "GeoCoordinates", "latitude": -31.857, "longitude": 115.899 },
"areaServed": "Perth metropolitan area",
"openingHoursSpecification": [
{ "@type": "OpeningHoursSpecification",
"dayOfWeek": ["Monday","Tuesday","Wednesday","Thursday","Friday"],
"opens": "07:00", "closes": "16:00" },
{ "@type": "OpeningHoursSpecification",
"dayOfWeek": "Saturday", "opens": "08:00", "closes": "12:00" }
],
"priceRange": "$$",
"review": [{
"@type": "Review",
"author": { "@type": "Person", "name": "Real Customer" },
"reviewRating": { "@type": "Rating", "ratingValue": "5" },
"reviewBody": "Sourced from the live Google Business Profile feed, never hand-written."
}],
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "4.9", "reviewCount": "112"
}
}
Plus suburb-intent copy blocks (the service-area section is built to take them), a Google Business Profile with the roof-space photo stream, and review schema fed from the real profile — never hand-written.
Iteration log
Pass 1 — Craft
- Screenshot review caught the “CEILING BATTS · INSTALLED” caption buried inside the batt layer — every DOM assert was green while the caption rendered illegibly over the orange fill. Both display texts now derive their baselines from the live batt-top (batt top −34 / −12) so they clear the layer at every R and removal state.
- The roof's tile-tick group was declared in markup but never populated — the roof read as two bare strokes. Added 18 procedural ticks along both pitches.
- The “vacuum extraction” label collided with the right roof slope and the hose exited through the tiles; both moved inside the roof space. Mode-tag chip widened 196→212 so the summer label stops kissing the border. Load-arrow lanes moved to a static list that clears the display text.
Pass 2 — Depth
- Badge price now does a quick opacity dip-and-return on every state change (skipped on first draw and under reduced motion) — the eye is pulled to the new figure without a layout twitch.
- Heat arrows gained a ±4px sine sway so the flow reads as convection, not a conveyor. The winter sky now also dims the inner sun disc, which previously ignored the season.
- Drove all seven worked scenarios plus the rapid-fire burst (10 control clicks in one evaluate — season, chips, stepper, removal, no settle between): final state matched the oracle with zero console errors, confirming the in-handler token invalidation.
Pass 3 — Hardening
- 375px: masthead checked first — wordmark right edge 245px vs call-button left edge 285px, no collision; scrollWidth 375 on both pages; sticky call/quote bar verified with a tel: link in the bottom 140px.
- Reduced-motion renders the settled static frame (11 arrows at rest opacity, probe settled, zero errors). rAF delta clamped at zero and capped; loop pauses when hidden. No backdrop-filter anywhere on conversion chrome.
- Chanel rule: removed the duplicate faint coastline stroke from the metro map. Copy read caught the minimum-FAQ boundary (“below about 80 m²” → “at 80 m² and below”, since 80 × $11 = $880 bills at the minimum). Phone pattern audited against v-flag rules — every literal special escaped.