Build notes · site 285 · Wave 25 — Industry Demos XIV

How The Careful Press was made

A fictional Nedlands dry cleaner, built as a conversion demonstration: one landing page whose job is a pickup booking, argued around the trade's most honest object — the counter ticket.

Concept

The Careful Press is a fictional dry cleaning & garment care business for Perth's western suburbs, aimed at shirt-cycling professionals, wedding parties and doona owners. The page has one job — book a pickup — and one signature: the Garment Rail, a conveyor that hangs your garments in lilac poly bags while a mono counter ticket prices them live, with the two rules that matter (the 5-shirt batch re-rate and the $40 free-delivery line) printed on the ticket itself, not in fine print.

Palette — pearl-and-lilac steam morning

The collection's first lilac-led industry site. Every colour is off the shop floor: the finished shirt, the press frame, the trade's own garment bags, the heated plates.

Pressed cotton#F7F5F0

Ground — the finished shirt under counter light.

Press iron#33313B

Structure and primary ink — the machine's enamelled frame.

Garment lilac#7A5EA8

Line/mark — the poly bags: rail, ticket rules, CTA. Never body text.

Copper press#B4642F

Secondary — heated plates and the counter bell. Chip-scale only.

Ink/line split (LEARNINGS W2/W5): raw lilac measures ≈4.9:1 and raw copper ≈4.0:1 on pressed cotton, so both are marks-only. Derived inks for small text: lilac-ink #5A4287 (≈7.6:1) and copper-ink #8A4519 (≈6.6:1).

Type

Techniques & the signature

Fully procedural single file: SVG illustration (rail, press, map), CSS gradients, no raster assets, no libraries. The Garment Rail is an SVG conveyor: each stepper tap builds a hanger group in JS — hook curling over the rail line, wire shoulders, a translucent lilac bag with a garment line-drawing inside, a copper-strung price tag swinging beneath, and a mono hook number (#113 onward, because the SMS "your shirts are on hook 113" is the trade's real UX). New hangers glide in from the right on a 950ms entrance ease at load and a 300ms snappy transition thereafter (dual-speed, LEARNINGS W21 — the inline entrance transition is handed back to the class rule once landed). When more than eight hooks hang, the conveyor translates left and older garments disappear into the press with an "in · N" counter. The ticket re-renders per interaction from a single compute(); window.__drycleaning exposes counts, totals and a settled flag QA gates on. Reduced motion hangs everything instantly, settled and complete.

The pricing model's shape

Garment subtotal = Σ qty × rate, where the shirt rate is $6.00 until the fifth shirt re-rates the lot to $5.50 (one uniform rule — the press runs in batches). Express multiplies garments only by 1.5. The delivery threshold reads the express-adjusted garment value — "everything on the ticket except the delivery itself, express included" — free at $40 and over, else +$8. Totals round to the nearest 5 cents (Math.round(v×20)/20); with the pinned price book the rounding almost never fires, but the rule is stated on-ticket so the arithmetic is auditable. The dress chip stays live at the $24 plain-dress rate with "beaded & formal quoted on sight" on the card (LEARNINGS W18 — a live control with a visible honesty statement); the wedding gown is its own $340 chip, not a dress variant.

Measured verification — every crossing, oracle-checked

The harness reimplements the price book independently from the spec (LEARNINGS W14) and drives the real steppers/toggles, asserting the displayed ticket at each boundary. All figures below are measured from the running page; zero corrections against the spec's worked examples.

ScenarioOracleDisplayedVerifies
4 shirts (walk-in)$24.00$24.00base shirt rate
5 shirts (walk-in)$27.50$27.50uniform 5+ re-rate to $5.50
Rest: 4 shirts + 2-pc suit + dress, pickup on$69.00$69.00 · FREErest pin, delivery free ≥ $40
Rest + express$103.50$103.50 · FREE+50% on garments only
2 shirts + trousers, pickup on$32.00$32.00$24 < $40 → +$8
2 shirts + trousers, express$44.00$44.00$36 still < $40; express counts toward threshold
… add a 2-pc suit$45.00$45.00 · FREEcrossing releases the $8
2 jackets + trousers$40.00$40.00 · FREEexact boundary — "at $40 and over"
1 shirt, walk-in$6.00$6.00cheapest output = services-grid floor
Documented gap (flagged): the express toggle's "in by 9am" cut-off is copy, not logic — the toggle prices honestly at any hour, and a real build would gate it against the clock (untestable headlessly without mocking time, LEARNINGS W23). The branch is deliberately trivial and declared here.

Conversion decisions

Local SEO we'd implement for the real business

On a real client's site this page would carry DryCleaningOrLaundry schema (the tightest schema.org type for the trade) with genuine reviews wired to their Google profile. Shown here as a sample — never emitted live on a fictional page:

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "DryCleaningOrLaundry",
  "name": "The Careful Press",
  "telephone": "+61-8-XXXX-XXXX",
  "priceRange": "$6–$340",
  "address": {
    "@type": "PostalAddress",
    "streetAddress": "(real street address)",
    "addressLocality": "Nedlands",
    "addressRegion": "WA",
    "postalCode": "6009",
    "addressCountry": "AU"
  },
  "openingHoursSpecification": [
    { "@type": "OpeningHoursSpecification",
      "dayOfWeek": ["Monday","Tuesday","Wednesday","Thursday","Friday"],
      "opens": "07:30", "closes": "18:00" },
    { "@type": "OpeningHoursSpecification",
      "dayOfWeek": "Saturday", "opens": "08:00", "closes": "13:00" }
  ],
  "areaServed": ["Nedlands","Dalkeith","Claremont","Cottesloe","Subiaco","Mosman Park"],
  "aggregateRating": { "@type": "AggregateRating",
    "ratingValue": "4.9", "reviewCount": "87" },
  "review": [{
    "@type": "Review",
    "author": { "@type": "Person", "name": "(real reviewer)" },
    "reviewRating": { "@type": "Rating", "ratingValue": "5" },
    "reviewBody": "(pulled from the client's live Google reviews)"
  }]
}
</script>

