Build notes · Generative Assets 400

How the Padstone Crushing page was made

The Deck: a side elevation of a mobile crushing spread drawn at true scale, with three stockpiles whose geometry, tonnage and price all come off one set of pinned anchors — and the three places where an independent oracle disagreed with the brief.

01The concept

Padstone Crushing is a fictional mobile crushing and screening contractor out of Kwinana. It brings a 40-tonne jaw and a two-deck screen to the demolition site so the slab, the footings and the brick become road base and drainage aggregate on the customer’s own pad — instead of being carted to a tip and product carted back. The audience is the demolition or civil contractor holding a cube figure and a cartage quote.

The page has one job: turn “what would it cost to crush this pile” into a booking, with the tonnage, the products and the price already on screen before anyone picks up a phone. Everything else on the page — the rates, the trust strip, the map, the FAQ — exists to make the number on the Deck believable.

02Palette

Argued from the three things that actually come off the screen: grey crushed concrete road base, blue-metal drainage aggregate, and the red of brick rubble. Every ratio below was computed with WCAG arithmetic from the hex values as shipped in the page’s :root block (35 rows, 0 failures) — not estimated, and not copied from the concept.

Crusher-dust paper#E4E2DC · page groundWarm grey with no cream in it. The whole site is the colour of the dust on a bonnet at the end of a campaign.
Basalt ink#1C1A17 · 13.40:1 paper · 17.36:1 card · 10.39:1 on the hairline tintBody ink and the primary button fill under white labels (17.36:1; hover #0E0D0B at 19.43:1).
Blue-metal slate#3D5560 · 6.08:1 paper · 7.87:1 card · 6.61:1 tintPrimary accent: the plant outline, the pile outlines, the survey grid, the dimensions. Clears body copy unminted, so it does text duty too — no second ink needed.
Brick red#8F4522 · 5.33:1 paper · 6.90:1 card · 5.79:1 tintThe brick-rubble product, the oversize-return flow line, and the levy declaration. Second accent, never left alone as a terracotta.
Plant steel#525A5F · basalt ink measures 2.47:1 on it — never usedMachinery labels are reversed out in #F2F1ED at 6.22:1. This is a pinned rule: the basalt ink never touches the plant body.
Road base#B7B3AA · ink 8.30:1Stockpile one. Recycled concrete minus-20.
Drainage aggregate#93A5AD · ink 6.80:1Stockpile two. The blue-metal note in the palette, carried into the drawing.
Rock armour#8E8C84 · ink 5.15:1Stockpile three. Lightened at spec time from #807E77, which measured 4.27:1 and failed the body ink.
Brick rubble#BC8468 · ink 5.49:1The feed pile when the material chip is brick and masonry.
Pad#D2CFC7 · ink 11.15:1 · slate 5.06:1The title strip under the ground line, where the 12.6 m dimension and the feed caption live.

Two things this palette is deliberately not: it is not cream-plus-serif-plus-terracotta (there is no cream, the display face is a grotesque, and the red is second behind a slate), and it is not near-black-plus-one-acid-accent (it is a light site with two argued accents and no acid anywhere).

03Type

Archivo Black carries the display — its single heavy weight is the point, because every sign on a crushing job is stencilled as fat as it will fit. Onest at 400/500/600 is the workhorse. Chivo Mono at 500 appears in exactly one role: quantities and rates — 1,800 t · 1,200 m³ · $16.50/t — so a number always looks like a number and never like prose.

The typographic moment is the rest state’s tonnage set enormous over the pad: 1,800 t at display scale with OUT OF 1,200 M³ OF MIXED DEMOLITION beneath it in mono at 0.235em tracking, and the price and days stamped beside it. That figure is derived from the live model on every render, so it can never drift from the badge on the drawing.

04The signature — the Deck

One inline SVG, no libraries. No three.js, no GSAP, no canvas: a measured technical drawing is better served by geometry you can label, dimension and assert than by a scene you can only look at. Every element is authored in the markup and only ever updated by attribute — nothing is inserted into the scene after the animation loop owns it, which is the one construction that reliably paints in headless Chrome.

Cone geometry at a true 37° angle of repose

