Generative Assets · Wave 17 · Industry Demos VI · Site 244

How Two Rivers Removals was built

A conversion-focused landing page for a fictional Belmont removals company — and the working notes behind it: palette, type, the Move-Size Estimator's honest pricing model, and the three-pass iteration log.

← Back to the site · Collection index

Concept

Two Rivers Removals is a fictional local and office removalist based in Belmont, working the whole Perth metro — named for the fact that every Perth move crosses the Swan or the Canning eventually. The audience is renters, owners, downsizers and small offices, most of them burned once before by a crew that padded the hours. The page has one job: turn "how much will this cost?" anxiety into a warm, pre-qualified estimate enquiry — which is why the signature element is a pricing tool, not an art piece.

The mood is moving-day, 7am: kraft cartons stacked square, blankets folded, tape guns loaded. Calm logistics told with warmth — no cartoon trucks, no urgency theatre.

Palette — argued from the loaded truck

#F2EADC · kraftGround. The carton wall — warm, matte, unbleached.
#2A3240 · carton inkStructure and primary ink — the marker-pen room labels.
#D97B29 · tape orangeLine/mark accent only: packing tape, load bars, CTA fills. Never body text on kraft.
#54739B · blanket blueSecondary — folded moving blankets: wrapped furniture, chips, the two rivers on the map.
#9C520F · tape ink (derived)Where tape orange must carry small type (eyebrows, prices), it darkens to pass 4.5:1 on kraft.
#44608A · blanket ink (derived)Blanket blue darkened for text links — the raw accent measures ~4.1:1, too thin for small text.

The ink/line token split (LEARNINGS W2/W5) is applied from the start: tape orange and blanket blue draw lines and marks; their derived ink variants carry any small type. CTA buttons set carton-ink text on tape orange (≈4.1:1) at 17px bold — large-text territory — with the dark-navy estimate card reserving pure tape orange for its two big mono figures.

Type

The typographic bravery moment is the estimate card: a dark navy panel where the whole quote renders as a mono ledger — uppercase micro-labels, hairline rules, and the hours range and estimate in 21px tape-orange mono. It reads like the printed quote the copy keeps promising.

The Move-Size Estimator — model, derivation, honesty

The signature element is a procedural SVG truck cross-section (no raster anywhere) that fills with drawn cartons and blanket-wrapped furniture to the estimated volume, above an estimate card whose numbers come from one model object — the same source of truth that pre-fills the enquiry form.

Volume table (spec-pinned)

HomeVolumeCrew rule
1-bed unit12 m³2 movers · 8t (40 m³ box)
2-bed apartment20 m³2 movers · 8t
3-bed house33 m³3 movers · 10t (50 m³ box) — over the 30 m³ threshold
4-bed+ house46 m³3 movers · 10t
Piano / pool table+2 m³ eachcounts toward the threshold

