MeridianBuild notes The site · Index

Generative Assets · site 436

A repair page that argues itself out of a sale, on purpose, in public.

Meridian Solar Service is a fictional Malaga repair outfit for solar systems somebody else installed. Its visitor owns an 8–16 year old system, has noticed the bill creeping up, and cannot get their original installer on the phone. They are not shopping for solar — they already have it, and they want to know whether it still works and whether fixing it is worth the money. The page's single job is to get that owner to book a $165 diagnostic without first sitting through a new-system pitch.

The proof element: “Repair or replace”

Two inputs, one plain result card, no roof drawn to scale. System size is five buttons — 1.5, 3, 5, 6.6 and 10 kW, the sizes Meridian actually repairs. Inverter age is an eleven-position slider, 6 to 16 years, the span the concept names as the working life of a 2010–2015 string inverter. The card prints the inverter replacement price against a like-for-like new system, at the same scale, in the same frame — W53's rule that the option the customer did not pick belongs beside the one they did, not in a paragraph underneath.

Price does not move with age — only the verdict does

Fifteen price cells (five sizes, two prices each, minus the shared new-system anchors) plus three verdict bands are the entire reachable state, and both are walked exhaustively rather than sampled: check-w56.mjs enumerates all 5×11 = 55 size/age combinations and the fifteen priced cells, and a browser-driven harness re-derives every one of them against the shipped page.

SizeInverter repairNew systemSaving
1.5 kW$1,2906.6 kW · $5,400$4,110
3 kW$1,2906.6 kW · $5,400$4,110
5 kW (rest state)$1,5906.6 kW · $5,400$3,810
6.6 kW$1,8506.6 kW · $5,400$3,550
10 kW$2,45010 kW · $7,900$5,450

Installed prices, GST included, small-scale certificate discount already applied — no STC value or certificate price is quoted anywhere on the page, per the wave's carve-out. The services grid's “inverter replacement from $1,290” is exactly the estimate's own floor, asserted rather than eyeballed.

Three verdict bands, walked, not sampled

verdict(size, age) is one nested comparison: 1.5 kW returns “put it toward a new system” at every age, because 1.5 kW repaired is still 1.5 kW — the page arguing itself out of a $1,290 job. Everything above 1.5 kW returns “replace the inverter, keep the panels” from 6 to 14 years, naming the exact dollar figure the business is turning down. At 15–16 years the verdict flips to “we’ll quote both,” because a panel warranty is usually shorter than a roof's working life. The harness walks all 55 cells and asserts the band counts directly: 11 cells return new system, 36 return keep the panels, 8 return quote both — a full partition, not a handful of worked examples.

The give-away, and where it sits

The saving runs $3,550 to $5,450 depending on size, and that range is printed under the chart before the visitor has asked — the same shape W52 found converts: publish the number a competitor would use to sell a new system, and say the size of it out loud. A subtle rust-coloured tick mark on the age slider itself, sitting exactly at the 15-year boundary, is the page's one second-read detail: a visitor who never drags the slider to the far end still sees, structurally, that the model has a hard edge there.

Palette — a reading, not sunshine

Solar yellow and orange are the default for this trade, and they are banned here by argument as well as by habit: the customer already has the sun, what they are missing is a measurement. The palette reads instead as an instrument — paper and a shaded panel, a near-black “underside of a panel” band, and one export-meter green. Every ratio below is read from the same row of contrast-w56.log as the hex beside it, not computed by hand.

Aa
paper #F7F8F6ground · ink 16.6:1
Aa
panel #E8EBE6cards · ink 14.7:1
Aa
band #1A2430underside of a panel · paperOnDark 14.72:1
Aa
bandHover #101823pressed dark · paperOnDark 16.75:1
Aa
exportGreen #10715Aevery CTA · paperOnDark 5.58:1
Aa
exportGreenHover #0B5344pressed · paperOnDark 8.44:1
Aa
exportInk #0E6853links, eyebrows · 6.31:1
Aa
muted #4B545Csecondary copy · 7.24:1
Aa
rustDerate #9B3A16the derating state, nothing else · 6.55:1
Aa
mapLand #ECEEEAmap · label 15.14:1
Aa
mapWater #C8D6DCocean · label 11.88:1

