Build notes · Generative Assets

Fig & Feather Beauty

A lead-generation page for a fictional Mount Lawley salon, built to answer the one question a beauty price list never answers: not what it costs, but how long you will be in the chair. Open the page · back to the index.

The concept

Publish the clock, not just the price

Fig & Feather is three rooms above a shopfront in Mount Lawley — two therapists, one part-timer, bookings by phone and DM. The customer arriving on the page has a wedding, a work event or a free Saturday, and one question no salon Instagram answers: how much is all of it, and how long will I be in the chair. Every salon in Perth publishes a price list and hides the minutes, so people book two services into a lunch hour that needs ninety, and everybody’s day falls over.

The page’s single job is to let someone build the whole visit before they ring, and see both numbers at once. The seller-costing part is in the arithmetic rather than in a slogan: a facial and a lash lift booked together share fifteen minutes of processing time, so the visit is 105 minutes instead of 120 — and the price does not move.

The proof element

Build your appointment

Six service chips. The card rules the chosen services in at the top, prices the ones you did not choose underneath at the same size, prints the running total and the running chair time at equal weight, and lays the visit out on a fixed four-hour scale against the gap it would have to fit into.

AppointmentPriceChair timeNext gap long enough
Brows + lash lift + gel manicure (the page at rest)$2052 h 10 · 130 minThursday 9:00am
Facial + lash lift, booked together$2101 h 45 · 105 minWednesday 11:00am
The same pair booked apart$2102 h 00 · 120 minThursday 9:00am
Minutes the shared processing gives backno change15 min
Brows + lash + facial + gel$3202 h 55 · 175 minFriday 10:30am
Leg wax + underarm$10355 minTuesday 9:15am
Underarm wax alone — the smallest booking$2810 minTuesday 9:15am
All six in one sitting$4233 h 50 · 230 minnothing this week holds it

Every figure above is computed by check-w54.mjs from the pinned menu and asserted against the concept spec, then driven again in a real browser across all sixty-four chip combinations. The services grid’s lowest “from $” and the card’s smallest booking are the same $28, deliberately.

The overlap rule lives in the arithmetic, not the copy

A sixty-four-subset sweep runs in the browser on every pass. It asserts that the fifteen-minute deduction fires on the facial-plus-lash-lift pair and on nothing else, that it never moves the price by a cent, and that price and minutes are correct on every other combination. A sentence claiming a discount is marketing; a sweep proving the model does it is a demonstration. The negative test in pass three confirms the sweep can actually fail: a visit that did not apply the overlap reads 120 minutes, and the assert catches it.

The availability strip is sample data, and the page says so twice

The week of gaps is written into the page. It is not connected to a diary, a calendar or a booking system, it holds nothing, and no appointment is ever made on this page. It is labelled “sample availability” beside its own heading, explained again in the paragraph underneath it, and answered again in the FAQ. For a real salon this is the one panel that would be wired to live data on day one — see the integrations note below — but a demonstration page has no business implying a connection it does not have.

Palette

Oat-blush paper, espresso band, a rose for money and a sage for time

No gold gradients and no rose-gold foil — the two clichés of the category. The discipline is that the two accents carry meaning rather than decoration: rose is always a price and sage is always a duration, everywhere on the page. Every hex and every ratio below is quoted from the same row of contrast-w54.log, the wave’s measured sweep. No colour was minted during this build.

#FBF6F2Paper. Body ink measures 15.58:1 on it.
#F0E2DAPanel — cards, tiles, the appointment card. Ink 13.21:1.
#2C2220Espresso band. Paper reversed out of it 14.42:1.
#1E1615Band hover and the footer. Paper reversed 16.57:1.
#241C1ABody ink. 13.87:1 on the map land, 12.8:1 on a rested slot.
#5D504CSecondary ink — 7.2:1 paper, 6.11:1 panel, 5.92:1 slot-rest. Those three only.
#8E3040Rose ink — every price. 7.39:1 paper, 6.27:1 panel, 6.2:1 on a held slot.
#3A5A32Sage ink — every duration. 7.28:1 paper, 6.17:1 panel, 6.36:1 on an open slot.
#A63C4ERose mark — the chair-time blocks’ edges. 5.79:1 paper, 4.86:1 slot-held.
#4A7040Sage mark — the handed-back block. 5.31:1 paper, 4.64:1 slot-open.
#E79CA6Rose signal. Dark grounds only — 7.15:1 on the band.
#9CC98CSage signal. Dark grounds only — 8.22:1 on the band.
#E3EADFA gap that fits, and the time handed back.
#F4DFD9The gap we would hold, and the booked chair time.
#E7E0DBA gap too short. Ink 12.8:1, secondary ink 5.92:1.
#4C6B62Map coast and the catchment ring — 4.86:1 on land, 4.19:1 on water.