Hours model (derived, documented here as QA's source of truth)

Measured scenario table (easy access | stairs)

HomeHours easyEstimate easyHours stairsEstimate stairs
1-bed unit2–3.5$330–$5802.5–4$415–$660
2-bed apartment3–4.5$495–$7453.5–5$580–$825
3-bed house5–7$1,025–$1,4356.5–8.5$1,335–$1,745
4-bed+ house7–8.5$1,435–$1,7459–11$1,845–$2,255
Correction shipped over the spec's worked example. The concept spec sketched "3-bed, easy access ≈ 5–7 hrs ≈ $825–$1,155" — a range priced at the 2-mover $165/hr rate. But 33 m³ sits above the spec's own 30 m³ crew threshold, so a 3-bed is a 3-mover, $205/hr job: the measured range is $1,025–$1,435. Per the wave rule (illustrative ≈ figures are hypotheses, pinned prices are anchors), the measured figure ships and the hypothesis is corrected here. The 5–7 hour range itself was confirmed by the model. The cheapest possible output — 1-bed, easy access, low end — lands exactly on the services grid's $330 floor (2 hrs × $165), so the page never argues with itself.

The truck fill

A deterministic layout generator (seeded Mulberry32, seed 20441) packs the 8t/10t box interior column-by-column with cartons (kraft fills, tape-orange stripes, mono room labels), blanket-wrapped furniture (blanket blue with strap lines) and appliances. Each item carries a cumulative area fraction of the full load; rendering draws every item whose fraction ≤ volume ÷ box capacity, so the 33 m³ rest state shows a 10-tonne truck two-thirds loaded, and a 4-bed with piano and pool table loads to the door. Pianos and pool tables load first, against the headboard wall — as they do in real trucks — with the carton wall shifted behind them. Positioning lives on outer <g transform> groups and the pop-in animation on inner groups, per the W16 rule that CSS transforms silently discard SVG transform attributes. Axles are honest too: the 8-tonne draws one rear axle, the 10-tonne two.

Conversion decisions

Local SEO we'd implement for the real business

This demo page deliberately ships no LocalBusiness/Review structured data — fictional structured data in a real index is dishonest markup, so index.html carries only a WebPage schema naming it a demonstration. For a real client, we'd ship this (with their real details, licence and live review feed):

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "MovingCompany",
  "name": "Two Rivers Removals",
  "telephone": "+61 8 XXXX XXXX",
  "address": {
    "@type": "PostalAddress",
    "addressLocality": "Belmont",
    "addressRegion": "WA",
    "addressCountry": "AU"
  },
  "areaServed": "Perth Metropolitan Area",
  "openingHours": "Mo-Sa 06:30-18:00",
  "priceRange": "$$",
  "aggregateRating": {
    "@type": "AggregateRating",
    "ratingValue": "4.9",
    "reviewCount": "180"
  },
  "review": [{
    "@type": "Review",
    "author": { "@type": "Person", "name": "Real customer" },
    "reviewRating": { "@type": "Rating", "ratingValue": "5" },
    "reviewBody": "Pulled live from the Google Business Profile feed."
  }]
}
</script>

Plus: Google Business Profile with service areas and photo posts, suburb-level landing content for the highest-volume corridors, and review velocity via post-job SMS.

What would change for the real client

Techniques

Iteration log

Pass 1 — Craft

Headless drive (fresh-load empty submit, filled submit, all four home chips, all four extras, access toggle, FAQ, sticky bar) with every displayed figure asserted against an independently reimplemented oracle across five scenarios plus rest — 51 of 52 asserts green. What the screenshots caught that the asserts didn't: the truck load read sparse (15 chunky items at two-thirds fill), and at 375px the trust line sat clipped under the sticky bar even though the fold probe had passed marginally. Fixed by densifying the packing generator (narrower columns, lower skyline-break probability → 20 items at rest, 28 at 4-bed) and tightening the mobile hero: the redundant ghost call button is hidden ≤600px — header and sticky bar already carry call — which pulled the AFRA trust line above the fold.

Pass 2 — Depth

Added the second-read detail: flipping the access toggle swaps the truck's rear ramp for a drawn staircase and changes the scene caption to "STAIRS AT ONE END — SLOWER, SAID SO" — noticed only when you play with the toggle, and it makes the price difference feel earned. Asserted the SVG swap uses the attribute path with the [hidden]{display:none !important} guard (computed display checked, per W10/W13). Added keyboard drivability (Enter on a focused chip changes the probe) and a drive-then-diff liveness check. Re-ran the full oracle suite: 58/58.

Pass 3 — Hardening

Reduced-motion emulation renders the loaded truck instantly settled (all items at opacity 1, no rise stall); the t=7s thumbnail capture shows the truck loaded and the gauge held at 66% — the stagger completes by ~1.3s and holds, so the scheduled screenshot can never catch an empty frame. Keyboard focus shows the 3px blue outline on chips. 375px: scrollWidth exactly 375, sticky bar inside the bottom 140px, tel target ≥44px. Chanel rule: removed the two decorative ground dashes under the truck and an orphaned blue swoosh in the hero art. Final copy read caught a flex-gap space before a comma in the trust line ("2-hour minimum , stated") — restructured the markup. Zero console errors across every run.