exportGreen and markGreen share one hex, #10715A, but never one job. The log declares them as two roles: exportGreen is a GROUND, licensed only to carry paperOnDark text (5.58:1 on paper's raw value, measured on the CTA fill), while markGreen is a MARK — a 3:1 threshold for non-text strokes and fills — licensed on paper, panel, mapLand and mapWater, but explicitly forbidden on band (2.64:1). That distinction caught a real defect during the build: the proof chart's first draft drew its hatched “gap” band directly on top of the dark new-system bar, putting a markGreen stroke on a forbidden ground. The fix was structural, not chromatic — the hatch now extends the SHORT (repair) bar's own row into empty paper, the same trick the collection's stone benchtop page uses to show a gap between two bars without ever painting one colour on top of another.

rustDerate is scoped tightly on purpose: it is the age slider's warranty-edge hint, the verdict card's left border when the panels are near end of warranty, and the invalid-field colour on the form — and it never appears on band, mapLand or either export-green ground, all of which the log marks forbidden for it. It carries one meaning throughout: something is ageing out.

Type

Archivo at 700/800, tight tracking, carries every heading — technical and a little cold, which is the point; this is not a warm trade. Source Sans 3 is the 17px workhorse. Roboto Mono at 500 is reserved for numerals only — every price, every age, every saving is set in it, so the result card reads like an instrument display rather than a marketing claim. It is also why “5 kW” in the headline sits a touch wider than the surrounding prose: tabular monospace figures are meant to look measured, not typeset.

Techniques

Two files, no libraries, no build step, no raster images, nothing beyond the Google Fonts stylesheet. The site page is roughly 53 KB with every byte of CSS and JavaScript inline.

  • The comparison chart is a static inline SVG with two bars and a hatched gap; JavaScript only ever mutates attributes on nodes that already exist in the markup, never inserts new ones — the pattern that keeps a scene reliably paintable under a headless renderer for the thumbnail contract.
  • Bar geometry is normalised to the new-system price, which is always the larger of the two figures across all fifteen cells (the smallest new-system anchor, $5,400, exceeds the largest inverter repair, $2,450) — so the “repair” bar never needs special-casing to stay inside the track.
  • The bar tween is wall-clock only. A single requestAnimationFrame loop reads performance.now() against a start timestamp, with no per-frame accumulator, so a slow frame cannot leave the drawing mid-tween at the 7-second thumbnail instant. A cold-load flag stops a resize from re-entering the entrance.
  • One ambient animation. The scanline under the card's heading is a CSS mask sliding one 36px period every 3.1 seconds — the sweep of a diagnostic tool, not a decoration — and it is the only thing moving at rest. prefers-reduced-motion stops it in CSS and matchMedia makes the bars paint at their settled width with no tween at all.
  • Radio-style chips use role="radio" and aria-checked, and the CSS selector has to match that attribute, not aria-pressed. The first draft styled .opt[aria-pressed=true] against markup that only ever sets aria-checked — a selector that is syntactically perfect and semantically dead, the same family as W52's .lbl.opt bug and W54's SVG hidden IDL property. Every state assert stayed green because the underlying state (state.size, the arithmetic) was always correct; only the paint was missing. Caught by looking at the rendered thumbnail, where none of the five size chips was ever highlighted, not by any assert.
  • The service-area map is a hand-drawn SVG: a coastline path, the extent as a dashed ellipse in markGreen, and eleven suburb marks around the Malaga base. Below 700px five secondary labels drop and the rest scale up; every suburb is also named in running text under the map.
  • The form validates in the browser and confirms locally. No fetch, no fake latency. The phone field's pattern escapes (, ) and - inside the character class — unescaped, the attribute is invalid under the RegExp v flag Chrome compiles it with, and a v-invalid pattern is silently discarded rather than rejected, so the field would accept anything with no visible sign of failure.

Three iteration passes

Pass 1 — Craft

Driven, not read: the size chips clicked through all five values, the age slider dragged to both ends, the form submitted empty and filled.

  • The selected-chip highlight never painted. CSS targeted [aria-pressed=true]; the markup and JavaScript only ever set aria-checked. The model was correct throughout — every price updated on click — but no chip ever visibly showed as selected. Fixed by matching the selector to the attribute actually in use, and re-verified by screenshotting each of the five states.
  • The hero column and the estimate card fought each other for height. The proof card is naturally taller than the headline and CTA row beside it, leaving a slab of dead paper under the trust line at desktop widths. Filled with a differentiator panel — “almost every ‘solar repairs’ page in Perth belongs to an installer” — which does two jobs at once: it balances the grid and it puts the page's actual competitive argument above the fold instead of leaving it for the why-us section six scrolls down.
  • The masthead's CTA button measured 39px tall against the checklist's 44px tap-target floor — padding tuned for a dense header before the floor was checked. Given min-height:44px and switched to an inline-flex box so the text centres inside it rather than growing the button unevenly.
  • Copy read end to end for Australian English and for the banned marketing words; none appear. Every claim is either a price the model computes or something the business actually does.

