Generative Assets · Industry demo 287 · How it was made

Duckdive Swim School — build notes

A fictional Joondalup learn-to-swim centre for parents in Perth's northern corridor. The page has one job: convert a worried parent into a free level-assessment booking — and the Lane Finder does the warming up.

Concept

Learn-to-swim is sold on trust, not price — so the page leads with the things parents actually vet: class size, water temperature, teacher checks. The signature element, the Lane Finder, turns the school's level system into a cross-section of the pool itself: an age stepper and four "can they…" chips resolve your child to one of five lanes, light it up, and show the term fee, two class times and the sibling discount arithmetic live. Every selection pre-fills the assessment form, so the demo interaction becomes a warmer enquiry.

Palette — the pool at 8 am

Pool-tile light#E9F5F4 — page ground; chlorine-clean, not clinical.
Deep-water ink#123F4D — body text at 10.5:1 on the ground.
Kickboard coral#E45C44 — CTAs and marks only, never small text.
Coral ink#B0361F — the derived text-safe coral (5.5:1) for links and small accents.
Buoy-line yellow#F2B838 — lane-rope dashes, badges, highlights.

The ink/line split rule applied: coral carries buttons and the lane ring, but any coral that has to be read small (links, the discount line) is the darker #B0361F coral-ink token.

Type

Baloo 2 for display — round and buoyant, kid-friendly without tipping into cartoon; the level names set along the lane ropes are the typographic moment. Nunito Sans as the workhorse: legible at 16px+ for a parent reading one-handed. Prices and the age readout are set in Baloo 2 so the figures feel like poolside signage, not an invoice.

The Lane Finder — how it works

The pool is a single inline SVG: a basin path with a depth-graded water gradient, a shimmering waterline, and five lane groups from shallow to deep — parent-held baby, bubble-blower, kickboard kicker, freestyler, streamlined squad diver. Each figure sits on an outer <g transform> for position while CSS animates a nested inner group (bob, glow, drift) — position attributes and animation never share an element, so no presentation-attribute clobbering. The resolved lane gets a class that lights a yellow glow (pulsing between 0.62 and 1 — a never-invisible floor, so the 7-second thumbnail always catches it lit) and a dashed coral ring around the swimmer.

Resolution logic mirrors the school's pinned rules exactly: under 3 → Paddlers; 3–4 → Splash; 5+ beginners → Stroke Foundations; 5+ who can swim 10 m → Stroke Development; 8+ who can swim 50 m → Squad. The skills chips stack like a ladder (ticking "swim 10 m" includes the earlier skills; unticking it clears the later ones). Under 3, the chips disable with an on-card reason — babies are assessed in arms — and under 5 the card states that distance doesn't move the level, because grouping is by age until school age: every control either changes the answer honestly or says why it can't.

Every handler bumps an animation token and clears the pending settle timer before swapping state, so rapid-fire taps can never leave a stale timer marking a superseded scenario as settled. First render gets the entrance choreography (staggered 0.6s fades, capped at 1.5s); after the first interaction a snappy class drops subsequent redraws to a 550 ms cap. window.__swimschool exposes the scenario, resolved level, fees and a settled flag for the QA harness.

Pinned fees the finder resolves against

LevelAges / entry ruleTerm fee (10 weeks)Per lesson
Paddlers6 months – 3 years, parent & baby$190$19
Splash3 – 4 years, first solo class$230$23
Stroke Foundations5+ beginners$230$23
Stroke Development5+, can swim 10 m$230$23
Squad8+, can swim 50 m, 2 sessions/week$340$17 a session
Private 1:1any age, 30 min$52

The sibling toggle demonstrates the multi-child discount live: 10% off the lower-fee enrolment, itemised. Rest state Splash + Paddlers baby = $230 + $190 × 0.9 = $401/term, measured from the running page and verified against an independently reimplemented oracle in the harness (as were $511 for Squad + baby and $361 for two Paddlers).

Conversion decisions

Local SEO we'd implement for the real business

This demo page ships only WebPage schema naming it a demonstration — fictional structured data in a real index would be dishonest markup. For a real swim school we'd ship business schema with their real details and review feed:

{
  "@context": "https://schema.org",
  "@type": "SportsActivityLocation",
  "name": "Duckdive Swim School",
  "description": "Learn-to-swim classes in a heated indoor 25 m pool in Joondalup.",
  "telephone": "+61-8-XXXX-XXXX",
  "address": {
    "@type": "PostalAddress",
    "streetAddress": "(real street address)",
    "addressLocality": "Joondalup",
    "addressRegion": "WA",
    "postalCode": "6027",
    "addressCountry": "AU"
  },
  "geo": { "@type": "GeoCoordinates", "latitude": "…", "longitude": "…" },
  "openingHoursSpecification": [
    { "@type": "OpeningHoursSpecification",
      "dayOfWeek": ["Monday","Tuesday","Wednesday","Thursday","Friday"],
      "opens": "08:30", "closes": "18:30" },
    { "@type": "OpeningHoursSpecification",
      "dayOfWeek": "Saturday", "opens": "07:00", "closes": "12:30" }
  ],
  "priceRange": "$190–$340 per 10-week term",
  "aggregateRating": {
    "@type": "AggregateRating",
    "ratingValue": "(from their live review feed)",
    "reviewCount": "(from their live review feed)"
  },
  "review": [
    { "@type": "Review",
      "author": { "@type": "Person", "name": "(real reviewer)" },
      "reviewBody": "(real review, with permission)",
      "reviewRating": { "@type": "Rating", "ratingValue": "5" } }
  ]
}

Plus: Google Business Profile with lesson categories, suburb landing content for the five catchment suburbs, and FAQ schema mirroring the on-page questions.

What changes for the real client

Iteration log

Pass 1 — Craft. Drove the full harness (58 asserts): every level boundary — 2½ → Paddlers, exactly 3 → Splash, exactly 5 beginner → Foundations, 5 + 10 m → Development, 7 + 50 m stays Development, 8 + 50 m → Squad — checked against a separately reimplemented oracle, plus empty and filled form submits on fresh loads. Screenshot review at 1200×750 and 375 found two craft defects the asserts couldn't see: the "2.0 m" depth label was near-invisible ink-on-deep-water (recoloured to pool-tile light) and anchor jumps tucked section headings under the sticky masthead (added scroll-margin-top: 78px). Confirmed the 375 masthead line — wordmark, phone icon, CTA — has no collision.

Pass 2 — Depth. Added the second-read detail: a resident rubber duck that drifts the full waterline on a 74-second alternate loop and dives 96° on a click, springing back upright — position on the outer group's transform attribute, animation on nested inner groups, so SVG attributes and CSS animation never fight. Added the age-readout pop (a 0.32s overshoot scale on change, suppressed for reduced motion and on the entrance). Re-drove everything: 60/60, including new asserts that the duck actually drifts and recovers from its dive.

Pass 3 — Hardening. Chanel rule: removed the third waterline shimmer ellipse crowding the deep end. Re-took the true 7-second-settle thumbnail — Splash lane lit (glow floor 0.62, never invisible), $230 card and the duck all inside the 1200×750 frame. Rapid-fire drive: 18 control clicks dispatched in one evaluate with no settles between — final state still matches the oracle and the settled flag lands true, because handlers token-invalidate in-flight timers before every state swap. Reduced-motion renders the settled lit-lane frame with every animation off. Phone pattern is v-flag-valid; zero console errors across all seven page sessions.