Build guide · site 351 · Wave 39, Industry Demos XXVIII
A fictional made-to-measure curtain business — curtains, sheers, tracks, rods and motorised tracks, measured and fitted across Perth metro from a showroom and workroom in Osborne Park. The page has one job: a measure-and-quote request with the window, fabric, heading and track already priced per window.
The audience is the couple who just moved into a new build with bare windows, the owner whose west-facing bedroom needs blockout, the renovator who wants sheers under linen on one track, and the downsizer replacing thirty-year-old pencil pleats. Curtain pricing is confusing from the outside — per metre of what, at which drop, is the track extra — so the page prices one window openly: width × a per-metre rate at the chosen drop, with the measure, the fitting and a standard track declared included. It is deliberately distinct from the collection's blinds demo (rollers and plantation shutters): this is soft furnishings on tracks.
The direction is the showroom in afternoon light — linen ground, walnut ink, dusty-plum CTAs, brass linework for tracks, rods and rules — soft, tactile, unhurried, like a sample held up to a window. The signature is the Window Dresser: a drawn window in a drawn wall with cornice, skirting and a slice of oak floor, curtains hung on a drawn track. The width stepper resizes the window; the drop chips move the hem to the sill, to the floor, or lift the track to the cornice; the fabric chips redraw sheer (the garden shows through), linen-look (opaque with a slub) or blockout (heavier folds and a lining edge); the heading chips redraw an S-fold wave or pinched fans; the toggles draw a second sheer on an inner track, a brass rod with rings and finials, or a motor housing on the track and a remote on the sill. A breeze lifts the hems on a loop, and the price hangs from the track as a fabric swing tag.
Every ratio below was computed by WCAG relative-luminance arithmetic from the shipped hex tokens in Pass 1 (the spec's figures were hypotheses; they landed within 0.1). Dusty plum #6B4A5E measured 6.62:1 on the linen ground and 7.59:1 under white labels — no two-token remedy needed for buttons or links; selected chips and button hover use the deeper fill #573B4C (9.84:1) and plum-coloured copy uses the ink #4F3545 (9.51:1 on linen, 10.19:1 on the swing tag). Brass #B7955A is 2.46:1 on the ground and is used only for lines and marks; brass-coloured copy uses #7A5C2A (5.40:1 on linen, 5.79:1 on the tag).
Two-forty wide, to the floor, linen over sheer
Cormorant Garamond 600 (with a 500 italic) for the wordmark, headings, the display line and the tag price — a fabric-house serif with soft, high-contrast strokes. The display moment sets the window huge above the drawing in tracked caps and re-words itself with the controls, ending on the one italic phrase: linen over sheer · blockout · sheer.
Figtree 400–700
Figtree for everything read: body at 17px on mobile and 18px from 760px, chips, ledger, form. Weight carries hierarchy (700 legends and buttons, 600 chips, 500 fine print); tracking only on the uppercase eyebrows and the tag's name line.
<g class="layer"> so the first draw staggers them in.geom() derives everything in centimetres × scale: the ceiling 270 cm above the floor, the track 240 cm above the floor (or 5 units under the cornice for ceiling-to-floor), the window head 20 cm below the track, the sill 100 cm above the floor, 15 cm of track overhang each side, panels stacked back to 30% of the track width, the hem 2 units above the sill or 1 above the floor. Drawn cap: the window is left-anchored and grows to the right; on the tall layout it caps at 317 units (about 288 cm) so the swing tag always hangs beside it, and the dimension tag keeps printing the true centimetres — one window is drawn whatever the windows stepper says, and the tag prints the count.panelD() builds each panel as a straight outer edge, a cubic inner edge whose foot sways with the breeze, and a quadratic hem that lifts at the inner corner. The same d fills four stacked paths: fabric gradient, a userSpaceOnUse fold pattern (a soft 26-unit gradient stripe for S-fold, a sharper 22-unit stripe for pinch pleat, a second copy for blockout's heavier shadow), a slub pattern of short horizontal hairlines for linen-look, and an offset shadow on the wall. Blockout adds a stroked lining edge along the inner curve. The heading is a separate stroked path — a wave along the top for S-fold, three-line fans from a pinch point plus a falling fold line for pinch pleat.dt = min(max(now − last, 0)/1000, .05) drives sway (two sines per side, amplitude 7 for sheer, 4.5 for linen, 3 for blockout), hem lift, and the inner sheer's segmented hem wave. Panel opacity is always 1 and the sheer fill never drops below 0.58 — the never-invisible floor is structural. Reduced motion renders one settled frame at t = 1.7 s; the loop pauses on document.hidden.hidden with the [hidden]{display:none!important} guard, never the IDL property): the sheer layer, the rod (which hides the track group), and the motor housing + remote fade in via a CSS animation on the group.placeBadge() hangs it from the track's right end (clamped inside the stage) and draws the string in SVG from the track end to the hole, so a clamped tag reads as swung sideways. It rotates ±1.4° on a slow alternate loop. Three width variants: wide (200px), mid 900–1099px (176px, smaller price), mobile (122px, short subtitle and note templates).<style>, Google Fonts the only external request.State is one object — width, drop, fabric, heading, three toggles, windows — and compute() is a pure function over it: main line = rate[fabric][drop] × width, billed at $260 when the arithmetic is under; pinch pleat +$15/m; the sheer layer at the sheer rate for the selected drop × width, only when the fabric is not sheer and the remembered choice is on; rod +$40/m; motor $450; per-window sum × windows; the whole-house note at five. Every rate is a multiple of 5 and every width a multiple of 20 cm, so every line is a whole dollar by construction. render() writes the result to every surface in one pass: the display line, the swing tag (name, price, subtitle, per-window line, notes), the scene (geometry, panels, marks, tags), the sheer switch (disabled with the on-card reason on sheer, aria-checked mirroring the effective state), the itemised ledger and the plain-text mirror list under the drawing. Restore-vs-reset is by memory: state.sheerLayer is never overwritten by the sheer chip, so sheer → linen-look or blockout restores the prior choice exactly. Every handler bumps a scene token before touching state; the settle timer publishes window.__curtains.settled only for the latest token, so a rapid-fire run of clicks ends on one definite, settled state.
| Main curtain, per metre of window width | To the sill · 140 cm | To the floor · 240 cm | Ceiling to floor · 270 cm |
|---|---|---|---|
| Sheer | $110/m | $130/m | $145/m |
| Linen-look | $160/m | $190/m | $210/m |
| Blockout, lined | $190/m | $230/m | $255/m |
| Rule / add-on | Amount | Applies |
|---|---|---|
| Window minimum | $260 per window | main curtain line only — when rate × width < 260 the main line bills $260 with the note "billed at the $260 window minimum — a small window is cut, made and hung like a big one" (mobile tag short form: "$260 window minimum applied"); pleat, sheer layer, rod and motor are additive on top |
| Pinch pleat heading | +$15/m | live at every combination; S-fold declared included |
| Sheer layer | + sheer rate at the drop × width | live on linen-look and blockout; DISABLED on sheer with "the sheer IS the layer — pick linen-look or blockout to layer a sheer under it"; the switch remembers its choice and RESTORES on return; not subject to the $260 minimum |
| Decorative rod | +$40/m | live at every combination; replaces the standard track (declared included otherwise) |
| Motorised track | +$450 a window | live at every combination |
| Windows 1–8 | × windows | every per-window line multiplies — main (after minimum), pleat, sheer layer, rod, motor; ≥ 5 adds the NOTE "five or more windows we price as a whole-house job on the measure — usually a little under this figure" (short form: "5+ windows: whole-house price on the measure"), no price change |
| Pelmets, Romans, re-hanging, dry-cleaning | no number | "quoted on the measure" — grid fine print and FAQ |
| Measure & fit metro-wide; standard track; free samples | included | declaration lines, never line items |
Totals are integer by construction and display to the whole dollar. The estimator's cheapest outputs equal the services grid floors: made-to-measure curtains from $260 a window; sheers from $110/m; linen-look from $160/m; blockout from $190/m.
| Scenario (full control vector set explicitly) | Spec | Measured | Also asserted |
|---|---|---|---|
| Rest: 240 cm · to the floor · linen-look · S-fold · sheer layer ON · rod OFF · motor OFF · 1 window | $768 | $768 | 190 × 2.4 = 456 + 130 × 2.4 = 312; display "TWO-FORTY WIDE, TO THE FLOOR, linen over sheer"; no notes on tag, ledger or mirror; per-window line absent |
| 100 cm · sill · sheer · all OFF · 1 | $260 | $260 | 110 → billed 260, minimum note on all three surfaces; sheer layer disabled with reason on switch, ledger and mirror; grid floor |
| 200 cm · floor · sheer / 220 cm · sill · sheer / 240 cm · sill · sheer | $260 / $260 / $264 | $260 / $260 / $264 | boundary three ways: exactly 260 no note · 242 → 260 + note · 264 no note |
| 400 cm · ceiling to floor · blockout · pinch pleat · sheer layer · rod · motor · 1 | $2,270 | $2,270 | 1,020 + 60 + 580 + 160 + 450; motor housing, remote, rod, rings, fans and lining edge drawn |
| Rest → chip to sheer → chip back to linen-look | $312 → $768 | $312 → $768 | layer locked OFF (memory kept), no minimum note at 312; layer RESTORED on return; sheer → blockout also restores ($864); the OFF-memory path restores off ($456) |
| Rest + pinch pleat / + rod / + motor | $804 / $864 / $1,218 | $804 / $864 / $1,218 | each single add-on from the rest vector |
| Rest × 4 windows / × 5 windows | $3,072 / $3,840 | $3,072 / $3,840 | per-window line "$768 a window × 4"; whole-house note absent at 4, present at 5 on all three surfaces |
| 100 cm · sill · sheer × 3 windows | $780 | $780 | minimum multiplies per window; the note appears once in the ledger |
| 18-cell matrix sweep at 300 cm × 2 windows (drop × fabric × heading, rod on pinch, motor on blockout) | oracle | all equal | every cell matched the independently reimplemented oracle |
The live page carries only WebPage + WebSite + BreadcrumbList JSON-LD naming it a demonstration — fictional business data in a real search index would be dishonest markup. For the real Linen & Line we would ship a HomeGoodsStore entity (the exact schema.org organization subtype for a curtain showroom) with opening hours, geo, price range, the service catalogue and a reviews feed:
{
"@context": "https://schema.org",
"@type": "HomeGoodsStore",
"@id": "https://linenandline.example/#showroom",
"name": "Linen & Line Curtains",
"image": "https://linenandline.example/showroom.jpg",
"url": "https://linenandline.example/",
"telephone": "+61 8 9000 0351",
"priceRange": "$260–$2,270 per window",
"address": {
"@type": "PostalAddress",
"streetAddress": "Scarborough Beach Road",
"addressLocality": "Osborne Park",
"addressRegion": "WA",
"postalCode": "6017",
"addressCountry": "AU"
},
"geo": { "@type": "GeoCoordinates", "latitude": -31.900, "longitude": 115.812 },
"openingHoursSpecification": [
{ "@type": "OpeningHoursSpecification", "dayOfWeek": ["Monday","Tuesday","Wednesday","Thursday","Friday"], "opens": "09:00", "closes": "17:00" },
{ "@type": "OpeningHoursSpecification", "dayOfWeek": "Saturday", "opens": "09:00", "closes": "13:00" }
],
"areaServed": ["Osborne Park","Joondalup","Scarborough","Subiaco","Perth","Midland","Fremantle","Canning Vale","Rockingham","Mandurah"],
"hasOfferCatalog": {
"@type": "OfferCatalog",
"name": "Made-to-measure curtains and tracks",
"itemListElement": [
{ "@type": "Offer", "itemOffered": { "@type": "Service", "name": "Made-to-measure curtains, measured and fitted" }, "priceSpecification": { "@type": "PriceSpecification", "minPrice": 260, "priceCurrency": "AUD" } },
{ "@type": "Offer", "itemOffered": { "@type": "Service", "name": "S-fold sheers" }, "priceSpecification": { "@type": "UnitPriceSpecification", "price": 110, "priceCurrency": "AUD", "unitText": "per metre of window width" } },
{ "@type": "Offer", "itemOffered": { "@type": "Service", "name": "Motorised track" }, "priceSpecification": { "@type": "PriceSpecification", "price": 450, "priceCurrency": "AUD" } }
]
},
"aggregateRating": { "@type": "AggregateRating", "ratingValue": "4.9", "reviewCount": "212" },
"review": [
{
"@type": "Review",
"author": { "@type": "Person", "name": "P. — Carine" },
"reviewRating": { "@type": "Rating", "ratingValue": "5" },
"reviewBody": "Lined blockout to the floor on a ceiling-fix track and the west bedroom is dark at three in the afternoon."
}
]
}
Alongside the markup: a Google Business Profile with the same NAP and hours, per-service landing sections with the per-metre anchors, a reviews feed synced from Google, and location copy that names the metro corridors the fitters actually cover.
pattern attributes — phone validation is JS.This is a demonstration build by Sapience Analytics. Want one pointed at your customers?
Linen & Line Curtains is a fictional business — a curtain and track industry demonstration designed & built by Sapience Analytics, part of the Generative Assets collection. Back to the site · Index.