Pass 2 — Depth

  • The hard edge in the model got a visual home. The verdict flips from “keep the panels” to “quote both” at exactly age 15, and nothing on the slider showed that before this pass. A short rust-coloured tick now sits on the track at the 15-year position — a structural cue a careful visitor notices on a second look, not a label competing with the live age readout.
  • A dead ornament removed. The chart carried a fourth text row repeating the verdict headline inside the SVG, in a class hidden below 640px anyway, duplicating the HTML verdict panel directly beneath it word for word. Deleted, along with the JavaScript that filled it, and the chart's viewBox height dropped from 200 to a fitted 192 once the row was gone — then grown back to 192 in the next pass for an unrelated reason (below).
  • The headline's own size was hard-coded. “This 5 kW system” never changed when a different chip was clicked — the size word was static text beside two live spans (price, comparison price). Selecting 1.5 kW still read “this 5 kW system,” describing the wrong product entirely. Wrapped in its own span and bound to sizeLabel, then re-driven across all five sizes and asserted against the visible text.
  • Micro-interaction. The five size chips get a small scale(.97) on :active, matching the existing 1px press on the primary buttons — tactile feedback on the control a visitor taps most.

Pass 3 — Hardening

  • A real contrast violation, found by checking paint order rather than colour values. The hatched “gap” rectangle was drawn at the same vertical position as the dark new-system bar, so its markGreen stroke painted directly on top of band — a pairing contrast-w56.log explicitly marks FORBIDDEN (2.64:1) regardless of the numbers, because a mark-grade stroke on that ground simply is not legible. The fix moved the ghost rectangle onto the short (repair) bar's own row, where it extends into blank paper instead of overlapping the dark bar — visually clearer besides, since the hatch now reads as “how far the repair bar would have to grow to match the new system,” on the row where that comparison actually lives. The viewBox was grown at the same time (192 units) so the visible chart clears the thumbnail contract's 120px-in-frame rule with margin, verified by measuring the rendered bounding box of every drawn rect rather than eyeballing it.
  • 375px pass. No horizontal overflow (scrollWidth 375), the h1, primary CTA and trust line all finish above the sticky bar (bar top 608px in a 667px viewport), and the chart's mobile media query re-sets its type in user-space units so 13px SVG text does not shrink to 8 rendered pixels at a 0.7 scale factor.
  • Reduced motion confirmed by emulation: the scanline's animation duration collapses to effectively zero and the bars paint at their settled width on the first frame, never tweened from empty.
  • Rest-after-drive and rest-after-resize. The chart's own clipped screenshot was measured before and after driving every control and after a viewport resize to 1000px; the byte size never collapsed, meaning the scene stays visible rather than re-entering an empty entrance state.
  • The green was tested, not believed. The state probe's numeric figures were captured at the 7-second thumbnail instant and again eight seconds later; the JSON was byte-identical both times, and the model was driven through all seven of the wave oracle's named cells (the five size/age combinations enumerated in check-w56.mjs plus both quote-both edge cases) with every output — inverter price, comparison price, saving, verdict — asserted against the independently written oracle.

Why this converts

Written for the business owner deciding whether to commission one of these

The primary CTA reads “Book a $165 diagnostic” rather than “get a quote,” because the visitor already knows a diagnostic costs money and a vague quote invites a phone call they are trying to avoid; it sits in the header, in the hero, and again at the foot of the estimate card, so the reader is never more than a scroll from acting on whatever they just read. The enquiry form asks for five things only — name, phone, suburb, system size, inverter age and one line on what is happening — because every field beyond that is a lead who closed the tab instead, and the size and age the visitor already chose in the estimator travel with the form as a pre-selected default so the two never contradict each other. The proof element is the argument, not a lead magnet bolted onto one: it prices the option Meridian would rather sell (a new system) beside the option it is recommending instead, and states in whole dollars, before any contact details are requested, exactly how much the business is turning down. The trust strip repurposes a real statistic — Western Australia's 49.46% rooftop solar penetration, the highest of any major grid in the world — not as decoration but as the reason this trade exists at all: a market that large has thousands of installers who have since closed. And the diagnostic's own price, $165 credited against any repair over $500, is stated three times in three different words, because a repair trade's biggest objection is not the number, it is the fear that the number is a foot in the door.

