Blackwattle — the site

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.

#1E1512 espressoPage ground. Dark site.
#F3E9DD cremaBody ink — measured 14.95:1 on espresso.
#D0883A copper drumGlow, marks, chrome, CTA fill (espresso labels). Measured 6.20:1 both ways.
#E8B06B copper inkPre-minted lighter variant — used for accent-coloured copy on the panel surface (8.43:1) and the italic display word.
#CDBBAA soft cremaDerived secondary ink — 9.63:1 on espresso, 8.77:1 on the card panel.
#8C6E5B burlapSack weave, spokes, ledger rules. Lines and marks ONLY — 3.84:1, never type.

Measured contrast (WCAG arithmetic on the shipped hex, Pass 1)

6.20:1copper #D0883A on espresso #1E1512 — spec hypothesis ≈6, passes; the pre-minted #E8B06B remedy was not needed for copper text, but is used for accent copy on panels anyway
6.20:1espresso labels on copper buttons — spec said ≈7, measured 6.20; still comfortably AA
14.95:1crema #F3E9DD on espresso
9.63:1soft crema #CDBBAA on espresso · 8.77:1 on the #2A1D18 panel
3.84:1burlap #8C6E5B on espresso — lines/marks only, no burlap-coloured type anywhere
8.01:1error ink #F0A38B on the form panel

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

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).

LineAnchorSemantics
House espresso blend$34/kgGrid 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/kgSame tiers, same run.
Swiss Water decaf$40/kgRoasted on its own batch.
Volume stepper5–60 kg a week, ×5Sacks drawn = kg ÷ 5.
Volume tiers5–10 list · 15–30 −$2/kg · 35–60 −$4/kgOne 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, weeklyFree from 10 kg · +$15 at 5 kgThreshold on the kg count, not the dollar subtotal. NOTE "Delivery $15 — free from 10 kg a week" at 5 kg; none at 10.
Roast chipslight / medium / darkNO price change — "roast changes the cup, not the price" on-card. Live at every combination; they change the drawing.
Grind chipswhole bean / espresso / filterNO price change — "we grind to your machine at no charge" on-card. Live at every combination.
Machine on loan$0Live (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-offLive at every combination. Adds the badge's second line "+ $350 one-off" and the ledger's one-off row.
Custom blends, private label, office subscriptionsno 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 / kgGrid-only anchors — no estimator line.

Worked examples (all measured from the page = oracle)

No spec corrections were needed — every worked figure landed on the pinned arithmetic.

Conversion decisions

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

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.

← Back to the site · Generative Assets index