The concept
Wagtail Dog Grooming is a fictional grooming & hydrobath salon in Bayswater, named for the bird that never stops moving — like every dog at bath time. The audience is oodle owners six weeks past due, double-coat owners shedding through a Perth summer, and new puppy owners who want the first groom to go well. The page has one job: book a groom — and it earns that booking with honesty (the matting promise, the double-coat promise, prices that are the price) rather than gloss.
Palette — argued from the salon
The spec's bright accents (#2E9BA6, #D97E4A) both fail 4.5:1 as small text on towel cream, so each got a derived ink twin — the line/ink split the collection's learnings mandate. The aqua is deliberately a warm-bathroom aqua on cream, distinct from tinting's dark-site ceramic cyan and poolcare's pool water.
Type
Baloo 2 for display — round, wagging, friendly weight; the masthead moment and every heading. Nunito Sans for body and UI — a workhorse that stays warm at 16.5px. Azeret Mono for kilograms and dollars — prices in mono read as ledger, not marketing, which is the whole honesty argument in one typeface choice.
The signature — the Groom Planner
Size, coat and service chips plus a deshed toggle drive a procedural SVG dog on the grooming table. The silhouette rescales per size chip (an outer <g> positions the dog with its paws anchored to the table; a nested <g> carries only the scale, so every size stands on the same mat). The coat re-renders procedurally per chip from a seeded PRNG — fine hatch lines for short coats, layered light-under/dark-over strokes with a saddle for double coats, looped coils for curly & wool — roughly 350 stroke elements per render, staggered in on first draw only (a firstDraw flag keeps every later redraw under 600 ms, so chip taps feel snappy, not ceremonial).
The before/after reveal is the comb line: two rect-clipped coat layers (scruffy strokes plus edge-fuzz spikes left of the line, neat strokes plus a sheen gradient right of it) share the dog's silhouette clip, and the line itself is a scene-space overlay recomputed from the dog-local comb position each frame. At rest it eases to ~60% groomed, then drifts gently between 52% and 68%. Suds and the hydrobath arc loop behind the dog with a CSS opacity floor of 0.45 — never invisible at any capture instant. Every SVG element keeps attribute state and CSS animation on separate nested nodes (the W16/W17 rule), and the whole scene renders settled at 60%/$145 under prefers-reduced-motion.
Second reads: a willie wagtail perched on the tile rail flicks its tail every seven seconds, the dog blinks, a dryer blows animated air lines at the finished side, and switching the deshed toggle on releases a drift of loose undercoat off the rump — so the toggle changes the rendering, not just the number.
The pricing model — shape and derivation
Every figure the planner shows is computed from the pinned anchors below; the card's cheapest possible output ($65, small wash & tidy) equals the services grid's "grooms from $65" floor.
| Service | small | medium | large | giant |
|---|---|---|---|---|
| Wash & tidy | $65 | $80 | $100 | $125 |
| Full groom | $95 | $120 | $150 | $185 |
| Curly & wool clip | +$25 on full grooms only | |||
| Deshed treatment | +$25, any size, any coat | |||
| Puppy intro (<6 mo) | $60 flat | |||
| Walk-in nail trim | $20 — outside the planner, walk-in only | |||
Three dimensions are deliberately flat, and the page says so rather than inventing gradients:
- Curly on a wash & tidy is a price no-op. The chip stays live — it changes the rendering to coils — and the card says why: "a tidy is a tidy whatever the coat — the clip is where wool earns its keep." The +$25 belongs to the clip, so it applies to full grooms only.
- Deshed is +$25 at every size. The undercoat rake is the same tool and the same patience whether the dog is 8 kg or 48 kg; the size premium already lives in the base price.
- Puppy intro is $60 flat — priced by patience, not kilos. Selecting it disables the size chips (with that reason on the page) and the deshed toggle (puppy coats haven't grown an undercoat yet), so no control can compute a dishonest figure.
Worked examples, cross-checked against the spec's anchor arithmetic and measured from the running page by the QA harness's independently reimplemented oracle: the rest state (medium · curly & wool · full groom) shows $120 + $25 = $145; with deshed toggled, $170; a giant double-coat full groom with deshed, $185 + $25 = $210. All matched with zero corrections. The services grid's derived cards say their arithmetic out loud: oodle grooms "from $120" is the small full groom $95 + $25; deshed treatments "from $90" is the small tidy $65 + $25.
Conversion decisions
- The planner shares the hero. At 1200×750 the signature's bounding box starts ~120px down — the thumbnail shows a dog mid-groom and a real price, not just copy. On mobile the value proposition, primary CTA and trust chips all land inside the first 667px.
- Two CTAs everywhere a decision happens: book (form anchor) and call (tel:). Header carries both; mobile gets a sticky call/book bar with 48px targets.
- The dog's name is a required form field. A groomer books dogs, not owners — the confirmation and even the validation summary use the dog's name back at you. The planner pre-fills the breed-and-coat notes with its configured scenario and measured price, so the enquiry arrives warm.
- Preferred day is a plain
type=datewithmin= today — no custom widget to break on a phone. - The confirmation is honestly local: "Thanks — request received… demonstration page, no booking was sent." No fake network round-trip.
Local SEO we'd implement for the real business
This demo page carries only WebPage schema naming it a demonstration — fictional structured data in a real index would be dishonest markup. For a real salon we'd ship LocalBusiness + Review schema like this:
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "LocalBusiness",
"name": "Wagtail Dog Grooming",
"description": "Dog grooming and hydrobath salon in Bayswater, WA.",
"telephone": "+61 8 XXXX XXXX",
"address": {
"@type": "PostalAddress",
"streetAddress": "…", "addressLocality": "Bayswater",
"addressRegion": "WA", "postalCode": "6053", "addressCountry": "AU"
},
"geo": { "@type": "GeoCoordinates", "latitude": -31.917, "longitude": 115.917 },
"openingHoursSpecification": [{
"@type": "OpeningHoursSpecification",
"dayOfWeek": ["Tuesday","Wednesday","Thursday","Friday","Saturday"],
"opens": "08:00", "closes": "16:00"
}],
"priceRange": "$65–$235",
"aggregateRating": { "@type": "AggregateRating", "ratingValue": "4.9", "reviewCount": "182" },
"review": [{
"@type": "Review",
"author": { "@type": "Person", "name": "…" },
"reviewRating": { "@type": "Rating", "ratingValue": "5" },
"reviewBody": "…"
}]
}
</script>
Plus: suburb-level service pages only if genuinely distinct, Google Business Profile with groom-category photos, and review markup fed by a real review flow — never hand-typed.
What would change for the real client
- Booking & CRM: a Gingr- or MoeGo-style grooming platform behind the form — client records, vaccination status, coat notes per dog, and real slot availability instead of a preferred-day field.
- SMS: Twilio reminders and the "blow-dry started, ten minutes" text the FAQ promises.
- Payments: Square card-on-file with no-show protection — the one-dog-at-a-time model can't absorb empty tables.
- Coat records: before/after photos per visit attached to the dog's record — the real version of this page's comb-line reveal.
- Books & reviews: Xero for accounts; Podium (or similar) driving the review flow that would legitimately power the schema above.
- Their photos, their real prices, their groomers' actual certificates — the structure stays, the honesty rules stay.
Techniques
- Single self-contained file, no libraries — SVG illustration + vanilla JS; the only external resources are Google Fonts.
- Seeded PRNG (mulberry32) so every coat render is deterministic — QA screenshots are reproducible.
- Nested clip-paths: silhouette clip × comb-side rect clip gives the scruffy/finished split without duplicating geometry; the edge-fuzz layer reuses the scruffy rect clip outside the silhouette so tufts poke past the outline.
- State probe
window.__petgrooming(state, measured price, groomed fraction, stroke count) — QA gates on state, never wall-clock. - Entrance gated on
document.fonts.readywith a 1.1s fallback timer; nothing is ever left at opacity 0 if JS dies. - rAF loops pause on
document.hidden; DPR is irrelevant here (pure SVG); reduced-motion renders the settled 60%/$145 frame.
Iteration log
Pass 1 — Craft
- Found a duplicate
cxattribute on the Ashfield map dot (the classic silent-parser-drop defect) — fixed before it could misplace the marker. - The coat-stroke entrance stagger was inert: strokes were created already at their final inline opacity, so the transition had nothing to animate. Added a pre-ready
.coat-stroke{opacity:0!important}override that releases when fonts settle. - Drove the full suite headlessly: empty submit on a fresh load (5 invalid fields + summary, no confirmation), filled submit (lexicon confirmation naming the dog), six planner scenarios asserted against an independently reimplemented oracle — rest $145, floor $65, giant+deshed $210, no-op $65, puppy $60, re-enable $145. All green.
- Looked at the 1200×750 and 375×667 screenshots: planner top at 121px shares the thumbnail with a readable mid-groom dog and the $145 card; masthead clean at 375 (wordmark right edge 131px vs phone at 196px).
Pass 2 — Depth
- Made the deshed toggle change the rendering, not just the price: loose undercoat tufts drift off the rump while it's on (hidden again for puppy, where the toggle is disabled).
- The driven-state screenshot (giant · double · full + deshed) caught the tufts rendered cream-on-cream — invisible. Recoloured them to the active coat's dark tone at 0.55 opacity.
- Swapped emoji paw ratings for a shared SVG
<symbol>— consistent across platforms, on-palette apricot. - Re-ran the full suite with the new asserts (fluff visible with deshed, hidden for puppy, saddle for double coats, coils for curly): all green.
Pass 3 — Hardening
- Keyboard path: Enter activates chips, Space flips the deshed switch, Enter opens the FAQ — all asserted headlessly, not just mouse-event dispatch.
- Chanel rule: removed the third caption line under the table ("every groom starts with a hello") — the label stack under the scene was one line too chatty.
- Tightened mobile hero padding so the trust chips clear the sticky bar at 375×667; reviewed 375 screenshots of the services grid and the service-area map — no overflow (scrollWidth 375), suburbs legible.
- Reduced-motion run settles at comb 60%, $145 shown, hero visible, zero console errors; suds opacity sampled ≥0.45 at five instants; comb-drift state probe confirms liveness (0.677 → 0.630 over 1.3s).
Honesty rules, applied
The business is fictional and the page says so plainly in the colophon. The phone number uses the reserved (08) 9000 0269 pattern; the address stays suburb-level; ABN and certificate numbers are visibly placeholder-patterned (ABN 00 000 000 000, Cert III 0000). Reviews are demo copy on a page declared fictional, never structured data. The two trade-honesty promises — humane clip-off for matted coats, never shaving double coats — are real grooming practice, quoted because being right about the trade is the demo's credibility.