Each stockpile is a real cone. Given a repose angle θ and a volume V, the cone’s height is h = ∛(3·V·tan²θ ÷ π) and its base radius is r = h ÷ tanθ; the drawing shows the cone’s central section, a triangle with base 2r and apex h. Tonnage converts to volume through a stockpiled-product bulk density of 1.50 t/m³. Both pinned endpoints fall straight out of it: the 13 t oversize part of a 200 m³ brick job draws 1.7 m high and 4.4 m across, and a 3,780 t armour pile draws 11.1 m high and 29.5 m across.

The fan, and the view-fit

Three cones dropped from three conveyor heads overlap at the toe, the way they do on a real pad. Apex spacing is OV × (rᵢ + rⱼ) and the plant’s discharge conveyor overhangs the first pile’s toe, so the sheet width is 2·lip + A + fieldWidth, clamped to the pinned 26–60 m frame. Two unknowns, two pinned endpoints: solving content(smallest) = 26 and content(largest) = 60 gives OV = 0.4768 and A = 10.2443 m (a 2.36 m conveyor overhang, which is what a discharge head actually reaches). The 12.6 m plant never changes size against the scale — it is the honest constant reference mark, and at the 60 m frame it is 21% of the sheet.

The split, and why it always adds up

The two screen apertures set the product split. Each part is floor(tonnes × pc ÷ 100) and the rounding remainder is added to the largest part, so the three products sum to the feed tonnage exactly in all 3,024 grid cells — no cell where 1,224 + 432 + 143 quietly loses a tonne. On the armour setting the oversize stops being a return and becomes a product: the third stockpile’s label changes from OVERSIZE to ROCK ARMOUR, the ledger name changes from “Oversize +40 mm — back to the jaw” to “Rock armour & gabion stone +100 mm”, and the brick-red return flow line to the hopper disappears.

Money in integer cents

Every rate is held as an integer number of cents and divided by 100 once, at display — 400 × 4.6 is 1839.9999999999998 in IEEE doubles, and a naive toFixed hides that until a later comparison fails. Density is held in hundredths of a tonne per cubic metre for the same reason. The dollars() helper throws if it is ever handed a cents value that is not a whole dollar, so an accidental float would take the page down loudly instead of printing a wrong number quietly.

Composition rules the drawing has to keep

Motion

Entrance choreography is gated on a cold-load firstDraw flag with a settle-timer backstop, and a resize can never re-enter it. That matters more than it sounds: taking a clipped headless screenshot changes device metrics and therefore fires resize, so an entrance that replays on every redraw photographs an empty pad while every state assert stays green. State changes redraw at 430 ms against the entrance’s 1,150 ms. The flow lines animate their dash offset continuously with dt = min(max(now − last, 0)/1000, 0.05) — clamped at zero as well as capped, because a rAF timestamp can precede the performance.now() that scheduled it and a negative delta drives geometry negative. Under prefers-reduced-motion the loop never starts and the settled frame draws immediately.

05Three places the brief and the arithmetic disagreed

Every figure on this page was re-derived from the pinned anchors in a separate oracle script written from the brief’s text — not from the brief’s answers, and not from the page. That script enumerates the full 3,024-cell control grid: every price, every day count, every product split, every cone, every frame width, the minimum-charge binding cells and the force/unwind chain. It agrees with the brief on all twenty-two pinned money figures and all four worked examples. It disagrees in three places, all of them on the drawing side. In each case the page ships the reading that reproduces the pinned endpoints, and the disagreement is written down here rather than quietly resolved.

1 · “Area proportional to tonnage” cannot produce the pinned pile heights

The brief specifies the stockpiles as “true cone sections at a 37° angle of repose with area proportional to tonnage”, and separately pins two endpoints: a 13 t pile draws 1.7 m high, and a 3,780 t pile draws 11.1 m high.

Those two claims are incompatible. If the drawn triangle’s area scales with tonnage then h ∝ √t, and 290.8× the tonnage is 17.05× the height — the 13 t pile’s 1.7 m would become 28.6 m, not 11.1 m. The pinned heights are a cube-root relationship: 290.81/3 = 6.62, and 11.1 ÷ 1.675 = 6.63. The endpoints describe a cone whose volume is proportional to tonnage — which is also the physically correct statement, since a stockpile’s volume is its tonnage over its bulk density.

