Kelvin & Coil — build guide

How the Kelvin & Coil appliance-repair demo was made

The concept. Kelvin & Coil Appliance Repairs is a fictional Balcatta whitegoods repairer — the name is the two things every whitegood runs on, temperature and a wound coil. The audience is a household standing in front of a dead washer (or a property manager who needs the fix documented), and the page's single job is to book a repair callout. The demo's argument: an appliance repairer's whole reputation is the moment the tech says "here's what it is, here's what it costs, here's whether it's worth it" — so the site performs exactly that moment, live.

Palette

Enamel-and-teal clinical light

Argued from the bench: white appliance enamel under kitchen light, the steel chassis behind it, a multimeter's LCD, and the amber tag on the live wire. Deliberately clinical rather than fresh — the diagnostic instrument is the brand. The bright meter teal is the LINE/MARK accent (traces, dial, CTA); body copy never sets in it — a darker teal-ink was derived for text duties, and an amber-ink likewise (both clear 4.5:1 on enamel).

#F3F5F6appliance enamel — ground
#2A3138chassis graphite — structure & ink
#0E7C86meter teal — lines, marks, CTA
#0A5E66teal-ink — derived text/button duty
#C97B23fault amber — the glowing fault
#8F5312amber-ink — derived text duty

Type

Manrope · Source Sans 3 · Chivo Mono

Manrope (700/800) is the display voice — an engineered rounded grotesque, precise without coldness, right for a trade that measures things. Source Sans 3 carries body and UI at ≥16px. Chivo Mono is the instrument face: every price, range, percentage, meter reading and the diagnostic caption strip set in it, so the numbers read as measurements, not marketing.

Signature

The Repair-or-Replace Adviser

Five appliance chips, a 1–15 year age stepper and a per-appliance fault select drive a procedural SVG cutaway built entirely in JS (line-work in graphite, ~40–60 elements per scene). The selected fault glows fault-amber with a pulsing halo, a dashed meter-teal probe line runs from a drawn multimeter to the part, and the meter's caption strip prints a per-fault reading ("OL — open circuit", "drum play 2.1 mm"). A verdict dial sweeps a needle between REPAIR and REPLACE: the needle angle maps the fault midpoint against the applicable threshold (60% of typical-new under ten years; 40% at ten-plus), so the dial genuinely shows how close the maths is to tipping. Scene redraws are dual-speed — 26 ms/element stagger on first paint (capped 1.5 s), 11 ms capped ~520 ms on every subsequent change — with a settled flag on the window.__appliancerepair probe. Second-read details: the energy-rating sticker prints the purchase year (2026 − age) and tilts, fades and gathers scuffs as the stepper climbs — rust spots appear at 12+ years — and each fault has its own meter reading.

Honest numbers

The pricing model, derived

All ranges are all-in (parts, labour, GST, with the $129 callout credited when the repair proceeds). Typical-new figures are comparable mid-range replacements. The verdict rule, printed on the page because it IS the product: replace when the fault's midpoint repair cost is ≥ 60% of typical-new, or when the appliance is ten-plus years old and the midpoint is ≥ 40%. Otherwise repair.

ApplianceTypical newFaultRangeMidpoint% of newVerdict <10 yr / ≥10 yr
Washing machine$899Drum bearings$360–$420$39043.4%REPAIR / REPLACE
Drain pump$220–$260$24026.7%REPAIR / REPAIR
Door seal$180–$220$20022.2%REPAIR / REPAIR
Fridge-freezer$1,199Thermostat$210–$250$23019.2%REPAIR / REPAIR
Door seal$190–$230$21017.5%REPAIR / REPAIR
Compressor$520–$620$57047.5%REPAIR / REPLACE
Oven & cooktop$1,049Heating element$190–$230$21020.0%REPAIR / REPAIR
Thermostat$230–$270$25023.8%REPAIR / REPAIR
Door hinges$170–$210$19018.1%REPAIR / REPAIR
Dishwasher$849Drain pump$210–$250$23027.1%REPAIR / REPAIR
Heating element$240–$280$26030.6%REPAIR / REPAIR
Inlet valve$180–$220$20023.6%REPAIR / REPAIR
Dryer$749Heating element$200–$240$22029.4%REPAIR / REPAIR
Drive belt$170–$200$18524.7%REPAIR / REPAIR
Thermostat$190–$220$20527.4%REPAIR / REPAIR