Local SEO we would implement for the real business

This demonstration ships WebPage and CreativeWork schema only — a fictional business carrying LocalBusiness and fabricated reviews would be dishonest structured data in a real index. For an actual solar repair operator, with their own ABN, address, phone number and reviews they hold, we would ship this instead:

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "ElectricianService",
  "@id": "https://example.com.au/#business",
  "name": "Meridian Solar Service",
  "description": "Solar system repair, diagnostics and inverter replacement for systems installed by any provider, across the Perth metropolitan area.",
  "url": "https://example.com.au/",
  "telephone": "+61 8 0000 0000",
  "priceRange": "$$",
  "image": "https://example.com.au/img/van.jpg",
  "address": {
    "@type": "PostalAddress",
    "streetAddress": "<the depot's own address>",
    "addressLocality": "Malaga",
    "addressRegion": "WA",
    "postalCode": "0000",
    "addressCountry": "AU"
  },
  "geo": { "@type": "GeoCoordinates", "latitude": -31.8720, "longitude": 115.9200 },
  "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" }
  ],
  "areaServed": [
    { "@type": "City", "name": "Perth" },
    { "@type": "AdministrativeArea", "name": "Perth metropolitan area, Western Australia" }
  ],
  "hasOfferCatalog": {
    "@type": "OfferCatalog",
    "name": "Solar repair services",
    "itemListElement": [
      { "@type": "Offer",
        "itemOffered": { "@type": "Service", "name": "Solar diagnostic call-out" },
        "priceSpecification": {
          "@type": "PriceSpecification",
          "price": "165.00", "priceCurrency": "AUD"
        } }
    ]
  },
  "aggregateRating": {
    "@type": "AggregateRating",
    "ratingValue": "4.9", "reviewCount": "87"
  },
  "review": [
    { "@type": "Review",
      "author": { "@type": "Person", "name": "A real, consenting customer" },
      "datePublished": "2026-08-20",
      "reviewRating": { "@type": "Rating", "ratingValue": "5", "bestRating": "5" },
      "reviewBody": "Found the blown isolator our old installer never would have answered the phone about." }
  ]
}
</script>

Two conditions we would hold the real client to. aggregateRating and review must be pulled from a review platform the business genuinely holds, never typed into the template — search engines penalise fabricated review markup, and a repair trade's reputation for honesty is the entire asset this page sells. And the priceSpecification on the diagnostic call-out only ships if the client commits to keeping it current, because a stale price in structured data is worse than none: it is the number that surfaces in the result.

What would change for a real client

  • Photography. Every visual here is procedural, which is this collection's constraint. A real repair business's page leads with photos of the actual van, the actual technician on an actual roof, and before/after inverter photos — the drawn chart stays, because it does a comparison job a photo cannot, but it sits beside real photography rather than instead of it.
  • The enquiry goes to a real job system. The form currently confirms locally with no network call. A real build posts straight into ServiceM8 or Fergus — both are the standard job-management platforms for small electrical and solar trades in WA — with the size, age and verdict from the estimator pre-filling the job card, so the office is looking at the same numbers the customer saw.
  • The diagnostic books a real slot. A live calendar view from inside ServiceM8/Fergus, or a scheduler like Calendly wired to the same availability, turns “we'll ring you back” into a specific time on the page — a measurably stronger conversion than a callback promise, and the natural next step once the enquiry is actually landing somewhere.
  • Reviews come from a feed, pulled live from Google Business Profile rather than written for the demo — the same reviews that back the Review schema above.
  • The rate card and the age/derate assumptions get reviewed annually. Inverter prices move with the market and the “10 to 14 years, 10–15% derate” figures are industry generalisations, not Meridian's own measured data; a real business would tune both against its actual job history rather than leave them as launch-day constants.
  • Analytics on the estimator itself, not just the page: which size and age combinations visitors actually enter is a live signal for which fleet of ageing systems is calling, and it is close to free to capture once the tool exists.