Hardline Water Treatment View the site  ·  Index

How it was made · site 398

The Column A bore-water treatment page whose best state is the one where it tells you to buy nothing. Build notes: the schematic, the integer capacity model, the measured palette, and the two numbers our own arithmetic would not let us print.

The concept

Hardline Water Treatment is a fictional Wanneroo operator who plumbs iron filters, softeners and UV for households on the Gnangara groundwater mound — and who talks about half the callers out of buying anything. The page has one job: turn “what do I need for my bore?” into a booking, with the treatment train, the regeneration interval and the installed price already on the screen before anyone picks up a phone. The answer the page is proudest of is nothing, so that state got designed first and everything else was built around it.

The audience is the person whose limestone paving has gone orange, not the plumber. Every figure on the page is computed live from a pricing model that was written down, enumerated and independently re-derived before a line of layout code.

Taking the page over mid-build

This is the honest record: the page was not built in one sitting. A first pass wrote index.html — complete markup, complete script, a working schematic — and then the machine lost its network before the guide was written or a single assertion had been run. The build notes and the reasoning behind it were gone.

So the work resumed cold, from the file on disk. Nothing was rebuilt. The page was read end to end, a fresh price-and-capacity oracle was written from the specification alone (deliberately not from the page's own code), and the shipped page was then driven — every chip, every stepper, every toggle, both submits — and compared against it. That is how the two arithmetic defects below were found: presence is not correctness, and a page that parses cleanly and asserts green can still be printing a number that is not true.

What the verification actually ran

3,195 oracle checks over the model's full 864-cell control grid, plus 1,172 assertions driven against the live page in headless Chrome: 108 grid cells clicked through and compared figure by figure, the six-step softener unwind chain driven in full, the designed $0 refusal asserted as a designed state, empty and filled submits, a rapid-fire drive with no settle between clicks, the screenshot-resize gate, reduced motion, focus states, and geometry at 375 / 900 / 960 / 1000 / 1024 / 1100 / 1200 at device pixel ratios 1 and 2.

Where our arithmetic disagreed with the brief

The brief for this wave is explicit: re-derive its numbers independently, and where your model disagrees, ship the reading that reproduces the pinned endpoints and report the disagreement rather than quietly changing either side. Every pinned price and capacity figure survived re-derivation untouched — $5,990 at rest, 3,906 L, every 6 days, the $1,640 floor, the $8,690 ceiling, 6,944 L on a garden bore, 6,250 L at seven people, the $3,100 iron-only cell, and the $0 refusal. Two things did not.

1. “It would regenerate every second day”

The brief pins the reason shown when the 25 L column is locked out at six people: “at six people the 25 L column would regenerate every second day.” Run the brief's own capacity model and it does not. A 25 L column holds 1,250,000 mg of hardness capacity; on a 320 mg/L domestic bore that is 3,906 L, and six people draw 960 L a day — four days, not two. On a 180 mg/L garden bore, which the same rule and the same sentence also cover, it is seven days. One string, two cells, untrue of both.

The sizing rule itself is right and was kept exactly as pinned, unwind chain and all. What changed is the sentence: it is now a template over the real counterfactual, computed with the same integer arithmetic as the read-out, so it cannot drift from it — and the argument moved to the part that is true at every cell, which is service flow rather than a dramatic interval. At seven people on a domestic bore the card reads “it would be regenerating every 3 days”; at six on a garden bore, “every 7 days”. The pinned closing clause is untouched: it’s the wrong size, not the cheaper one.

2. The annual salt bill

Not a brief figure — a number the first pass had written into the FAQ, and the one place on the page where the arithmetic was badly wrong. It read “something like $80–$120 a year on salt”. The rest-state column regenerates every six days at about 9 kg of salt a time: roughly sixty regenerations, about 520 kg a year. At $18–$26 for 20 kg that is $470–$680 a year — five times what the page claimed.

It now prints the real number, with the derivation beside it, because on a page whose whole argument is “we will tell you when not to buy”, understating the running cost of the thing you are buying is the one error you cannot afford. It also happens to be the strongest sentence in the FAQ.

One smaller gap, resolved rather than disputed: the brief pins the refusal badge's second line for scheme water (scheme water · 60 mg/L · nothing worth installing) but the same $0 state is also reachable on a bore with nothing ticked, where that string would be false. The page derives the parallel line for that case — domestic bore · 320 mg/L · nothing selected yet — and the pinned bare refusal headline is used verbatim in both.

The signature: the Column

One SVG treatment train drawn left to right across the fold: the bore or the meter, the pipe run, the sediment and iron filter, the softener column with its resin bed and brine tank, the UV steriliser, and the outlet to the house. Everything on the page moves left to right, because that is what water does.

Two things make it more than a diagram. First, the water in the pipe changes colour at the stage that actually earns it — iron-stain ochre in, treated teal out, and the change happens at the iron filter and nowhere else, because nothing else in the train removes iron. A separate pale rind runs along the top and bottom of the water for as long as the hardness is still in the line, and stops at the softener. If you fit a softener and no iron filter, the water stays ochre the whole way and the rind stops early: the drawing will not pretend a vessel does a job it does not do. Second, the vessels you have not bought stay on the line as empty dashed outlines, so the drawing is always a picture of what you are not buying as much as what you are.

How it is built

Plain SVG, assembled synchronously in one pass, no libraries at all — no three.js, no GSAP, no canvas. The frame is deliberately fixed at 520 units wide and never zooms: the point of the drawing is the empty positions, and they only read if they hold still. The smallest element in the model, a lone UV vessel, is 14% of the frame's width and stays legible at 375 px. Annotation is sized in CSS pixels and converted into scene units on every draw, so a caption is 13 px on a wide frame and 12 px on a phone rather than shrinking with the viewBox.

The scene's height is a function of the badge's measured height, not a fixed aspect ratio — the train is pushed down by exactly the badge's box plus a margin, which is why the taller three-line refusal badge never lands on a vessel. Below 640 px the badge stops floating and docks beneath the artwork instead. A single requestAnimationFrame loop drives the flow line's dash offset and the UV lamp's glow; its delta is clamped at zero as well as capped, so a vsync timestamp that precedes the scheduling call can never drive a negative length. The flow line's opacity never drops below 0.42, so the drawing is visibly alive at any capture instant rather than at a phase we tuned for.

The entrance choreography — an eased opacity stagger, part by part — runs on a cold-load flag with a settle-timer backstop. A resize never re-enters it. That matters more than it sounds: taking a clipped screenshot changes device metrics and fires the resize handler, so a page that replays its entrance on resize photographs itself half-drawn while every state assertion stays green. The standing check drives the controls, waits with nothing poking the page, and then requires every animated group fully opaque.

The model

Money is held as integer cents and divided by 100 once, at display; hardness capacity as integer milligrams of calcium carbonate; litres and days as floors of integer divisions. There is no floating-point anywhere in the price path, and every one of the 864 cells lands on a whole dollar.

capacity      = resinLitres × 50,000 mg CaCO3   // 50 g/L, the moderate salt dose
litresBetween = floor(capacity / hardness)
dailyUse      = people × 160 L
daysBetween   = floor(litresBetween / dailyUse)

total = 0                                   // nothing selected: the refusal state
      | 650 + softener + iron + uv + plumbing
Anchors, and the worked cells the page was verified against
LineFigureNote
Install & commissioning$650Charged once per job. It is the floor, and it only exists when there is something to commission.
Iron & manganese filter$2,450Catalytic media vessel, backwash valve, 20 µm pre-filter.
Softener, 25 L resin$2,890Locked out from six people up.
Softener, 40 L resin$3,450About 14 kg of salt a regeneration.
Whole-house UV$990Rated dose only. No health claim, anywhere.
Bypass loop / meter run+$680 / +$1,150Not billed in the $0 state — nothing to connect.
Rest state — domestic bore, 4 people, 25 L, iron on, no UV, existing tap-in$5,9903,906 L between regenerations · every 6 days.
Garden bore, 4 people, 25 Lsame hardware6,944 L · every 10 days. The source changes the interval, not the cost.
Domestic bore, 7 people, forced 40 L6,250 L · every 5 days.
Cheapest reachable non-zero — UV only, existing tap-in$1,640Equals the services grid's “from” floor exactly.
Dearest reachable — 40 L, iron, UV, meter run$8,690
Scheme water, everything forced off$0The designed refusal. No dollar figure is printed at all.

The two caps, and the unwind

The softener size is capped by either of two independent controls, which is what makes the unwind interesting. Scheme water forces it to none and disables the iron filter with it; six people or more disables the 25 L column and forces the selection up to 40 L. The size you picked while you were free is remembered, and restored only when it becomes valid again — on a partial unwind, where the constraint relaxes but your remembered size still does not fit, the forced value holds. The six-step chain is driven in full on every pass:

StepStateSoftenerWhy
1domestic bore · 4 people25 LFree choice. The memory is set.
2domestic bore · 7 peopleforced 40 LThe household cap bites.
3scheme · 7 peopleforced noneThe source cap bites too; iron is disabled with it.
4scheme · 4 peoplehold noneOne cap lifted, the other still imposes it.
5domestic bore · 7 peoplehold 40 LPartial unwind: the remembered 25 L is still invalid.
6domestic bore · 4 peoplerestore 25 LThe memory becomes valid and comes back.

The memory is cleared by any explicit pick made while the chip set is free. Implementation detail worth recording: what is drawn and priced is a pure function of (pick, source, people) rather than a mutable “current size”. Tracking a current value and inferring the lock from whether it is allowed fails step 4, because none is always allowed and so never reads as forced.

Controls that honestly cannot change the price

Two of them, and both say so on the page rather than pretending. The household stepper changes the regeneration interval and the daily draw and never the price — “more people don't cost more to plumb, they just get you to the next regeneration sooner” — and the pass asserts that stepping 1 to 8 leaves the total identical at a fixed column size while the interval visibly moves. In the $0 state the connection chip still redraws the pipework but is not billed, and the card says why: there is nothing on the wall to connect yet.

Palette

Argued from what actually comes out of a Perth bore: the ochre iron leaves on limestone, the teal of treated water in a glass, and the pale grey-green of the aquifer itself. Every ratio below was measured with WCAG arithmetic from the shipped custom properties, not estimated — 39 rows, none failing. Both accents clear 4.5:1 as body text on their own ground, so neither needed a second darker ink minted for type.

Aquifer paper · #EFF2F1ground · ink 14.33:1A grey-green with no warmth in it. Holds the page off the cream-and-terracotta default.

Deep water ink · #13242014.33 paper · 16.14 cardBody copy, the ledger's ground, and the primary button under white labels.

Treated teal · #0E6E725.34 text · 6.01 as fillThe treated flow, vessel marks, eyebrows. Hover #0A585B at 8.20:1.

Iron ochre · #95560F5.16 text · 5.81 as fillThe raw flow, the stain, the hardness figure, the refusal badge's edge.

Muted ink · #4A5A566.45 paper · 7.27 cardSecondary copy and every form placeholder.

Raw bore water · #C89050ink on fill 5.81:1The water before anything touches it.

Treated water · #8FC6C8ink on fill 8.52:1After the iron comes out. Also the sticky bar's call button.

Resin bed · #C2A276ink on fill 6.71:1The ion-exchange bed, drawn at its real depth per column size.

Scale rind · #E5DCC8ink on fill 11.84:1Hardness still in the line, and the salt in the brine tank.

Vessel body · #DCE4E2ink on fill 12.48:1Every pressure vessel. Pipe steel is #AEB8B7 at 7.95:1.

Gnangara mound · #C6D4CDlabel ink 10.53:1Deepened in Pass 2 — see the log.

Indian Ocean · #C2D8DAlabel ink 10.86:1Map water, deepened with the mound.

Map labels are body-ink-class text and are set in the ink with a ground-coloured halo wherever they cross a fill; the accents draw lines and marks on the map and never letters. Neither accent is used as a ground for type anywhere on the page: every label in the schematic sits on the white card, below or above the drawing, never inside a fill.

Type

Epilogue at 700/800 for display — a geometric grotesque with a slightly mechanical lowercase, chosen because a treatment train is an assembly of standard parts, not a bespoke object. Mulish at 400/600 carries the body: quiet, wide-apertured, comfortable at 16 px on a phone in one hand. DM Mono at 500 gets exactly one job and never leaves it — water figures and machine data: 320 mg/L · 3,906 L · 25 L resin. Every eyebrow, spec label and unit on the page is mono; every sentence is not.

The typographic moment is the read-out under the schematic: the regeneration interval set at roughly 4.3 rem with its unit riding at 42% of that size, BETWEEN REGENERATIONS beneath it in mono at 0.19 em tracking, and the installed price stamped beside it at the same scale across a hairline. Two numbers, enormous, in a page otherwise set very quietly — the interval in teal because it is a property of your water, the price in ink because it is a property of our work.

Iteration log

Three passes on the page as it stood, each one driving the interface rather than reading it. What follows is what was actually found and changed, not a checklist.

Pass 1

Craft

  • The two arithmetic defects above. Both came out of the oracle, not out of looking: the “every second day” lock reason and the five-times-understated salt bill.
  • The control panel was ragged-bottomed. align-items:start on the control grid left each row's cards ending at different heights — a six-card panel that read as six unrelated cards. Removed; the cards in a row now share a baseline.
  • The ADWG footnote wrapped to three lines inside a 110 px column. It was sitting in the hardness cell of the four-column spec grid while the other three columns hung empty beneath their values. It is a statement about the whole read-out, so it now spans the grid under a hairline, and reads as the footnote it is.
  • Form placeholders were Chrome's default grey, roughly 4.3:1 on the field ground. Set explicitly to the muted token, 6.45:1.
  • The right column opened on a card while the left opened on an eyebrow. The signature now carries its own eyebrow and one line of explanation outside the card, so both columns start on the same foot at the same y — and the drawing is named before you reach it. It was tried inside the card first, in the empty upper-left; at mid widths it collided with the taller three-line refusal badge, so it came back out.

Pass 2

Depth

  • The second-read details were invisible. The suspended iron in the raw water was drawn in the ochre accent, which measures 2.09:1 against the water it sits in — it could not be seen at any width. Re-inked to the body ink at 34% (1.78:1 composited), which is also closer to what suspended iron and manganese actually look like in a glass, and spaced tighter.
  • So was the stain on the house — the single drawn consequence of leaving the iron in, at 0.30–0.55 opacity on a pale wall. Now at full weight, 3.81:1 composited. Turn the iron filter off on a bore and the page shows you the orange paving.
  • A new second read: the brine tank's salt level is real. It stands at the dose the ledger quotes — a shorter block for the 25 L column's 9 kg, a taller one for the 40 L column's 14 kg. Nobody is meant to notice first time.
  • The Gnangara mound did not register. The mound is the entire service-area argument and it measured 1.09:1 against the land it sits in — invisible. Mound and ocean were both deepened (#C6D4CD and #C2D8DA), separating them to 1.23:1 and 1.20:1 while the labels keep 10.5:1 or better in the ink.

Pass 3

Hardening

  • 375 px: no horizontal overflow, no offending element, wordmark right edge clear of the call button's left edge in the masthead, every tap target at or above 44 px, sticky bar owning both a tel: link and an in-page anchor to the form. Checked at native resolution, not from a downscaled capture.
  • Badge clearance re-checked as a rectangle-intersection test rather than a formula, at 375 / 900 / 960 / 1000 / 1024 / 1100 / 1200 and at device pixel ratio 2, in the rest state and in the taller refusal state: the badge's box never intersects a drawn vessel, and no two labels in the schematic overlap at any width.
  • The screenshot-resize gate: a clipped screenshot is taken, then the page is left completely alone for 1.6 seconds, and every animated group is required to be fully opaque and the scene settled. Repeated for a viewport resize and a DPR change.
  • Rapid-fire: sixteen control clicks dispatched in a single evaluation with no settle between them, then a second burst of fifteen — zero console errors, state coherent against the oracle afterwards, scene at rest and visible.
  • Form patterns proved by driving them, not by reading the attribute: both pattern attributes were fed a deliberately invalid value and asserted to reject it, then a valid one and asserted to accept it. A pattern Chrome rejects at parse time validates nothing and looks perfectly fine.
  • Chanel rule: the decorative opening quote-glyph came off the reviews. The card, the blockquote and the attribution already say “this is a quote” three times over; without it every review starts on the same left margin as its caption, and the captions now bottom-align across the row.
  • Reduced motion: the scene draws settled and opaque with no loop at all, and the controls still work. Focus: a 3 px teal outline on every chip, stepper, link, summary and field, verified by focusing them.

Conversion decisions

Why the refusal state exists at all

A treatment company that will tell you not to buy is the most valuable thing this business owns, so it is the loudest state on the page rather than a footnote. One click on Scheme and the softener and iron filter lock out with a reason, the price becomes $0, three empty vessels stay drawn on the line, and the badge says nothing to install — we'd rather tell you than sell you. No dollar figure is printed anywhere in that state. It is also the answer for the Mount Claremont renovator who has been quoted $6,000 to soften Water Corp water, which is a real conversation this trade has weekly.

Why the price is on the drawing

The badge sits on the schematic rather than only in a ledger below, because the first screen is the commercial argument. The rest-state total appears in the hero paragraph as well as on the badge, so it survives any crop.

Why these form fields

Name, phone, suburb, source, whether it has been lab-tested, and one free-text box asking what the water does rather than what it is — “the paving's orange” is worth more to a quoting plumber than a guess at a hardness figure. The lab-test question is the one that changes the callback: it sorts the people who need a filter from the people who need a $90 test first. Nothing is required beyond name, phone, suburb and source, because every extra required field costs enquiries. The estimator hands off to the form on an explicit button, never silently, writing the whole configuration into the message box — a warm lead instead of a name.

Honesty, on a page selling water treatment

No health claim appears anywhere. Hardness, iron and sediment are removed; a UV lamp inactivates bacteria to its rated dose and no further, and the page says that beside the toggle, beside the service, and in the FAQ. The NHMRC guideline values quoted are aesthetic values and the page uses the word every time. The business is a plumbing business, not a laboratory, and the FAQ says so in those words.

Local SEO we’d implement for the real business

The live page carries only WebPage, CreativeWork and BreadcrumbList schema that names it a demonstration — putting fictional LocalBusiness or Review data into a real search index would be dishonest markup. For an actual Wanneroo operator we would ship the block below, with real reviews from a verified feed rather than hand-written strings, alongside a Google Business Profile in the Water Treatment Supplier and Plumber categories, suburb landing pages for the corridor, and the FAQ marked up as FAQPage.

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Plumber",
  "@id": "https://example.com.au/#business",
  "name": "Hardline Water Treatment",
  "description": "Bore and scheme water treatment across Perth's northern corridor: iron and
                  manganese filtration, softening, UV and sediment.",
  "url": "https://example.com.au/",
  "telephone": "+61 8 9000 0398",
  "priceRange": "$$",
  "image": "https://example.com.au/img/wanneroo-install.jpg",
  "address": {
    "@type": "PostalAddress",
    "streetAddress": "Unit 4, 118 Prindiville Drive",
    "addressLocality": "Wangara",
    "addressRegion": "WA",
    "postalCode": "6065",
    "addressCountry": "AU"
  },
  "geo": { "@type": "GeoCoordinates", "latitude": -31.7960, "longitude": 115.8080 },
  "areaServed": [
    { "@type": "City", "name": "Wanneroo" }, { "@type": "City", "name": "Two Rocks" },
    { "@type": "City", "name": "Yanchep" },  { "@type": "City", "name": "Gnangara" },
    { "@type": "City", "name": "Ellenbrook" }, { "@type": "City", "name": "Joondalup" }
  ],
  "openingHoursSpecification": [
    { "@type": "OpeningHoursSpecification",
      "dayOfWeek": ["Monday","Tuesday","Wednesday","Thursday","Friday"],
      "opens": "07:00", "closes": "16:30" },
    { "@type": "OpeningHoursSpecification",
      "dayOfWeek": "Saturday", "opens": "08:00", "closes": "12:00" }
  ],
  "hasOfferCatalog": {
    "@type": "OfferCatalog",
    "name": "Water treatment",
    "itemListElement": [
      { "@type": "Offer",
        "itemOffered": { "@type": "Service", "name": "Whole-house UV steriliser" },
        "priceSpecification": {
          "@type": "PriceSpecification", "priceCurrency": "AUD",
          "minPrice": 1640, "valueAddedTaxIncluded": true } },
      { "@type": "Offer",
        "itemOffered": { "@type": "Service", "name": "Iron & manganese filter" },
        "priceSpecification": {
          "@type": "PriceSpecification", "priceCurrency": "AUD",
          "price": 2450, "valueAddedTaxIncluded": true } },
      { "@type": "Offer",
        "itemOffered": { "@type": "Service", "name": "Water softener, 25 L resin" },
        "priceSpecification": {
          "@type": "PriceSpecification", "priceCurrency": "AUD",
          "price": 2890, "valueAddedTaxIncluded": true } }
    ]
  },
  "aggregateRating": {
    "@type": "AggregateRating",
    "ratingValue": "4.9", "reviewCount": "137"
  },
  "review": [{
    "@type": "Review",
    "author": { "@type": "Person", "name": "R. Petrovic" },
    "datePublished": "2026-07-14",
    "reviewRating": { "@type": "Rating", "ratingValue": "5", "bestRating": "5" },
    "reviewBody": "Paving had gone the colour of a brick. Iron filter and softener in over
                   one day, head programmed in front of me. Two summers and no stain."
  }]
}
</script>

What would change for a real client

The page is deliberately backendless. For a working Wanneroo installer the same front end would sit on their tools rather than on a local confirmation:

  • Jobs and scheduling — the enquiry lands in ServiceM8 or Simpro as a job with the full configuration attached (source, hardness, column size, interval, price), so the installer opens a card that already knows the answer. Tradify or AroFlo where that is the incumbent.
  • Service intervals — UV lamps are annual and sediment cartridges are not; a recurring job template per installed asset, seeded at commissioning, is the difference between a one-off sale and a maintenance base.
  • Reviews — a live Google Business Profile feed replacing the demo copy, which is also what makes the Review schema above honest to ship.
  • Quotes and invoicingXero for the invoice, with the configuration becoming the quote's line items rather than being retyped.
  • Lab reports — the “do you have a report?” answer routes to a file upload, and a parsed hardness and iron figure pre-loads the estimator so the customer sees their water, not a typical bore.
  • Measurement — call tracking on the tel: links and configuration-level analytics, because knowing which trains people build before they ring is worth more than a page-view count.