Measured from the running page (verified against an independently reimplemented oracle across all 30 appliance × fault × {8, 10} year scenarios): the rest state — washing machine, 8 years, drum bearings — displays $360–$420 against typical-new $899, 43% of new, verdict REPAIR; stepping the age to 10 arms the second rule and the dial crosses to REPLACE ("the next fault is already queued"). The fridge compressor (displayed 48%, raw 47.5%) crosses identically: REPAIR at 8 years, REPLACE at 10. Both crossings match the spec's pinned examples with zero corrections; on-page percentages are rounded to the nearest whole (43.4 → 43, 47.5 → 48). The adviser's cheapest range floor — $170 (oven door hinges, dryer drive belt) — equals the services grid's "from $170" floor, so the page never argues with itself.

Documented model shape: no pinned fault reaches the 60%-of-new line on its own, so the first rule's REPLACE branch is unreachable through the UI's own data — it exists in code (and in the printed rule) for the real build, where a quoted repair on a cheap unit can cross it. Age below ten years honestly no-ops on price and verdict, and the card says so ("age hasn't tipped the maths yet — at ten years it starts to") while the cutaway visibly weathers — a live control that never silently ignores input.

Conversion decisions

Why the page is shaped like this

Local SEO

The schema we'd ship for the real business

This demo page emits only a WebPage schema naming itself a demonstration — fictional LocalBusiness/Review markup in a real index would be dishonest. The real client's build would ship:

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "LocalBusiness",
  "name": "Kelvin & Coil Appliance Repairs",
  "telephone": "+61-8-XXXX-XXXX",
  "areaServed": ["Balcatta", "Stirling", "Karrinyup", "Osborne Park", "Joondalup", "Perth"],
  "address": { "@type": "PostalAddress", "addressLocality": "Balcatta",
               "addressRegion": "WA", "postalCode": "6021", "addressCountry": "AU" },
  "openingHoursSpecification": [
    { "@type": "OpeningHoursSpecification",
      "dayOfWeek": ["Monday","Tuesday","Wednesday","Thursday","Friday"],
      "opens": "07:00", "closes": "18:00" },
    { "@type": "OpeningHoursSpecification", "dayOfWeek": "Saturday",
      "opens": "08:00", "closes": "14:00" }
  ],
  "priceRange": "$129–$620",
  "aggregateRating": { "@type": "AggregateRating",
                       "ratingValue": "4.9", "reviewCount": "214" },
  "review": [{
    "@type": "Review",
    "author": { "@type": "Person", "name": "Real Customer" },
    "reviewRating": { "@type": "Rating", "ratingValue": "5" },
    "reviewBody": "Pulled from the live review feed, never hand-written."
  }]
}
</script>

— fed from the client's actual review platform, with per-service Service schema (washing machine repair, fridge repair…) and suburb-level FAQ markup from the questions the phone actually gets.

Real client

What would change for the real business

Process

Iteration log

Pass 1 — Craft

Drove the whole page headlessly (puppeteer, port 7003): 62 asserts green — rest state ($360–$420 / 43% / REPAIR), the full 30-scenario oracle sweep, both spec crossings, empty-submit validation on a fresh load (10 signals), filled-submit confirmation, FAQ, sticky bar, fold composition. Screenshot review then caught what the asserts couldn't: the energy-label wear sticker rendered at fixed coordinates that missed every appliance cabinet (floating in mid-air beside the washer) — replaced with a per-scene anchor; the dial's "% of new" caption sat mid-dial where the needle crossed it — moved to the baseline between the REPAIR/REPLACE labels; scuff marks could land off the narrower fridge cabinet — re-ranged to the shared body zone.

Pass 2 — Depth

Screenshot-reviewed all five cutaways plus both REPLACE states at 12 and 10 years. Moved the fridge's sticker off the compressor fault zone (it crowded the glowing part) and the dryer's sticker off the belt ring. Added the verdict pop — a 500 ms overshoot scale on the REPAIR/REPLACE word only when the call actually flips, so the moment the maths tips is felt. Confirmed the second-read layer: purchase year on the sticker (2026 − age), per-fault meter readings, rust spots at 12+ years.

Pass 3 — Hardening

375px masthead first (W18/W21): wordmark right edge vs call-button left edge compared explicitly — clear, button fully in viewport, and the header collapses to icon + "Call" below 430px. Above-fold assert at 375 (h1 + ≥44px CTA + trust line all visible), sticky bar fixed in the bottom band with a 48px tel target, both pages' scrollWidth ≤ 375 (the guide's pricing table ships in a .table-scroll wrapper). Reduced-motion renders the scene settled with a static needle and no probe-line flow. Removed the hero's pinstripe background overlay (the one excess ornament). Zero console errors across every run; QA gates on the state probe's settled flag, never wall-clock waits.

Techniques

Under the enamel