Shipped: cone-volume-proportional. h = ∛(3·(t ÷ 1.5)·tan²37° ÷ π) reproduces both pinned endpoints to the printed decimal (1.675 m → 1.7; 11.096 m → 11.1). The drawn cross-section’s area then scales as t2/3, which is what “area proportional to tonnage” should have said. The prose is wrong; the numbers are right.

2 · The largest stockpile in the grid is 4,608 t, not 3,780 t

The brief argues there is no drawn cap because “the largest single stockpile in the grid is 3,780 t (a 4,000 m³ rock job on the armour setting) at 11.1 m high and 29.4 m across”. Enumerating all 3,024 cells, that is not the maximum. A 4,000 m³ concrete job on the road base setting yields 6,400 t of feed and takes 72% of it as minus-20: 4,608 t, which draws 11.9 m high and 31.5 m across. Rock tops out at 3,780 t only because the armour split gives its biggest share (54%) to a job whose 4,000 m³ is 7,000 t rather than concrete’s 6,400 t at 72%.

The brief’s conclusion survives: 4,608 t still fits its own frame comfortably (31.5 m of pile inside a 55.4 m sheet), so no drawn cap is needed. Only the identification of the maximum was wrong, and the 3,780 t / 11.1 m / 29.4 m figures it quotes are themselves correct for the rock-armour cell.

Shipped: no drawn cap, as pinned — and the harness asserts both cells: that the true maximum is 4,608 t at concrete/4,000 m³/road base, and that it is narrower than its own frame. The 3,780 t rock pile is asserted at 11.1 m as a second, separate check.

3 · Two quantities the drawing needs that the brief never states

The stockpiled bulk density. The brief pins the material densities of the feed (1.60 / 1.35 / 1.50 / 1.75 t/m³) and pins the drawn heights, but never names the density that converts a product tonnage back into a drawn cone. It cannot be the material’s own density: at rock’s 1.75 the 3,780 t pile draws 10.5 m, not 11.1 m. Solving from the pinned endpoints gives 1.50 t/m³ uniquely, which is a sensible loose density for screened crushed product and is what the page uses for all three products on every chip.

The width rounding. The brief prints the 3,780 t pile as “29.4 m across”. The exact figure is 29.451 m, which rounds to 29.5 and truncates to 29.4 — while the same brief prints its height as 11.1 m, which is the rounded form of 11.096 (truncation would give 11.0). One figure is truncated and its neighbour is rounded.

Shipped: a fixed 1.50 t/m³ stockpile density, documented here because it is a derived constant rather than a pinned one; and standard rounding for every displayed dimension, so the page prints 29.5 m where the brief prints 29.4 m. The 0.05 m difference is a rounding convention, not a geometry difference — the underlying value is identical.

Two smaller notes, not disagreements

06The price model

tonnes   = floor(volume × densityCenti ÷ 100)
subtotal = $4,850  mobilisation & demobilisation
         + tonnes × rate            [$16.50 · $14.00 · $14.00 · $21.00]
         + tonnes × $3.20           if our excavator feeds the jaw
         + $1,900                   if the water cart is on
billed   = max(subtotal, $9,800)    minimum campaign
total    = billed + zoneFlat        [$0 · $2,400 · quoted]

Mobilisation, the per-tonne rate, the feed line and the water cart all sit inside the $9,800 minimum. The zone float sits outside it, because it is a separate low-loader movement rather than campaign time. The minimum is a dollar floor, not a tonnage floor: the per-tonne lines always bill the actual tonnage, and the ledger says so on the row.