Colour is never the only carrier of state. A selected chip changes its ground, its glyph (a tick for a plus) and its word (“On the card” for “Add”). A service not booked changes its ink, its rule to a dashed one, and prints “add $115” rather than “$115”. A gap block carries “Next”, “Fits” or “Short” in words as well as a ground. Anyone reading this page in greyscale gets the same answer.

Type

One editorial serif, one workhorse, tabular figures everywhere

  • Playfair Display 700 for the headline, the section heads and the two big result figures. One restrained editorial serif is the whole beauty-room reference — no italic swashes, no script face, nothing that reads as a wedding invitation. It is used at three sizes and nowhere else.
  • Inter 400/500/600/700 for everything a customer has to read quickly: body copy, the price list, the chips, the availability strip, the form. Body sits at 17px, never below 16px on a phone.
  • Tabular numerals on every price, every duration and every clock time, so a column of figures lines up and a total that changes does not shift the layout under your thumb.
  • The two result figures are set at identical size and weight — the whole argument of the page is that the money and the minutes are equally important, so the typography is not allowed to rank them.

Technique

How the appointment card is drawn

One self-contained HTML file, no libraries, no build step, no raster images, 72 KB. The card is a single inline SVG on a 520×340 user-unit grid, driven by about 120 lines of plain JavaScript.

  • The ruled rows are six static SVG groups. Choosing a service does not add or remove a row — it re-orders all six by translating each group to 28 × index, repaints the name and the glyph, and swaps the price text between “$115” and “add $115”. A heavier rule marks the boundary between booked and not booked and moves with it.
  • The chair-time bar is a fixed four-hour axis, not a fitted one, so the same visit is the same length whatever else is on the card. The pale block underneath is the gap the salon would hold; the blush blocks on top are the services; a hard marker sits at the end of the gap. When nothing in the week is long enough, the booking visibly runs past the marker and the label switches from “THU 9:00AM HOLDS 2 H 10” to “LONGEST GAP THIS WEEK IS 3 H 30”.
  • The fifteen-minute overlap is drawn where it happens: the second service of the pair is drawn fifteen minutes short, and a paper-coloured block with a dashed sage edge is drawn in the space it gave up, labelled in the card. The picture and the arithmetic are the same object.
  • The service-area map is inline SVG with a dashed catchment ring. At phone widths the far suburbs drop out and the frame zooms onto the ring — the viewBox is swapped on a matchMedia change, not on a scroll or a timer — so the surviving labels render at twelve real pixels instead of six.
  • Nothing on this page animates on a clock. There is no requestAnimationFrame, no setInterval and no frame accumulator anywhere in the file — the entrance is a one-shot CSS animation that comes to rest, and everything else moves only when a person touches it. The state probe is byte-identical at seven seconds and at fifteen.
  • State probe: window.__beautysalon exposes the selected service keys, the price, the minutes, the minutes if booked apart, the overlap actually applied, and the chosen gap — as plain numbers, so the checks read state instead of scraping text.

Iteration log

Three passes, each one driving the card and the form

Pass 1 — Craft

  • The h1 did not name the suburb. It read “Brows, lashes, skin and nails.” and left Mount Lawley to the eyebrow. Rewritten to name the services and the suburb in one line, and the eyebrow cut to “Since 2016 · Mount Lawley” so it stops wrapping at 375.
  • The service chips rendered the price column before the service name, at every width. .c-fig had a row span and no explicit column, so CSS grid auto-placement put it in column two and pushed the name into column three. Every chip child now has an explicit row and column, and the name takes its own line with the money, the minutes and the state word underneath.
  • The two result sub-labels wrapped to two lines in a 150px column at 375. Cut to “3 services” and “130 minutes”.
  • Card type went from 15/14 to 16/15 user units: the 520-unit card renders at 0.64 scale on a 375 screen, and everything in it was being read at ten pixels.
  • At 1440 the three-across price list left the hero column some 400px shorter than the proof column. Two-across balances them.
  • The availability strip’s full day names overflowed a 76px column inside the 510px proof card. Three letters in the column layout, full names in the phone row layout; gap words cut to Next / Fits / Short.
  • SVG text does not wrap, and both bar-note strings were running off the edge of the card. Shortened and measured against the 472-unit content width.
  • “the salon down the way” cut from a heading — a street-type noun inside an idiom, on a page whose address rule is suburb-level only, and a weak line besides.
  • The map was rebuilt. The old 760×520 frame rendered 780px tall at 1440, and four labels sat on their own dots or on each other. New 900×460 frame, every label offset and every position checked.
  • Form driven three ways: empty (no confirmation, four errors shown), junk in the phone field (no confirmation, only the phone error, field flagged), good data (confirmation with a ticket carrying $205 and 2 h 10). Card driven through the rest state, the pair, the full visit, the smallest booking and the empty card.

