How the Longtable demo was built
Longtable Catering Co. is a fictional Perth caterer — corporate, weddings and wakes out of a Leederville kitchen. The page has one job: turn "how much per head, and what's actually included?" into a priced, dated enquiry. Everything on it, from the golden-hour pass illustration to the map, is procedural SVG and CSS — no photography, no raster assets.
Signature
The Menu Builder
Event chips (office lunch / cocktail party / wedding / wake), a 10–150 guest slider, and four service chips drive two things at once: a procedural SVG pass scene and an honest estimate card. Drop-off mode lines labelled trays along the bench and multiplies them with the guest count; grazing lengthens the table plank by plank; staffed canapés sprout picked trays with apron-silhouette staff standing behind the pass; plated sets cloched covers in rows. The scene is rebuilt per state change from primitive shapes (a mk() helper creating namespaced SVG elements into one group), with a mono caption stating the count — and a second-read detail: pick "wedding" and a small cake waits at the end of the pass; pick "wake" and a jar of rosemary appears, for remembrance.
The card shows the working, not just the answer: rate × guests, the derived staff count for staffed services, the inclusions for that service, and — whenever a minimum bites — a wine-barred note explaining why. The builder state pre-fills the enquiry form's guests and event-description fields (a calculator-to-form handoff), and the whole state is exposed on window.__caterer for verification.
Honest numbers
The pricing model and its shape
All four per-head rates and their minimums are the spec's pinned anchors; the model around them is deliberately simple and fully disclosed:
| Service | Rate | Minimum | Staffed |
|---|---|---|---|
| Drop-off trays | $18/head | $220 order floor | — |
| Grazing table | $26/head | 20 guests | — |
| Staffed canapés | $42/head | 30 guests | 1 per 25 |
| Plated (3 courses) | $78/head | 40 guests | 1 per 25 |
- total = max(rate × guests, dollar floor), with guests hard-clamped to the service's guest minimum. No rounding — anchor arithmetic is exact, and the card shows exact dollars.
- The per-head rate is deliberately flat across guest counts. No volume taper is pinned by the spec, so none was invented: what changes with scale is the staffing count and logistics, both shown on the card rather than hidden in a gradient.
- Staff = ⌈guests ÷ 25⌉ for canapés and plated, included in the rate and stated as working ("60 guests → 3 staff") — the ratio is part of the price, so the page shows the arithmetic.
- The drop-off floor bites below 13 guests ($220 ÷ $18 ≈ 12.2): at 10 guests the card shows "10 × $18 = $180 → $220" with the reason. This $220 is also the builder's global cheapest output, matching the services grid's drop-off line exactly — verified, not assumed.
- Guest-minimum clamps explain themselves. Slide to 10 and tap "plated": the slider snaps to 40 and the card says why ("plated dinners start at 40 guests"). A control that can't honestly move the price gets a stated reason, never a silent no-op.
Measured from the running page against an independently re-implemented oracle: rest state 60 × $42 = $2,520 with 3 staff; plated clamp scenario $3,120 at 40 guests / 2 staff; canapés at 100 $4,200 / 4 staff; wedding default 90 × $78 = $7,020 / 4 staff. All card figures matched the oracle exactly.
Palette
Argued from the pass at golden hour
#F7F1E4The ground — pressed tablecloth in late sun. Warm butter, held deliberately away from gallery cream.
#3A322CStructure and body ink — cast iron and toasted crust. Carries the estimate card and footer.
#8E3B55Line/mark accent — the beet-stained board and the shiraz pour. CTAs, garnish marks, underlines. Never body text.
#7A7C4ESecondary — the sprig on the tray. Marks, focus rings, the delivery ring on the map.
#5A5C34Olive branch fails 4.5:1 as small text on linen (≈3.9:1), so links, eyebrows and active chips use this derived darker ink (≈6.2:1).
#E1A0B4Wine is too dark to accent on walnut, so totals and card highlights use this lifted tint (≈6.4:1 on walnut).
The spec flags this direction as living near the banned cream-serif-terracotta default. The line is held three ways: the accent stays wine-and-olive (nothing terracotta anywhere), the grounds stay warm butter rather than gallery cream, and the serif is strictly display-only over a sans body.
Type
A menu card, set properly
- Instrument Serif (display only) — a menu-card serif with sharp modern terminals. The masthead's italic table, section heads, and the review pull-quotes carry the typographic bravery.
- Nunito Sans (body, UI) — a warm, round workhorse that keeps forms and service cards legible at 16.5px.
- Sometype Mono — every number on the page: per-head rates, guest counts, the estimate, phone, dates. Prices reading as typeset ledger entries is the point.
Conversion decisions
Why the page is shaped this way
- The hero quotes before you scroll. A dark chip floats on the pass illustration echoing the builder's rest state ($2,520 / 3 staff) — a menu already priced at the seven-second glance, linking straight into the builder.
- The date is the second conversion. Catering is date-gated, so the form leads with "Check your date" and the promise "we pencil your date for 7 days — no deposit to ask" appears at the field, in the side copy, and in the confirmation.
- Form fields: name, phone, suburb, date (min = today), guests, and an event description pre-filled from the builder scenario — editable, and the sync stops the moment the visitor types (a dirty flag per field). Empty submits mark every missing field with a reason; the confirmation is local, designed, and plainly says nothing was sent.
- Mobile-first: sticky call/estimate bar at ≤760px with 48px targets, masthead reduced to wordmark + call pill, all above-fold requirements met at 375px.
- Trust placement: the strip sits directly under the hero (registration, liability, RSA); the honesty lines live where the doubt occurs — staffing cost explained on the canapé card, leftovers on the plated card, delivery fee on the map and in FAQ.
Local SEO
What we'd ship for the real business
This demo page emits only a WebPage schema naming it a demonstration — fictional structured data in a real index is dishonest markup. For a real client, the page would carry LocalBusiness + Review schema like this:
Would-ship JSON-LD (sample)
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "FoodEstablishment",
"name": "Longtable Catering Co.",
"description": "Corporate, wedding and wake catering across Perth from Leederville.",
"telephone": "+61 8 XXXX XXXX",
"address": {
"@type": "PostalAddress",
"addressLocality": "Leederville",
"addressRegion": "WA",
"postalCode": "6007",
"addressCountry": "AU"
},
"areaServed": ["Leederville","Perth","Subiaco","Mount Lawley","Fremantle","Swan Valley"],
"priceRange": "$18–$78 per head",
"openingHours": "Mo-Fr 08:00-16:00",
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "4.9",
"reviewCount": "187"
},
"review": [{
"@type": "Review",
"author": { "@type": "Person", "name": "Real client name" },
"reviewRating": { "@type": "Rating", "ratingValue": "5" },
"reviewBody": "Pulled from the live Google / Podium review feed."
}]
}
</script>Real client
What would change
- FoodStorm — the enquiry lands as an order draft with date, guest count and service pre-filled; production sheets and delivery runs flow from the same record.
- Square — deposit and invoice links issued from the quote; the "pencilled date" becomes a real 7-day hold released or converted automatically.
- Deputy — the 1-per-25 staff derivation feeds straight into rostering for the event date.
- Xero — invoicing and job-level P&L per event.
- Mailchimp — seasonal menu flows to the office-manager list (the standing Thursday order is the retention engine).
- Podium — the review feed replaces demo copy, marked up as real structured data per the panel above.
- Their photography could replace the procedural pass scene — though the demo argues it doesn't have to.
Techniques
Under the linen
- Two hand-composed SVG scenes (hero pass, builder pass) sharing one visual grammar: layered warm gradients for the hour, a walnut bench, linen edge, procedural trays. The builder's item layer is rebuilt per state in JS; positions live in SVG attributes, CSS animates only opacity-safe elements (steam, sun glow) on separate nodes — never a property the element also sets as an attribute.
- Procedural service-area map: coastline and Swan River as gesture paths, a dashed olive ring at the honest 15 km scale (10 px/km), suburbs as named points, the kitchen in wine.
- No libraries at all — vanilla JS, native <details> FAQ, CSS-only load choreography gated on a JS-added class so a no-JS load is simply static and visible. Steam and glow animations carry opacity floors ≥.55 and are killed by reduced-motion.
- State probe window.__caterer: event, guests, service, per-head, raw, total, staff, min flags, form status — QA gates on state, never wall-clock.
Process
Iteration log
Pass 1 — Craft
A 57-check headless suite (independent pricing oracle re-derived from the spec, fresh-load empty submit, filled submit, clamp scenarios, sticky bar, FAQ, masthead geometry, reserved-phone sweep, JSON-LD scoping) ran green first time — then the screenshot review earned its keep. At 1280 the hero quote chip's label spans were rendering inline (missing display:block) and the chip covered the tray illustration entirely; recomposed the bench 56px higher, moved tongs and the olive sprig onto the bench face, lidded the jars. At 375 the slider's "40 ≈ a typical office floor" landmark collided with the "10" tick — moved to its own row with a tick stem. Anchor targets were hiding under the sticky header — scroll-margin-top on every id. Two write-time defects also caught: an invalid --wine-deep token and a dead branch in the minimum-note logic.
Pass 2 — Depth
Drove all four services and looked at each render: drop-off trays read undersized against the canvas — scaled to 86px with two-line labels; the hero's chevron "canapés" read as arrows — replaced with dolloped rounds. Added the second-read event garnish (wedding cake / rosemary jar), the estimate-total bump micro-interaction (re-triggered CSS animation, killed under reduced motion), and nudged the map's Leederville label clear of its own halo.
Pass 3 — Hardening
Chanel rule: removed the builder scene's light-shaft polygon — sun glow and window disc already carry the hour. Masthead-first at 375: wordmark and call pill measured inside the viewport with no overlap, sticky bar fixed with ≥48px tel target, h1 + CTA + trust chip all above the fold. Fresh-load empty submit raises four field errors with aria-invalid (guests arrives valid from the builder sync); filled submit confirms inside the "thanks / received / we'd call" lexicon with the 7-day pencil promise. Reduced-motion renders everything settled. Final suite: 57/57 with zero console errors; every guide figure above re-measured from the shipped code.