Material anchors and worked examples
MaterialLoose densityRateThroughputPer 6.5-hr day
Concrete slab & footings1.60 t/m³$16.50/t120 t/h780 t
Brick & masonry rubble1.35 t/m³$14.00/t140 t/h910 t
Mixed demolition (rest state)1.50 t/m³$14.00/t130 t/h845 t
Limestone & rock1.75 t/m³$21.00/t95 t/h617 t
Worked examples, each with its full control vector
Control vectorTonnesDaysTotalProducts
mixed · 1,200 m³ · road base · your excavator · suburban (cart forced) · metro — rest state1,800 t3$31,9501,224 / 432 / 144
brick · 200 m³ · road base · your excavator · industrial pad · metro · no cart270 t1$9,800 min binds, subtotal $8,630217 / 40 / 13
concrete · 800 m³ · drainage · our machine · suburban (cart forced) · metro1,280 t2$31,966576 / 602 / 102
rock · 4,000 m³ · armour · our machine · industrial pad (cart forced by material) · Peel7,000 t12$178,5501,680 / 1,540 / 3,780
rest vector · beyond Peel1,800 t3$31,950 + quoted1,224 / 432 / 144

The cheapest reachable output on the whole grid is $9,800, which is exactly the “$9,800 minimum campaign” anchor printed in the services grid — the estimator and the rate card cannot disagree. The dearest non-quoted cell is $178,550. Across all 3,024 cells there are zero non-integer cent values and every total is a whole dollar by construction.

The forced water cart, and the partial unwind

Two different controls can force the water cart on: a suburban block (“a crusher on a suburban block runs behind a water cart — that’s not an upsell, it’s the condition we work under”) and limestone & rock (“limestone makes dust on any site”). Where both apply, the suburban string wins and exactly one note prints — never two. The toggle’s pre-force value is remembered, and an explicit user change while it is free clears the memory.

The interesting case is the partial unwind, driven in every pass:

Holding rather than restoring on a partial unwind is the pinned semantic, and it is the one a naive implementation gets wrong — lifting the site constraint alone would flick the cart off on a limestone job.

What the page refuses to quote

The declaration “Tip-levy savings not included — that’s your weighbridge dockets, not our number” appears wherever the total does: the hero stamp, the badge on the drawing, the ledger, the plain-text mirror and the services note. Crushing on site genuinely avoids landfill levy under the Waste Avoidance and Resource Recovery Levy Act 2007 (WA), and putting a dollar figure on that saving would be the single easiest lie on the page — it depends on the customer’s tip, their material classification and their own dockets. Beyond Peel, the float is the other refusal: the badge prints $31,950 + the float out — quoted rather than a bare “quoted”, so the computable part stays visible.

07Conversion decisions

Why the number leads at desktop and the button leads on a phone

The hero column is a flex column whose children are re-ordered by breakpoint. On a phone the primary CTA sits immediately under the lede, because a tradesperson holding a phone in one hand needs the button before the argument. Above 1000 px the display figure moves up between the lede and the CTA — which also lands the rest-state total inside the first 750 px, the band the collection’s thumbnail captures. Same markup, same DOM order for a screen reader, two different reading orders.

Why the price is on the drawing

A ledger below the fold does not make the screenshot. The badge on the sheet carries the total, the tonnage, the days, the setting, the material, the split, the forced-cart note and the levy declaration — and it is the badge’s measured height that sets the drawing’s height, so it can never end up covering a stockpile. Below 640 px it docks directly beneath the artwork, still inside the signature’s container, and switches to the pinned short forms (3 days · road base · mixed, 68/24/8 split, Levy savings not quoted.). The ledger and the plain-text mirror keep the full templates at every width.

Why these form fields

Name, phone, suburb, what’s in the pile, roughly how many cubic metres, when, and a line about the pad. Volume and material are the only two inputs that actually move the price, so they are the two the Deck can hand over: “Send these numbers to the form” fills the material, the volume and a plain-English scenario note, then scrolls. Suburb is required because it sets the float. The date is optional because a demolition programme rarely has one yet, and making it required would cost enquiries.

The estimator does not pre-fill the form on load. That keeps the empty-submit path honest — an empty submit raises validation on every required field and the confirmation panel stays hidden, which is exactly what the panel’s contract says.

Two details that are deliberately not upsells

08Local SEO we’d implement for the real business

