Generative Assets · Industry Demos XXV · site 340
Blackwattle Coffee Roasters — how it was made
The concept: a fictional specialty roastery in Osborne Park with an espresso bar out front, supplying cafés and offices across the Perth metro. The audience is the café owner who is tired of a big roaster's account manager, the office manager buying beans by the box, and the home barista who wants the wholesale blend by the kilo. The page has one job — a wholesale trial-order request with the blend, weekly volume and kit already priced. It is the wave's dark site: the roastery at 6pm, drum still ticking.
Palette — argued from the roastery
Espresso for the ground, crema for the ink, the copper drum for the glow and the CTA, hessian for the linework. It sits nowhere near the banned defaults: no acid accent on black, no cream-and-terracotta serif — the accent is a metal, the ground is a drink.
Measured contrast (WCAG arithmetic on the shipped hex, Pass 1)
Type
Fraunces (700, SOFT 100, optical size driven per role — 96 for the h1, 144 for the display line, 40 for badges and cards) is the serif on a good coffee sack: soft, a little wonky, warm at weight. Work Sans carries everything a café owner has to actually read — chips, ledger, form, FAQ — at 17px on mobile. The typographic moment is the volume set huge in Fraunces caps, "TWENTY KILOS A WEEK, ROASTED TUESDAY", swapping the number word live with the stepper.
Techniques
- The Roast Drum & Order Builder (signature) — one inline SVG scene (760 × 480 viewBox) with an HTML price badge overlaid on the drum's face plate. The drum spokes rotate and 16 drawn beans tumble inside a clipped window: each bean rides the drum wall from 78° to 228° then falls on a gravity-eased chord back to the bottom, on its own phase and speed, driven by a single rAF loop with the delta clamped at zero and capped at 50 ms (
dt = min(max(now − last, 0)/1000, .05)). The window glow pulses between 0.82 and 1.0 of the roast's glow level, so the never-invisible floor is ≥ 0.59 at dark and ≥ 0.72 at medium. Reduced motion renders one settled frame (beans spread mid-tumble, drum at 22°) and never starts the loop;document.hiddenpauses it. - Controls → drawing. Blend chips relabel the hero sack ("HOUSE / ESPRESSO", "SINGLE / ORIGIN", "SWISS WATER / DECAF") and recolour the mark on every mini sack; roast chips recolour the beans and their crease and change the glow colour and level; grind chips rewrite the bag's grind tag and its glyph (a whole bean, four fine dots, two coarse dots). The volume stepper stacks 5 kg sacks on the pallet three wide (1–12) with a dual-speed pop-in — 60 ms stagger on first draw, 26 ms after — and moves a pointer along the tier bar under the pallet. Machine on loan draws a two-group machine and grinder on the bench; barista training draws a tamper and notepad on the shelf. Every handler bumps a token BEFORE the state swap so in-flight sack pop-ins from a previous drive are dropped, and the probe's
settledflag flips true only after the last scheduled pop-in. - Second-read details — the chalkboard on the wall ("ROAST DAYS · TUE · THU · CUPPING TUESDAY 7–9"), the smoke wisps from the chimney (CSS keyframes on inner paths, positioned by an outer
transformgroup so the W16 attribute rule holds), the sack tie in copper, the volume word swapping in the display line, and the stepper readout's small scale bump. - Service-area map — a stylised SVG of the metro with the coast, the Swan River drawn from the Fremantle mouth, fourteen named suburbs and the Wednesday run as a dashed copper loop animated by
stroke-dashoffset(attribute holds the dash array, CSS animates only the offset). - No libraries. Vanilla JS, inline SVG, CSS. Google Fonts only.
Honest numbers — the pricing contract
Everything the builder computes is anchor arithmetic from the wave spec, measured from the running page and cross-checked against an independently reimplemented oracle in the verification harness. Prices are wholesale, per kilo, GST inclusive; totals display to the whole dollar (every anchor and multiplier is integer-safe).
| Line | Anchor | Semantics |
|---|---|---|
| House espresso blend | $34/kg | Grid floor "from $34/kg — 5 kg a week minimum, delivery free from 10 kg". Cheapest priced output: 5 kg → 170 + 15 = $185 a week. |
| Single origin, rotating | $42/kg | Same tiers, same run. |
| Swiss Water decaf | $40/kg | Roasted on its own batch. |
| Volume stepper | 5–60 kg a week, ×5 | Sacks drawn = kg ÷ 5. |
| Volume tiers | 5–10 list · 15–30 −$2/kg · 35–60 −$4/kg | One clamp: the discount comes off the selected blend's rate; NOTE "Volume tier: −$2 a kilo from 15 kg" / "−$4 a kilo from 35 kg". Both boundaries sit on the ×5 grid — asserted 10 → list, 15 → −2, 30 → −2, 35 → −4. |
| Metro delivery, weekly | Free from 10 kg · +$15 at 5 kg | Threshold on the kg count, not the dollar subtotal. NOTE "Delivery $15 — free from 10 kg a week" at 5 kg; none at 10. |
| Roast chips | light / medium / dark | NO price change — "roast changes the cup, not the price" on-card. Live at every combination; they change the drawing. |
| Grind chips | whole bean / espresso / filter | NO price change — "we grind to your machine at no charge" on-card. Live at every combination. |
| Machine on loan | $0 | Live (default ON at rest) at 15 kg and up — "on loan on 15 kg a week and up — no rental". OFF and DISABLED below 15 kg with reason "loan machines start at 15 kg a week — under that, ask about lease-to-own". Restore-vs-reset: the owner's choice is remembered (machinePref) and RESTORED when the volume steps back to ≥ 15 kg. |
| Barista training | +$350 one-off | Live at every combination. Adds the badge's second line "+ $350 one-off" and the ledger's one-off row. |
| Custom blends, private label, office subscriptions | no number | "Priced on the brief — tell us the volume and the cup you're after" (designed price-less line, ledger + services copy). |
| Retail bags | $16 / 250 g · $48 / kg | Grid-only anchors — no estimator line. |
Worked examples (all measured from the page = oracle)
- Rest: house, 20 kg, medium, espresso grind, machine ON, training OFF → 20 × (34 − 2) = $640 a week, tier note "−$2 a kilo from 15 kg", delivery free, no one-off line.
- Single origin 5 kg → 5 × 42 + 15 = $225 + delivery note, machine disabled; single origin 10 kg → $420, no note, machine still disabled.
- Decaf 35 kg → 35 × 36 = $1,260 (−4 tier); decaf 30 kg → 30 × 38 = $1,140 (−2 tier).
- House 15 kg → $480; house 10 kg → $340 (list).
- Rest + training → $640 a week + $350 one-off. Rest + dark roast + filter grind → $640, drawing changes, probe fields change.
- Machine restore: rest → 10 kg (locked OFF) → 15 kg (restored ON, $480); owner turns it OFF at 15 → down to 10 → back to 15 → stays OFF.
- Rapid-fire (eleven clicks in one evaluate): single origin, 40 kg, dark, whole bean, training ON → $1,520 a week + $350, 8 sacks drawn, no stale pop-ins.
No spec corrections were needed — every worked figure landed on the pinned arithmetic.
Conversion decisions
- Above the fold at 375: eyebrow, h1 naming the trade and the area, the "roasted Tuesday, on your bench Wednesday" promise, a copper trial-order CTA, a ghost call button, and three trust chips (since 2017, HACCP FS-000000, cupping Tuesday) — measured to sit inside 740 px with the sticky bar showing.
- Two CTAs, one job. Header, hero, ledger, area block and sticky bar all point at the same trial-order form; the phone is one tap in the header (icon-only at 375 to keep the masthead in one row) and in the sticky bar.
- The builder pre-fills the form with the scenario ("Trial order: House espresso blend, 20 kg a week, medium roast, espresso grind — $640 a week, machine on loan.") and stops overwriting once the visitor edits it, so the lead arrives warm and specific.
- Form fields — name, phone (v-flag-safe pattern), suburb, business type (café / office / restaurant / home barista) and the message. Front-end validation with per-field messages, focus rings, and a designed confirmation panel hidden on load and on an empty submit, shown only after a valid submit — "Thanks — trial order received. We'd call you back within one business day…". No network, and the fine print says so.
- Price INSIDE the art — the $640 badge sits on the drum's face plate and both the scene and the badge intersect the first 750 px at 1200 wide; at 375 the tight variant stacks "a week" under the figure so it never covers the hero sack.
- Honesty on the page: reserved phone (08) 9000 0340, suburb-level address, placeholder ABN and HACCP numbers, the fictional-disclosure colophon, and live JSON-LD limited to WebPage / CreativeWork / WebSite / BreadcrumbList / ListItem.
Local SEO we'd implement for the real business
This demo ships no business-shaped structured data — fictional LocalBusiness or Review markup in a real index would be dishonest. For a real Blackwattle we'd ship the block below. The top-level type is CafeOrCoffeeShop: the espresso bar makes it the exact schema.org organization subtype, more honest than a bare Store or LocalBusiness, with the wholesale side carried in description, makesOffer and areaServed. Reviews would come from the real feed, never typed in.
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "CafeOrCoffeeShop",
"name": "Blackwattle Coffee Roasters",
"description": "Specialty coffee roastery and espresso bar in Osborne Park supplying cafés and offices across Perth. Wholesale from 5 kg a week, delivery free from 10 kg, machines on loan from 15 kg.",
"url": "https://www.example-blackwattle.com.au/",
"telephone": "+61 8 9000 0340",
"servesCuisine": "Coffee",
"priceRange": "$$",
"address": {
"@type": "PostalAddress",
"streetAddress": "[unit / street — real address]",
"addressLocality": "Osborne Park",
"addressRegion": "WA",
"postalCode": "6017",
"addressCountry": "AU"
},
"geo": { "@type": "GeoCoordinates", "latitude": -31.90, "longitude": 115.81 },
"openingHoursSpecification": [
{ "@type": "OpeningHoursSpecification", "dayOfWeek": ["Tuesday","Wednesday","Thursday","Friday"], "opens": "06:30", "closes": "15:00" },
{ "@type": "OpeningHoursSpecification", "dayOfWeek": "Saturday", "opens": "07:00", "closes": "13:00" }
],
"areaServed": ["Osborne Park","Leederville","Mount Lawley","Perth","Subiaco","Victoria Park","South Perth","Fremantle","Cottesloe","Scarborough"],
"makesOffer": [
{ "@type": "Offer", "itemOffered": { "@type": "Product", "name": "House espresso blend, wholesale" }, "priceSpecification": { "@type": "UnitPriceSpecification", "price": "34.00", "priceCurrency": "AUD", "unitCode": "KGM" } },
{ "@type": "Offer", "itemOffered": { "@type": "Product", "name": "Single origin, wholesale" }, "priceSpecification": { "@type": "UnitPriceSpecification", "price": "42.00", "priceCurrency": "AUD", "unitCode": "KGM" } }
],
"sameAs": ["[Google Business Profile]", "[Instagram]"],
"aggregateRating": { "@type": "AggregateRating", "ratingValue": "4.9", "reviewCount": "112" },
"review": [
{
"@type": "Review",
"author": { "@type": "Person", "name": "[real reviewer]" },
"datePublished": "[date]",
"reviewRating": { "@type": "Rating", "ratingValue": "5" },
"reviewBody": "[pulled from the Google reviews feed]"
}
]
}
</script>
Alongside the schema: a Google Business Profile with the roastery photographed on roast day, a "wholesale coffee Perth" landing page per suburb cluster with the delivery-day promise, and the FAQ marked up as FAQPage once the answers are the real business's.
What would change for the real client
- Ordermentum for the wholesale ordering, standing orders and invoicing — the trial-order form would create the account and the first standing order there, and the builder's tiers would read from its price lists.
- Shopify for the retail bags and the office subscriptions (the price-less line on this page), with the same blend names and roast dates.
- Xero for the ledger — Ordermentum and Shopify both push invoices to it, so wholesale and retail land in one set of books.
- Their photography (drum, bar, the Tuesday cupping), their real reviews feed, real farm/lot traceability per sack, and the machine-service log surfaced to accounts.
Iteration log
Pass 1 — Craft
Screenshot review at 1200×750 and 375 native, plus a headless harness that grew to 129 asserts across the three passes (oracle on ten scenarios, tier and delivery boundaries, machine lock and restore, two rapid-fire drives, empty and filled submit, reduced motion, 375 masthead and overflow, guide checks). Found and fixed: the tier-bar labels collided (three long labels on 58-unit segments) → two-row labels with the active tier lit copper; the grind tag's text overran the tag and struck the glyph → tag widened to 78 units, glyph moved right of the text; the badge covered the hero sack at 375 (badge right edge 203 px vs sack left 195 px) → the tight variant stacks "a week" under the figure, badge ≈ 60 px wide; the third trust chip dipped 4 px under the 740 px fold at 375 → hero padding and h1 tightened below 520 px; the map's "Osborne Park roastery" label sat on the star and on Leederville, and the river left the coast in the wrong place → whole map re-plotted with the Swan River from the Fremantle mouth and labels placed clear; hero grid rebalanced .9 / 1.1 and h1 capped at 50 px so the drum reads larger in the thumbnail. Contrast measured (figures above) — copper passed at 6.20:1 so no remedy was needed for copper type; the pre-minted #E8B06B is used for accent copy on panels regardless.
Pass 2 — Depth
Added the chalkboard on the wall as the second-read detail; tied the window glow's colour and level to the roast (light 0.72, medium 0.85, dark 1.0, pulsing ×0.82–1.0 — floor ≥ 0.59); recoloured the bean crease per roast so light beans read light, not just paler; gave every mini sack a blend-coloured mark so the whole pallet changes with the blend chip; added arrow-key movement inside each chip group; added the small scale bump on the volume readout; softened the price hints inside chips. Verified the rapid-fire drive twice (eleven clicks, then fourteen) with zero stale pop-ins and the settled flag reliable.
Pass 3 — Hardening
375 masthead checked first: wordmark right edge < phone-icon left edge < CTA left edge, no wrap, no overflow (scrollWidth 375, zero offenders outside SVGs). Reduced-motion path: no rAF loop, beans placed, sacks visible, smoke static, route static. Focus rings on every chip, toggle, stepper, field and link. rAF dt clamped at zero and capped. Chanel rule: removed the decorative wall rule behind the chimney. Label-collision sweep on the scene at native resolution: badge clear of the hero sack and the machine label, tier labels clear of each other, the sack-count caption moved under the tier bar so it never meets the grind tag at a 12-sack stack (the tag itself was lifted above the neck tie so column-one sacks never cover it). Guide tables in .table-scroll, guide 375 clean, one h1 on both pages, colophon on both pages, JSON-LD allowlist on index, no console errors, no backdrop-filter. Final copy read over the FAQ, the ledger foot and the confirmation panel.