Concept
True North Solar sells to the Perth homeowner watching a $700 quarterly bill, half-decided on a battery since the rebates, and wary of door-knockers. The design direction is the day curve — morning-sky clarity, engineering not evangelism, a page that shows its maths. Everything on the page argues one honest thesis: in Perth, with DEBS export credits at 2–10c against 31c grid power, self-consumption is where solar pays — so the system should be sized from your bill and your household pattern, not from a sales script.
Palette
The ink/line split (LEARNINGS W2/W5) is applied twice: panel gold and export green each get a darker derived ink for any run of text, while the bright originals are reserved for curves, fills, dots and buttons with navy type.
Type
Space Grotesk (500/600/700) for display — technical warmth, the confidence of a spec sheet rather than a brochure; its slightly squared counters suit a page full of kilowatt figures. Public Sans for body and UI — a workhorse that stays legible at 16px on a phone in sunlight. Every kWh and dollar is set with font-variant-numeric: tabular-nums so readouts don't jitter as the slider moves — the typographic bravery here is restraint plus the oversized bill figure in the control rail.
The signature: Day-Curve Saver
A quarterly-bill slider ($250–$1,500), three household-pattern chips and a battery toggle drive a two-part procedural SVG scene. Above, a north-facing roof fills with 440 W panels — 15 at 6.6 kW up to 30 at 13.2 kW — each panel a bilinear-interpolated quad on the roof plane with a white catch-light strip, drawn in with a 40 ms stagger. Below, a day-curve chart plots hourly generation (a cosine bell normalised to the system's daily yield) against the household's 24-bucket usage profile, and shades the honest bands: self-consumed (gold), exported (green), still-from-grid (navy), and — with the battery on — a hatched from-battery band that visibly eats the evening deficit. The curves are cosine-interpolated at six samples per hour purely for drawing; all figures come from the 24 hourly buckets.
The arithmetic is deliberately checkable: usage is inferred from the bill at ~31c/kWh less a ~$100/quarter supply charge; yield is 1,520 kWh per kW per year (a 6.6 kW array ≈ 10,032 kWh/yr, inside the quoted 9,500–10,500 band); DEBS credits are 10c in the 3–9pm window and 2c otherwise, applied hour-by-hour to whatever is actually exported; the battery is a 13.5 kWh store simulated across the day from 6am with 90% round-trip efficiency. Annual saving = self-consumption × 31c + battery discharge × 31c + hourly export credits, × 365. Payback is cost ÷ saving, presented as a ±15% range for orientation and shading. System cost interpolates from the services grid's own anchors — $3,900 at 6.6 kW, landing exactly on $7,900 at the first 10 kW+ configuration — so the saver and the price list can never argue with each other. At rest the saver sits mid-demonstration: $650 bill, out-weekdays, 6.6 kW, curve drawn, bands shaded, ≈$900/yr saving and a 3.8–5.1 yr payback showing. Live state is exposed at window.__solar.
Second-read details: the chart quietly marks the DEBS 3–9pm peak window; pushing the slider to the top earns a "roof full — 30 panels is our single-phase cap" note; and with the battery on, an honesty line computes the battery's marginal saving (≈$1,290/yr at the rest configuration) and admits it pays back slower than the panels.
Conversion decisions
- Above the fold at 375px: wordmark, name + value proposition as the single h1, primary CTA, and the three trust chips — no scroll needed. The saver is one swipe below, because the audience's objection is trust, not curiosity.
- Two CTAs, one job: "Book a free roof assessment" (commitment) and "Run your numbers first" (permission) — the second exists because the persona is door-knocker-burned; letting them self-serve the maths is the trust move.
- Saver → form handoff: configuring the saver pre-fills the "System you're interested in" field (e.g. "10.12 kW solar + 13.5 kWh battery — from the Day-Curve Saver"), and the rail's CTA scrolls to the form with focus on Name — the demo interaction becomes a warmer lead. A dirty-flag stops the prefill overwriting anything the visitor typed.
- Form fields: name, phone, suburb (required — the minimum a scheduler needs), bill bracket and roof notes (optional — qualify the lead without adding friction). Local validation with per-field messages and
aria-invalid; the confirmation renders locally, names the suburb and system, and says plainly that no enquiry was sent — never a fake network round-trip. - Sticky mobile bar: call + assessment, 56px tap targets — most solar research happens on a phone during the ad break.
- Honesty as positioning: "we don't door-knock", the export-limit FAQ, the blackout-backup caveat and the battery-payback admission are conversion copy here, not compliance — this trade's buyers convert on the absence of pressure.
Local SEO we'd implement for the real business
This demo ships only WebPage schema naming it a demonstration — fictional structured data in a real index would be dishonest markup. For a real installer we'd ship LocalBusiness + Review schema fed by their actual details and review stream:
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "LocalBusiness",
"@id": "https://truenorthsolar.example/#business",
"name": "True North Solar",
"description": "Solar and battery installation across Perth's northern suburbs.",
"telephone": "+61-8-XXXX-XXXX",
"address": { "@type": "PostalAddress", "addressLocality": "Balcatta",
"addressRegion": "WA", "addressCountry": "AU" },
"geo": { "@type": "GeoCoordinates", "latitude": -31.86, "longitude": 115.82 },
"areaServed": ["Balcatta","Stirling","Karrinyup","Joondalup","Wanneroo"],
"openingHoursSpecification": [{ "@type": "OpeningHoursSpecification",
"dayOfWeek": ["Monday","Tuesday","Wednesday","Thursday","Friday"],
"opens": "07:00", "closes": "16:00" }],
"aggregateRating": { "@type": "AggregateRating",
"ratingValue": "4.9", "reviewCount": "187" },
"review": [{ "@type": "Review",
"author": { "@type": "Person", "name": "…from the live review feed…" },
"reviewRating": { "@type": "Rating", "ratingValue": "5" },
"reviewBody": "…" }]
}
</script>
Plus: Google Business Profile with service-area suburbs, suburb-level landing sections fed from the one template, and review markup wired to the live feed (never hand-typed).
What would change for the real client
- Design & quoting: the Day-Curve Saver's assumptions get replaced by OpenSolar design-studio output — real roof imagery, shading analysis, per-panel layout — with the web saver kept as the top-of-funnel hook that hands off to a real proposal.
- Job management: enquiries land in Simpro or Tradify with the saver's configuration attached, triggering the assessment booking and SMS confirmation the demo only describes.
- Monitoring: post-install pages surface Fronius Solar.web, SolarEdge or Enphase monitoring, and the marketing site can quote real fleet-wide generation instead of a yield constant.
- Their photos, their review feed, their SAA/EC numbers, a real booking calendar, and analytics on the saver's slider (the bill distribution of your traffic is priceless sizing data).
Techniques
SVGVanilla JSCSS choreography Everything is procedural SVG and CSS — no libraries, no rasters. The roof panels are bilinear quads on a perspective plane; the chart is built as path strings from the shared 24-bucket model; the service-area map is hand-drawn coastline, river and freeway with a dashed service ring. Entrance choreography is IntersectionObserver + staggered CSS transitions gated on document.fonts.ready with a timeout fallback and a no-JS guard; prefers-reduced-motion renders everything settled and complete. SVG visibility (battery, legend entries) toggles via the hidden attribute — never the .hidden IDL property, which silently no-ops on SVG elements (LEARNINGS W13) — backed by the [hidden]{display:none !important} guard.
Iteration log
Pass 1 — Craft
Drove every control headlessly against an independent reimplementation of the arithmetic: rest state ($650 / out weekdays / no battery) asserted at 6.6 kW · 15 panels, $3,900 (equal to the services-grid floor), ≈$900/yr displayed saving and 3.8–5.1 yr payback — all matching the oracle to the dollar. Screenshot review then caught what the 45 green asserts couldn't: the header CTA wrapped to two lines at 375px and pushed scrollWidth to 389 (fixed with a short-label swap and a ≤430px header tighten), the booking bullet points split around an <em> because the flex li made every text node a flex item (wrapped in a span), the Dianella map label crowded Balcatta's HQ caption (nudged), and the Sapience band's buttons wrapped at 1440 (flex-basis fix). Also re-tuned the Dianella review's figures ($684 → $326) to match what the saver itself computes for that bill.
Pass 2 — Depth
Drove the battery path and the extremes: $940 / home / battery asserted at 10.12 kW · 23 panels with solar cost exactly $7,900 (the "10 kW+ from" anchor), $17,800 all-in and $3,480/yr — again equal to the hand-computed value; $1,500 / evening-heavy asserted the 30-panel cap and its "roof full" note. Added the second-read layer: the DEBS peak-window strip on the chart, the battery marginal-saving honesty line, and a value-tick micro-interaction on the readouts (a 0.4s settle animation that only fires when a figure actually changes, suppressed under reduced motion). Bumped all chart label sizes ~15% after the 375px screenshot showed them fading at phone scale.
Pass 3 — Hardening
Full-suite re-run: 47/47 — zero console errors on desktop and mobile, one h1 per page, 375px scrollWidth exactly 375 with wordmark/CTA/h1 inside the viewport box, above-fold h1 + ≥40px CTA + trust chips in the first mobile viewport, sticky call bar with 56px tap targets in the bottom 140px, empty submit raising three aria-invalid fields, filled submit rendering the local confirmation in the required lexicon, drive-then-diff pixel change on the saver, and the reduced-motion pass rendering all reveals and all 15 panels settled at full opacity. Honesty sweeps: every (08) number on the page uses the reserved 9000 exchange, JSON-LD is WebPage-only, colophon and demo band present. Chanel cut: removed the map's second dashed radius ring — ring + freeway + river was one ornament too many. Final screenshot read of every section, then this log was checked against the shipped constants.