Pass 2 — Depth

  • The services you did not choose are now priced inside the card, at the same size as the ones you did. All six stay ruled on at all times: booked at the top in full ink, the rest underneath a moving divider, carrying the same minutes and the same price with the word “add” in front of the money. A customer who can see what they turned down reads the card as a model rather than a pitch.
  • The second read: the gap is drawn on the same axis as the booking. The pale block underneath the chair time is the gap the salon would hold, with a hard marker at its end. It costs no vertical space and it makes the overrun state — all six services, 3 h 50, nothing in the week long enough — a picture instead of a sentence.
  • A dead toggle, found by looking rather than by asserting. hidden is an IDL property of HTMLElement; an SVGElement does not have one, so svgText.hidden = false wrote a JavaScript expando and did nothing at all. Every SVG toggle on the card was inert: the “15 MIN HANDED BACK” label had never once appeared, and the divider rule never left its markup position. The fix is to toggle the attribute, which [hidden]{display:none!important} styles perfectly well on SVG. Nothing in the state probe could ever have seen this — it took opening the drawing in five states and reading it.
  • The handed-back block was re-coloured from the “fits” green to paper with a dashed sage edge, so it reads as time returned rather than time held.
  • A press micro-interaction on the chips, and nothing else — the page is a calm one.
  • Card driven through all sixty-four chip subsets, asserting price, minutes, and that the overlap fires on the facial-plus-lash pair and nowhere else. Form driven three ways again after the changes.

Pass 3 — Hardening

  • The Chanel cut: the proof element had three names stacked on each other — an eyebrow reading “THE APPOINTMENT CARD”, an h2 reading “Build your appointment”, and the drawing’s own header reading “appointment card”. The eyebrow went, and bought 26px above the fold.
  • At 375 the map labels were rendering at six pixels. The frame now zooms onto the catchment ring at phone widths and the surviving labels are set larger in user units — then the zoom was found to clip “Swan River”, the right-anchored “North Perth”, and the cap-height of “MOST OF THE DIARY”. Widened twice more and re-measured until every visible label is inside the frame and at least nine pixels tall.
  • Negative tests, in the same sitting, because a check that has never failed is not evidence. Moving the refusal block into the FAQ trips all three of its placement asserts. Growing the sticky bar to 300px at 375×667 makes the above-fold assert fire on the CTA and the trust line — on the shipped page it passes with the bar at 602 and the trust line finishing at 458. A facial-plus-lash visit with the overlap not applied reads 120 minutes and fails the sweep.
  • The match count of every breakpoint selector is asserted, not assumed: the seven far map labels, the two masthead phone labels, the masthead CTA, the nav, the five strip days and the six price tiles all return a non-empty NodeList.
  • Asserted mechanically: no requestAnimationFrame, no setInterval, no performance.now, no .hidden assignment on an SVG node, and the page under 120 KB.
  • Twenty-six tab stops, every one painting a visible :focus-visible outline. Under prefers-reduced-motion: reduce the whole page is settled and fully visible 300ms after load. Crossing the map breakpoint in both directions produces no console error and restores the wide frame.
  • Form driven three ways a third time; card driven through the sixty-four subsets a third time. 82 page asserts and 22 hardening asserts green.

Why this converts

The call button and the enquiry anchor are both above the fold at 375 and both repeated in a sticky bar, so the two things a customer might want to do next are never more than a thumb away, and the headline, the primary button and the trust line are each measured to finish above that bar rather than under it. The form asks four questions and no more — name, phone, which day, what you want — with the last one pre-filled from the appointment card, because a customer who has already built their visit should not have to type it out again to ask about it. The proof element answers the question the category refuses to answer, printing the chair time beside the money at the same size and naming the practice it is breaking, which turns a price list into a reason to trust the salon. The refusal sits beside the result, not in the small print: a business that tells you plainly what it will not quote is more believable about what it will. And the trust strip is deliberately modest — open Tuesday to Saturday, smallest booking $28, one therapist start to finish — because specific, checkable, unglamorous facts convert better than badges, and every one of them is something the salon could be held to on the phone.

Honesty

What this page declined to publish