This page ships no business-shaped structured data. Its live JSON-LD is WebPage, CreativeWork, WebSite and a BreadcrumbList — nothing that would put a fictional contractor into a real index. For a real client we would ship the schema below, with their verified NAP, their real licence and works-approval numbers and their real reviews.

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "GeneralContractor",
  "name": "Padstone Crushing",
  "description": "Mobile crushing and screening on site — recycled road base and drainage aggregate from demolition concrete, brick and limestone.",
  "url": "https://padstonecrushing.com.au/",
  "telephone": "+61 8 9000 0400",
  "priceRange": "$$",
  "address": {
    "@type": "PostalAddress",
    "streetAddress": "Unit 4, 12 Example Road",
    "addressLocality": "Kwinana",
    "addressRegion": "WA",
    "postalCode": "6167",
    "addressCountry": "AU"
  },
  "geo": { "@type": "GeoCoordinates", "latitude": -32.24, "longitude": 115.83 },
  "openingHoursSpecification": [{
    "@type": "OpeningHoursSpecification",
    "dayOfWeek": ["Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],
    "opens": "06:00", "closes": "18:00"
  }],
  "areaServed": [
    { "@type": "City", "name": "Perth" },
    { "@type": "City", "name": "Rockingham" },
    { "@type": "City", "name": "Mandurah" },
    { "@type": "City", "name": "Bunbury" }
  ],
  "hasOfferCatalog": {
    "@type": "OfferCatalog",
    "name": "Crushing and screening",
    "itemListElement": [
      { "@type": "Offer", "name": "Mobilisation and demobilisation",
        "priceSpecification": { "@type": "PriceSpecification", "price": "4850", "priceCurrency": "AUD" } },
      { "@type": "Offer", "name": "Crushing and screening — concrete",
        "priceSpecification": { "@type": "UnitPriceSpecification", "price": "16.50",
          "priceCurrency": "AUD", "unitCode": "TNE" } }
    ]
  },
  "aggregateRating": { "@type": "AggregateRating", "ratingValue": "4.9", "reviewCount": "38" },
  "review": [{
    "@type": "Review",
    "author": { "@type": "Person", "name": "Verified customer name" },
    "reviewRating": { "@type": "Rating", "ratingValue": "5" },
    "reviewBody": "Twelve hundred cube of slab and none of it left the block."
  }]
}
</script>

Alongside it: a verified Google Business Profile with the yard’s real coordinates and service-area radius, per-suburb landing pages only where there is genuine local intent (“concrete crushing Kwinana”, “recycled road base Mandurah”) rather than a doorway page for every suburb, real photographs of real campaigns, and the reviews pulled from the profile rather than typed into the HTML.

09The three iteration passes

Every pass drove the interactive elements and asserted output — an empty submit and a filled submit clicking button[type=submit] specifically, every chip, both stepper directions across the whole range, the water-cart toggle, the full force/unwind chain, and one rapid-fire drive with no settle between clicks. Loading the page is not driving it. The harness ends at 323 assertions, checked against an oracle reimplemented from the brief’s anchors in separate code, plus a standalone 56-check spec oracle and a 35-row contrast script that reads the tokens out of the shipped stylesheet.

Pass 1 Craft

  • Measured the palette from the shipped tokens, not the concept’s numbers: 35 rows parsed out of the page’s own :root block, 0 failures. Every figure matched the brief exactly, including the two that needed care — rock armour at 5.15:1 (lightened at spec time from a 4.27:1 grey) and the plant steel, where the basalt ink measures 2.47:1 and is therefore never used; machinery labels are reversed out in #F2F1ED at 6.22:1. Added stroke:none to the plant labels so the new steel outline could not bleed into them.
  • Screenshot review caught what the asserts could not. The first render buried the plant: the feed pile was drawn at true scale with its apex on the sheet, so a 23 m mound sat squarely over a 12.6 m machine and the reference mark — the whole point of the drawing — was invisible. Re-anchored the feed pile with its apex off the sheet so it reads as a slope running in behind the plant.
  • The water cart’s on-scene label collided with the OVERSIZE tonnage label. Removed the cart’s own label (the title strip and the ledger both name it), moved the cart into the gap between the conveyor head and the first stockpile, and scaled it to 0.88.
  • Hero ordering: the display figure originally sat after the CTA at every width, which put the rest total at y = 829 at 1200 wide — outside the first 750 px. Re-ordered the hero column by breakpoint (figure fourth above 1000 px, sixth below) so the phone keeps its button first and the desktop lands the total at ~478 px.
  • Model strings carried HTML entities (&amp;) into textContent paths. Moved every string to plain text and added an esc() helper on the two innerHTML builders instead.
  • Rate display was collapsing $14.00 to $14. Split rate() (always two decimals, from integer cents) from dollars(), and made dollars() throw on any non-whole-dollar value.