What would change for the real client

Iteration log

Pass 1 — Craft

Looked at the 1200×750 rest capture: the ticket's shirt line truncated to "4 × business shirt…", ellipsing away the unit rate — shortened ticket labels to "4 × shirts @ 6.00" (the rate is the point of the line). The rail read sparse right of centre, so hook spacing widened 66→74px and the first slot moved to 145px (max eight visible hooks before the press swallows overflow). Fixed a malformed hook path from the first build, and split marks from inks: raw lilac/copper demoted to lines and chips, with derived lilac-ink/copper-ink for all small text.

Pass 2 — Depth

Added the second-read details: mono hook numbers on every hanger, the "in · N" press counter when the conveyor carries garments off-stage, and a counter-bell ding (500ms rotate keyframe) whenever the ticket total changes. Enforced dual-speed redraw — the 950ms entrance transition is handed back to the 300ms class rule after landing, removals fade in 180ms, and the settle cap after any interaction is 650ms. Re-drove every pinned crossing against the independent oracle: 4/5-shirt re-rate, the $32/$44/$45 delivery sequence, the exact-$40 boundary, rest and rest-express — all matched, zero corrections.

Pass 3 — Hardening

375px masthead first (LEARNINGS W18/W21): wordmark right edge 197px vs phone-link left edge 215px — no collision; nav and header CTA drop cleanly, scrollWidth holds 375 on both pages. Sticky call bar sits flush at the bottom with 48px tap targets. Fresh-load empty submit raises four invalid fields with visible messages; filled submit confirms in the demo lexicon. Reduced-motion path hangs all six rest garments instantly, settled at $69.00. No pattern attributes (v-flag risk avoided — phone length checked in JS with setCustomValidity). Chanel rule: removed the ticket's decorative perforation background that read as noise. Zero console errors across the full drive.