← Back to the Linen & Line Curtains demo Index

Build guide · site 351 · Wave 39, Industry Demos XXVIII

How the Linen & Line Curtains demo was built

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.

Concept

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.

Palette — measured, not assumed

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).

#F3EFE8
LinenPage ground — unbleached linen in afternoon light.ground for every ratio below
#2A2622
WalnutBody ink, headings, trust strip, demo band.13.10:1 on linen · white on it 15.01:1
#5A524A
Soft inkSecondary copy, ledger notes, form fine print.6.69:1 on linen · 7.66:1 on white · 6.17:1 on the booking ground
#6B4A5E
Dusty plumPrimary buttons, switch knobs, links, the map ring.white on it 7.59:1 · on linen 6.62:1
#573B4C
Plum fillButton hover, selected chips, the Osborne Park star.white on it 9.84:1
#4F3545
Plum inkLinks, the italic display words, the swing-tag price.9.51:1 on linen · 10.89:1 on white · 8.64:1 on the plum tint
#B7955A
BrassRods, track lines, rules, chip outlines, leader lines, sticky Call ground.2.46:1 on linen — lines only · walnut on it 5.33:1
#7A5C2A
Brass inkEyebrows, the tag name line, note lines, dimension tags.5.40:1 on linen · 6.19:1 on white · 5.79:1 on the tag

Type

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.

Techniques

Signature implementation

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.

Pricing model (pinned anchors)

Main curtain, per metre of window widthTo the sill · 140 cmTo the floor · 240 cmCeiling 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-onAmountApplies
Window minimum$260 per windowmain 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/mlive at every combination; S-fold declared included
Sheer layer+ sheer rate at the drop × widthlive 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/mlive at every combination; replaces the standard track (declared included otherwise)
Motorised track+$450 a windowlive at every combination
Windows 1–8× windowsevery 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-cleaningno number"quoted on the measure" — grid fine print and FAQ
Measure & fit metro-wide; standard track; free samplesincludeddeclaration 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.

Worked examples — measured from the running page against an independent oracle

Scenario (full control vector set explicitly)SpecMeasuredAlso asserted
Rest: 240 cm · to the floor · linen-look · S-fold · sheer layer ON · rod OFF · motor OFF · 1 window$768$768190 × 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$260110 → 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 / $264boundary 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,2701,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 → $768layer 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,218each single add-on from the rest vector
Rest × 4 windows / × 5 windows$3,072 / $3,840$3,072 / $3,840per-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$780minimum 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)oracleall equalevery cell matched the independently reimplemented oracle

Conversion decisions

Local SEO we'd implement for the real business

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.

What would change for the real client

Iteration log

Pass 1 — Craft

  • Measured every palette pair (table above). Plum cleared 4.5 both ways so buttons and links keep the argued hue; the pre-minted fill and ink are used for selected chips, hover and coloured copy. Brass demoted to lines and marks; brass-ink for tags, eyebrows and note lines.
  • First 1200 screenshot: the window sat small in a wide empty wall and the drop tag ran off the left edge — scale lifted 1.35 → 1.4 units/cm, window left edge moved to 70 units, stage height 560 → 540, and a ceiling gradient above the cornice replaced a blank pale band. Linen and blockout fills deepened (they had matched the wall) and every panel gained an offset shadow on the wall.
  • Swing tag: the name line "MADE TO MEASURE · 1 WINDOW" wrapped — now "Made to measure" at one window and "3 windows · total" above; the tag hangs from the track end with the string drawn to the hole rather than overlapping the panel edge.
  • Map: the reach-ring caption sat above the viewBox, the Osborne Park label ran into Mount Lawley and Midland, and the samples tag overflowed — caption moved to the top-right, label centred above the star, two dots moved, tag widened.
  • Copy read: hero eyebrow shortened to "Osborne Park showroom" so it never wraps at 375; the sheer-layer switch prints the live rate for the selected drop ("+$130/m at this drop").

Pass 2 — Depth

  • Enriched the fabric: S-fold's top wave and soft fold gradient, pinch pleat's heading band, three-line fans and falling fold lines with a sharper stripe, blockout's doubled fold shadow and stroked lining edge, linen's slub hairlines, sheer's white fold stripe over the garden.
  • Second-read details: the swing tag rotates ±1.4° on a slow alternate loop around its hole; the sheer layer's hem is a segmented wave out of phase with the main panels; a soft floor shadow appears under floor-length hems and disappears at the sill drop; the motor housing carries a small "MOTOR" label and a plum LED.
  • Micro-interaction: chip and stepper changes redraw snappy (460 ms settle) after the first 1.4 s staggered entrance; the ledger, tag and mirror list re-render in the same pass so all four surfaces agree instantly.

Pass 3 — Hardening

  • 375px: masthead measured (wordmark right 234 vs call left 318), zero overflow offenders, sticky bar fixed with a tel: link, tall layout selected. Native-res 375 screenshots at rest and at 100 cm · sill · sheer × 5 checked the tight tag (short subtitle and short note templates) and the tag/panel intersection at rest measured 0 px². Drawn cap on the tall layout (317 units) keeps the tag beside the window at any width; the wide layout tag clamps inside the stage and overlaps the outer panel below its heading only at 380–400 cm (27–44px of a 120px panel at 1200 wide) — a swing tag hung over the leading edge, documented rather than capped so the last two stepper taps still grow the drawing.
  • 1000px mid-width check: 176px tag hangs clear of the panels; 1200 rest: stage top 265px and tag bottom 512px both inside the first 750px.
  • Label-collision sweep at native resolution (rest, 400 ceiling blockout pinch rod motor, 100 sill sheer): width tag on the floor slice, drop tag left of the track, motor housing at the left track end and remote on the sill never meet the swing tag or each other.
  • Reduced motion: settled frame at t = 1.7 s, layers shown without stagger, tag static; probe settles in 20 ms. Visibility pause and resize re-layout (wide ↔ tall) verified. Zero console errors across load, every drive and the rapid-fire run (fourteen dispatched clicks in two evaluates → settled on 260 · floor · linen · pinch · sheer ON · rod · motor × 3 = matched the oracle).
  • Form: hard reload → empty submit raises four field errors and the panel stays hidden; filled submit shows the panel. Keyboard arrows move the drop chips. No pattern attributes — phone validation is JS.
  • Harness: 686 asserts across index and guide — every worked example, the 18-cell matrix, three-surface notes with explicit absence, restore paths, prefill, 375 masthead, reduced motion, guide h1/overflow/table-scroll/colophon/panel.

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.