How this demo was made
Studio Eleven Dance — a Joondalup dance school, priced before the curtain goes up
Studio Eleven Dance is a fictional dance school in Joondalup — ballet, jazz, hip hop, contemporary, tap and acro for tiny tots to adults, 10-week terms, one December concert. The page has one job: a term enrolment request with the dancer's age group, classes and extras already priced. Its audience is the Currambine parent enrolling a five-year-old for the first time, the Kinross teen adding contemporary to ballet, the Edgewater adult coming back to tap.
Palette — a darkened auditorium, measured with WCAG arithmetic
House lights down: a violet-black ground, chalk text (the barre, the rosin), a magenta stage gel for the buttons, a warm follow-spot gold for marks. Every ratio below was measured from the shipped tokens with WCAG arithmetic during Pass 1 — not transcribed from the spec. The spec's computed figures held for every pinned pair; the one token minted on the page was a lifted chalk-dim for small meta copy on cards.
- house-dark
#17141Fpage ground; cards#221D2C, lifted cards#2A2438 - chalk
#F1ECF5body ink · 15.62:1 on the ground · 14.11:1 on cards · the ticket-stub face - chalk-soft
#BDB4CAsecondary copy · 9.12:1 ground · 8.24:1 card · 7.49:1 lifted card - chalk-dim
#9A91ABsmall meta lines · 6.06:1 ground · 5.48:1 card (lifted from#8F86A0, which measured 4.75:1 on cards) - magenta
#C0306BCTA fill, white labels 5.42:1; hover#A625596.93:1. On the ground it measures 3.35:1 — lines and marks only - magenta-ink
#F07AABthe pre-minted lighter ink for magenta-coloured copy (links, chip reasons, the tip line) · 6.99:1 ground · 6.31:1 card - spotlight gold
#F2C14Efollow-spot, footlights, chip outlines, rules, accent text · 10.82:1 ground · 9.78:1 card · 8.92:1 on the stage floor - stub inks
#4A4357/#2B2535on the chalk ticket stub · 8.09:1 and 12.73:1; Sapience band mail link#7A2C527.82:1
The pre-minted two-token pair did its job: the magenta gel stays the hue of every button, while anything written in magenta uses #F07AAB. On a dark site the remedy runs the other way from the light-ground trades — the ink is minted lighter, not deeper.
Type
Syne 700/800 for display — wide, kinetic, a stage-poster face that makes "$928" and "DANCE CLASSES IN JOONDALUP" feel like lettering on a programme. Work Sans for everything a parent has to read at 9pm on a phone: the planner chips, the ledger, the form. Body sits at 17px (17.5 from 760px), no line under 16px anywhere on mobile. The typographic bravery is the display band under the hero — "3 CLASSES A WEEK · $928 A TERM" set near-viewport-wide in Syne 800 at −0.035em tracking — and that figure is the rest-state total, measured from the running planner, not typed by hand.
The signature — the Term Planner
A drawn SVG stage: proscenium frame, back wall with a barre, perspective boards, nine footlights, a follow-spot cone sweeping the boards. Every selected STYLE chip adds one drawn silhouette in a pose that reads as that style — ballet arabesque, jazz kick-line, hip hop freeze, contemporary floor reach, tap heel-dig, acro bridge. The AGE chip scales every figure's height (tots 0.62 → adults 1.0) and rewrites the name drawn on the spot. The extras draw their marks: the concert pack hangs a curtain swag and puts a drawn ticket on every wrist, the uniform pack hangs the studio's XI crest on the back wall, registration leans a clipboard against the stage-left pillar. The price lives inside the artwork as a ticket stub hanging off the proscenium (on-scene from 641px, docked beneath the stage at 640px and below).
Implementation. Six poses are stored as polyline point lists in a local frame (feet at the origin, nominal height 100) and rendered as a round-capped stroke path plus a head circle; a second, wider gold stroke behind each figure is the "lit" layer. The scene rebuilds on every state change — figures are placed with spacing 140 for up to three, narrowing to 560/(n−1) for six (the cap is the six styles themselves, so no drawn cap was needed). Layout position sits on an outer <g transform> and the CSS entrance animation on an inner group, so the attribute and the stylesheet never fight. The follow-spot is a rAF loop with a clamped delta (dt = min(max(now − last, 0)/1000, .05)): in idle it wanders sinusoidally (period 11 s) around a base that drifts slowly back to centre, amplitude tied to the spread of the figures; a state change bumps a token in the handler, sets the spot to sweep to the changed figure with an exponential ease, and on arrival resumes idle — so a rapid-fire burst of four clicks produces exactly one sweep. Each frame sets a per-figure "lit" amount from the spot's distance, which fades the gold layer in and lifts the chalk stroke to full opacity. The redraw is dual-speed: first draw gets the staged entrance (wall, barre, floor, spot, footlights, frame at 110 ms intervals; figures at 0.55 s + 0.14 s stagger), every later change redraws in 320 ms and the probe's settled flag flips at 560 ms. Reduced motion parks the spot centre-stage, skips the loop and settles immediately.
The pricing model — pinned, then measured
Per 10-week term, GST inclusive, whole dollars by construction (every rate is a multiple of ten, so 10% of a classes line is always an integer).
| Control | Rule | Values |
|---|---|---|
| Age chip | per-class term rate · class length · availability | tiny tots 3–5 $150 / 30 min (ballet and jazz only) · juniors 6–12 $190 / 45 min · teens 13–17 $220 / 60 min · adults $200 / 60 min (no acro — "acro runs for 6–17s") |
| Style chips | multi-select; each = one class a week | ballet · jazz · hip hop · contemporary · tap · acro. Disabled chips show the on-chip reason; a selected style the age group can't take is locked off and remembered, and restored when an allowing age group returns — unless the user toggles it in between |
| Classes line | n × rate; n ≥ 3 → 10% off the classes line only | juniors 3 × $190 = $570 − $57 |
| Third-class note | exactly n = 2 and a third style available | "add a third class and the whole classes line takes 10% off — that's $<3 × rate ÷ 10> back" → juniors $57 · teens $66 · adults $60; absent for tiny tots (only two styles) and once n ≥ 3. Short form on the badge at ≤640px: "a third class takes 10% off — $x back" |
| Concert pack | +$85 × n (actual count), outside the discount | $0 at n = 0 |
| Uniform pack | +$120 flat | live at every combination |
| Registration | +$40 flat, on at rest | live at every combination |
| Empty state | n = 0 | badge "$ — · tick a style to price the term", line 2 absent, extras greyed "pick a style first"; grid floor = tiny tots, one class = $150 |
| Sibling discount | declaration only | "second dancer 10% off their classes — add them on the enrolment form" — on the card and in the FAQ, never computed (one dancer per planner) |
Worked examples — measured from the running page against an independent oracle
| Scenario | Arithmetic | Measured |
|---|---|---|
| Rest: juniors · ballet + jazz + hip hop · concert, uniform, registration on | 570 − 57 + 255 + 120 + 40 | $928 · "3 classes a week · 135 min of dance" · note absent |
| Tiny tots · ballet · registration on / off | 150 + 40 / 150 | $190 · "1 class a week · 30 min of dance" / $150 |
| Tiny tots · ballet + jazz · registration on | 300 + 40 | $340 · note absent (no third style) |
| Teens · ballet + contemporary · nothing else → add tap | 440 → 660 − 66 | $440 + note "$66 back" → $594, note absent, discount line −$66 |
| Adults · all five available styles | 1,000 − 100 | $900 · acro disabled with reason |
| Juniors · all six · all extras on | 1,140 − 114 + 510 + 120 + 40 | $1,696 · "6 classes a week · 270 min of dance" |
| Juniors · hip hop + acro → adults → juniors | 380 → 200 → 380 | acro locked off, then restored |
| Untick everything | — | "$ — · tick a style to price the term", no line 2 |
| Rapid-fire from empty: ballet, jazz, tap, untick jazz in one evaluate | 2 × 190 + 40 | $420 · one sweep counted |
| Cheapest per age | one class, nothing else | $150 · $190 · $220 · $200 = the services-grid floors |
The harness also drove the full extras matrix (4 ages × concert × uniform × registration at two styles, 32 cells) against the oracle, asserted note presence on all three surfaces (badge, ledger, a11y mirror) and note absence where the spec pins it, and read the probe window.__danceschool in-page via JSON.stringify. 310 asserts (304 on the site, 6 on this guide), zero console errors, zero spec corrections — every worked figure is pure anchor arithmetic and reproduced exactly.
Conversion decisions
- Above the fold at 375px: wordmark, the value line naming trade and area in the h1, "Book a free trial class" and "Call", the trust line (first class free · WWC-checked · since 2016) — all inside 667px, with the sticky Call / Book bar under them. The header keeps a
tel:link at every width; under 430px the header CTA hides because the sticky bar already carries it and the masthead row would otherwise overflow. - Why "free trial", not "enrol": a dance parent's first action is a trial class, not a term commitment; the planner prices the term so the trial conversation starts informed. The planner's own CTA reads "Enrol with this plan" and jumps to the form with the plan already written into the message.
- Form fields: parent/guardian name, dancer's first name, phone, suburb, the dancer's age select (pre-filled from the planner), an experience select (first time / some / returning — it picks the level), and the pre-filled plan textarea with an invitation to add a sibling. Front-end validation per field, visible focus states, and a local confirmation panel — hidden on load and on an empty submit, shown only after a valid filled submit, naming the dancer and the planner total. No network round-trip is faked.
- Honest price anchors: the services grid floors ($150 / $190 / $220 / $200) equal the planner's cheapest output per age group; the 10% line, concert pack and uniform pack in the grid are the planner's own numbers.
- The display band repeats the rest-state total in display type so the price is readable from across the room — and it is the measured total, re-derived whenever the rest state changes.
- Reviews are three specific demo stories (the cereal-aisle arabesque, the Kinross solo, the Thursday tap ritual) marked "demo review" — never structured as Review data.
Local SEO we'd implement for the real business
This demo page ships only WebPage / WebSite / BreadcrumbList JSON-LD naming itself a demonstration — fictional business data in a real index would be dishonest markup. For the real studio we would ship the block below. Schema.org has no DanceSchool type, so the honest top-level type is EducationalOrganization (with LocalBusiness as an additional type for the opening-hours and geo fields), plus real reviews as Review with AggregateRating drawn from the studio's actual Google profile.
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "EducationalOrganization",
"additionalType": "https://schema.org/LocalBusiness",
"name": "Studio Eleven Dance",
"description": "Dance school in Joondalup: ballet, jazz, hip hop, contemporary, tap and acro for tiny tots to adults.",
"url": "https://www.studioelevendance.com.au/",
"telephone": "+61 8 9000 0363",
"image": "https://www.studioelevendance.com.au/images/studio-one.jpg",
"address": {
"@type": "PostalAddress",
"streetAddress": "[real street address]",
"addressLocality": "Joondalup",
"addressRegion": "WA",
"postalCode": "6027",
"addressCountry": "AU"
},
"geo": { "@type": "GeoCoordinates", "latitude": -31.745, "longitude": 115.766 },
"areaServed": ["Joondalup", "Currambine", "Kinross", "Edgewater", "Wanneroo", "Iluka", "Ocean Reef", "Padbury", "Heathridge"],
"openingHoursSpecification": [
{ "@type": "OpeningHoursSpecification", "dayOfWeek": ["Monday","Tuesday","Wednesday","Thursday","Friday"], "opens": "13:00", "closes": "20:30" },
{ "@type": "OpeningHoursSpecification", "dayOfWeek": "Saturday", "opens": "08:30", "closes": "13:30" }
],
"hasOfferCatalog": {
"@type": "OfferCatalog",
"name": "Term classes",
"itemListElement": [
{ "@type": "Offer", "name": "Tiny tots (3–5), 10-week term", "price": "150", "priceCurrency": "AUD" },
{ "@type": "Offer", "name": "Juniors (6–12), 10-week term", "price": "190", "priceCurrency": "AUD" },
{ "@type": "Offer", "name": "Teens (13–17), 10-week term", "price": "220", "priceCurrency": "AUD" },
{ "@type": "Offer", "name": "Adults, 10-week term", "price": "200", "priceCurrency": "AUD" }
]
},
"aggregateRating": { "@type": "AggregateRating", "ratingValue": "4.9", "reviewCount": "87" },
"review": [
{
"@type": "Review",
"author": { "@type": "Person", "name": "[real reviewer]" },
"datePublished": "2026-03-02",
"reviewRating": { "@type": "Rating", "ratingValue": "5" },
"reviewBody": "[the studio's real Google review text]"
}
]
}
</script>
Alongside it: a Google Business Profile with the real address and class photos, suburb pages only if the studio genuinely runs classes in a second suburb, and FAQ schema from the real FAQ once the answers are the studio's own.
What changes for the real client
- Enrolments and direct debit run through a studio-management platform — Jackrabbit Class or DanceStudio-Pro are the kind we'd wire — so the planner's "Enrol with this plan" hands the priced plan straight into the real registration flow instead of a textarea.
- Term payments via Stripe (pay-by-term or a direct-debit schedule across the term), with the planner's ledger becoming the line items on the checkout.
- The timetable becomes a Google Calendar embed the studio already edits, replacing the "typical week" table.
- Concert and holiday-workshop news through Mailchimp, with the enrolment form opting families into the concert list.
- Their photos, their reviews feed (Google Business Profile), their real ABN, WWC numbers and syllabus affiliations replace every placeholder-patterned string — and the would-ship schema above goes live with real data.
Techniques
- No libraries. Plain SVG + vanilla JS, all inline. Google Fonts (Syne, Work Sans) are the only external resources; no raster anywhere — the stage, the map, the favicon and every mark are drawn.
- SVG stage with linear/radial gradients for the cone, pool, footlights and floor; a clip path keeps the cone inside the proscenium; the ticket stub is an HTML overlay masked with two radial-gradient notches and a dashed perforation.
- rAF spot loop with a clamped-at-zero delta, paused on
document.hidden, never started under reduced motion; the probe'sspotXis written every frame so liveness can be read from state. - Procedural service-area map: a grid pattern, a gradient ocean, Lake Joondalup, the Mitchell Freeway and eight named suburbs around the studio star.
- Entrance choreography gated on
document.fonts.readywith a 900 ms fallback; nothing is left at opacity 0 without JS (theno-jsclass guards every reveal).
Iteration log
- Pass 1 — Craft. Measured every shipped token pair with WCAG arithmetic: all spec pairs held (chalk 15.62:1, white-on-magenta 5.42:1, magenta-ink 6.99:1, gold 10.82:1);
chalk-dimmeasured 4.75:1 on cards for small meta lines, so it was lifted to#9A91AB(5.48:1). The 375 masthead overflowed to 396px (wordmark + phone + "Free trial" in one row) — the header CTA now hides under 430px, the phone link becomes a "Call" pill and the wordmark drops to .9rem (wordmark right edge 284px vs call left edge 290px, measured). The hero h1 at 2.8rem ran to seven lines in Syne 800 — cut to 2.45rem and 1.55rem on phones; the ghost CTA showed a double space between "Call" and the number (two flex items under a 9px gap) — wrapped in one span, and the number hides under 430px so both CTAs sit in one row above the fold. At 1000px the nav wrapped onto two lines and the header button broke across two — nav now shows from 1120px, every nav link and button is nowrap. The hero fact tiles wrapped "from $150" onto two lines and, once nowrapped, pushed the grid column to 413px at 375 — tiles becameminmax(0,1fr)with "$150 / from, a 10-week term". The proscenium label sat under the ticket stub — left-aligned it. The hours list beside the form ran "Mon–Fri" into "1:00pm" (no gap, a long right-hand cell wrapping left) — added a 16px gap and shortened the cell; the pre-filled plan textarea clipped its fourth line — lifted to 148px. The services heading said "Seven honest prices" above eight cards — rewritten. The empty-state badge wrapped the prompt onto a second line (innerTextread "$ —⏎· tick a style…") — now one string in one span, set in Work Sans so it reads as a prompt rather than a price. With six figures on stage the silhouettes overlapped, so five or more draw at 0.88 scale. - Pass 2 — Depth. The second-read detail: each figure has a gold glow layer driven by its distance from the spot, so the dancers brighten as the light passes and the chalk stroke lifts to full opacity under it. The spot no longer just wanders — a state change sweeps it to the changed figure (exponential ease, token-invalidated in the handler) and on arrival it resumes idling around that figure with the base drifting back to centre over ~8 s. Added a 220 ms footlight flare on every state change (skipped under reduced motion and on first draw). The probe's
spotXwas only written on render, so the liveness assert read two identical values on a moving spot — now written per frame. The confirmation panel names the dancer and the planner total ("…to book Anika's free trial class and confirm the $928 term plan"). The rest state's display band, the planner's badge, the ledger and the confirmation all quote the same measured $928. - Pass 3 — Hardening. Reduced-motion run: spot parked at x=400, probe settled immediately, no sweep, pricing still live. Rapid-fire (ballet, jazz, tap, untick jazz in one evaluate): final $420, exactly one sweep counted, settled once. Phone
patterncompiled under the v flag with zero console errors. At 375: no horizontal overflow, the stub docks beneath the stage, the badge carries the short note form ("a third class takes 10% off — $66 back") while the ledger and a11y mirror keep the full template, every form control ≥44px (inputs 48px), body copy 17px, sticky Call / Book bar present in the bottom 140px and its Book link lands on the form (the harness forces instant scroll before clicking — W16). Keyboard: Enter toggles a style chip and the gold focus ring shows. Label-collision sweep at 375 / 1000 / 1200 native resolution: the stub never covers a figure at any width (asserted against figure bounding boxes at 1000), the spot name sits clear of the feet and the footlights. Chanel rule: removed the unused gold button variant. Final copy read: Australian English throughout, every number on the page traces to a pinned anchor.
Studio Eleven Dance is a fictional business — a dance school industry demonstration designed & built by Sapience Analytics, part of the Generative Assets collection. Back to the site · Index.