Pass 2 Depth

  • A 5 m survey grid behind the drawing, snapped to the ground line and stepping to 10 m once the frame opens past 42 m, so the mesh never gets busier than the drawing it sits under. It turns the sheet’s empty air into a scale reference instead of a gap, and it is a <pattern> whose size is set by attribute — no per-frame DOM churn.
  • Slate hairlines on the plant and on every pile. Road base #B7B3AA against the mixed feed #A9A49B is a 12% lightness step; without an outline the feed pile and the first stockpile read as one mountain.
  • Second-read detail: the three flow lines from the conveyor heads carry a stroke width proportional to that product’s share of the feed, so the road-base setting visibly throws most of the material at one pile and the armour setting redistributes it. And on any non-armour setting a brick-red dashed line arcs from the oversize stockpile back to the hopper — the oversize returning through the jaw. Switch to the armour setting and that line disappears, because the oversize has become a product.
  • Motion timing: entrance 1,150 ms with an ease-out cubic, subsequent redraws 430 ms; the flow dashes run continuously at 1.55 units/s with the delta clamped at both ends. The scene never draws blank — cold load paints at 14% of target immediately and grows from there, gated on document.fonts.ready with a 380 ms fallback because font metrics drive the label-collision sweep.
  • Micro-interaction: the arrow keys walk each chip group and fire the selection, so the whole estimator is drivable from the keyboard; service cards lift 2 px on hover with the shadow deepening rather than the border changing colour.

Pass 3 Hardening

  • The 375 masthead was broken and the assert said it was fine. The wordmark’s strapline is white-space:nowrap inside a flex:0 1 auto; min-width:0 box — it overflowed its own container without widening it, so “CRUSHING & SCREENING · KWINANA” ran clean under the call button while a box-to-box gap measurement stayed positive. Retired the strapline below 560 px and the second masthead CTA below 640 px, and rewrote the assert to measure the painted text with a Range rather than the flex box that is allowed to shrink under it.
  • Label-collision sweep at native resolution, at 375 / 1000 / 1200 and across five driven states. It found three real defects: a docked tonnage label running off the right edge at the 26 m frame (now flips to the other side of the apex), “BOTTOM DECK 20 MM” running off the left edge at 375 (short forms below 641 px plus a slide-right clamp on the whole callout, leaders included), and the two title-strip captions overprinting each other at 375 — 3⁷WIXE̶DPOSE — now stepped down through pinned shorter forms and dropped entirely if they still will not fit.
  • The screenshot-resize gate. Entrance choreography runs on a cold-load firstDraw flag with a settle-timer backstop; the resize handler calls render(false) and can never re-enter it. The standing assert drives the controls, resizes to 1000 px, changes device scale factor to 2 (which is what a clipped screenshot does), waits with nothing poking the page, then requires all eight animated groups at opacity 1, the stockpiles to have real drawn area, settled === true and firstDraw === false.
  • Reduced motion runs in its own browser context: settles immediately, rest figures correct, scene opaque and drawn, and the chips still work.
  • Form pattern proved by driving checkValidity() on a deliberately invalid value, not by reading the attribute — Chrome compiles pattern as a v-flag regex and a bad escape leaves the field silently validating nothing.
  • Removed one ornament: the feed pile’s two-line on-scene name plate. Its information moved to the title strip, where it cannot collide with the plant, and the drawing got quieter for it.
  • Final copy read: no banned marketing filler, Australian English throughout, and the FAQ’s “what we don’t do” answer left blunt — we don’t demolish, don’t cart, don’t certify a pavement design and won’t touch asbestos-contaminated material.

10What would change for the real business

11Honesty