The collection’s rule is that an unverifiable figure about somebody else’s price, standard or scheme does not get softened — it gets left out, and the omission gets listed. For a beauty page the risk is not arithmetic, it is claims about a person’s skin.

  • No cosmetic, dermatological or health outcome is claimed anywhere. The facial is described by what is done — cleanse, exfoliate, extractions if you want them, mask, massage — and the price list calls it “a treatment, not a promise”. There is no “clinically proven”, no skin-condition claim, no anti-ageing claim and no result of any kind promised. A mechanical scan for that vocabulary runs over both pages on every pass.
  • No face and no skin is drawn. There is no before-and-after, no model, no procedural portrait. The drawn subject is an appointment card, because a beauty page has no business rendering somebody’s complexion to sell a service.
  • No price for injectables, needling under prescription, or anything a nurse or a doctor signs for. That is somebody else’s consultation and somebody else’s fee. The refusal sits directly beneath the result card — its placement is asserted, and so is its absence from the FAQ, because an assert is what stops a later pass tidying it into the small print.
  • No claim about who is required to hold what registration. The footer lists an ABN and a public-liability reference in visibly placeholder patterns and says so; it does not state a licensing requirement, because we cannot verify one. The patch-test line is written as the salon’s own policy — “we ask for a patch test at least 24 hours before” — and never as a rule that applies to anyone else.
  • No street address. Suburb-level only, on both pages, scanned for mechanically on every pass. The salon is “above the shops, Mount Lawley” and nothing more specific.
  • No live-booking claim. The availability strip is labelled sample data beside its own heading, in the paragraph under it, and again in the FAQ. Nothing on the page books anything, takes a deposit, or sends a request anywhere — the form confirms locally and says so on the button’s own helper line.
  • No fictional structured data. The page emits WebPage and CreativeWork only, both naming it a demonstration. The reviews are demonstration copy, are labelled as such on the page, and are deliberately not marked up.

For a real salon

The local SEO we would ship, and the systems we would wire it to

A demonstration must not put fictional business data into a real search index, so the structured data below is printed here as a code sample rather than emitted on the page. For a real Fig & Feather it would ship on day one, with the operator’s own address, hours, and a review feed they actually own.

{
  "@context": "https://schema.org",
  "@type": "HealthAndBeautyBusiness",
  "name": "<the salon's real trading name>",
  "image": "https://<their domain>/room.jpg",
  "telephone": "<their real number>",
  "priceRange": "$$",
  "address": {
    "@type": "PostalAddress",
    "streetAddress": "<their real address>",
    "addressLocality": "Mount Lawley",
    "addressRegion": "WA",
    "postalCode": "6050",
    "addressCountry": "AU"
  },
  "geo": { "@type": "GeoCoordinates", "latitude": "<lat>", "longitude": "<lng>" },
  "openingHoursSpecification": [
    { "@type": "OpeningHoursSpecification",
      "dayOfWeek": ["Tuesday","Wednesday","Friday"],
      "opens": "09:00", "closes": "18:00" },
    { "@type": "OpeningHoursSpecification",
      "dayOfWeek": "Thursday", "opens": "09:00", "closes": "20:00" },
    { "@type": "OpeningHoursSpecification",
      "dayOfWeek": "Saturday", "opens": "08:30", "closes": "15:00" }
  ],
  "hasOfferCatalog": {
    "@type": "OfferCatalog",
    "name": "Services",
    "itemListElement": [
      { "@type": "Offer",
        "itemOffered": { "@type": "Service", "name": "Lash lift & tint" },
        "price": "95.00", "priceCurrency": "AUD" }
    ]
  },
  "aggregateRating": {
    "@type": "AggregateRating",
    "ratingValue": "<from their own review feed>",
    "reviewCount": "<from their own review feed>"
  }
}

Note what is different: HealthAndBeautyBusiness, Offer, Service and AggregateRating are exactly the types this demonstration is forbidden to emit, because they would put an invented salon and invented ratings into a real index. That is the whole distinction — the markup is the sales evidence, not the page.

  • Booking system. The availability strip is the panel a real client cares about most, and it is a small job to point it at a live diary — Timely, Fresha, Phorest, Kitomba or Square Appointments all expose the availability a page like this needs. The rule we would keep is the one on this page: show the gap that actually fits the visit the customer just built, not a wall of every fifteen-minute slot in the week.
  • Reviews. Pulled from the platform the salon already collects them on, rendered with the real author names and marked up properly — never written by us.
  • The service menu becomes one data file the salon owner edits: name, price, chair time, and whether it shares processing with anything else. The card, the price list and the schema all read from it, so a price rise is one change in one place rather than four.
  • Enquiries land in the inbox and the CRM the operator already opens — a form that emails somebody who never reads email is worse than no form.

Sapience Analytics

Want one pointed at your customers?

This is a demonstration build. We make pages like it for Perth businesses that are losing bookings to an Instagram DM nobody reads until nine at night.

sapienceanalytics.com.au · danny@sapienceanalytics.com.au