Firstlatch Back to the site
Build notes · Generative Assets

How the Firstlatch page was built

A lead-generation page for a fictional Perth lactation consultancy, designed for one reader: a parent five days postpartum, at 3am, holding a phone in one hand.

The concept

Firstlatch is an invented one-person practice — an IBCLC working across Perth's inner north and east — and the page has exactly one job: get a frightened parent to book the soonest appropriate consult without having to decide anything complicated. The Perth search for this trade is won by a mums-group round-up and a practitioner directory, so every real operator underneath is one person whose own page is outranked by a list about them.

Everything about the design comes from the reading conditions rather than from the category. Body text is 17px because it is read tired and in the dark; the sticky call bar owns a real tel: link because the reader has one hand; and the fee card answers the money question before it asks a single one, because at 3am nobody wants a form standing between them and a price.

Palette

Every hex and every ratio below is pasted from the same row of contrast-w57.log, and each was re-measured with WCAG arithmetic during the build. All twenty-one declared pairs the page uses reproduced the log exactly — nothing moved.

PAPER
Lamp-lit oat
#FBF7F2
Warm, not white. Clinical white on a phone at maximum dim is a slap; oat reads at low brightness without glare.
PANEL
Warmer wash
#F1EAE1
The whole top of the page, so the fee card can be paper and read as a lifted object rather than a box.
NIGHT BAND
Plum-brown night
#2C2126
The footer and the sticky bar. A plum-brown dark, not a neutral charcoal — it belongs to the rose rather than sitting beside it.
paper on band 14.56:1
CTA · ROSE CLAY
Rose clay
#9E4055 · hover #853547
A dusty clay red rather than a pink. It carries every call and book action, and it is the only saturated colour on the page.
paper on CTA 5.93:1 · on hover 7.58:1
INK
Ink
#241E21
Body copy everywhere, and every label on the service-area map. Warm-black so it never looks printed on the wrong stock.
15.35 paper · 13.72 panel · 13.94 map land · 11.94 map water
MUTED
Muted
#5A5057
Second-rank copy: hints, captions, chart ticks. The log forbids it on the map, so no map text uses it.
7.24 paper · 6.47 panel
ROSE INK
Rose ink
#8C3247
The split-ink half of the CTA colour. Prices, emphasis and the saving figure are set in this, never in the button rose.
7.4 paper · 6.61 panel
SAGE INK
Sage ink
#3C6152
Section eyebrows, the travel segment, the map's rings and dots, and the drawn saving. The calm counterweight to the rose.
6.51 paper · 5.82 panel · 5.91 map land · 5.06 map water
MAP LAND / WATER
Map grounds
#F2ECE3 · #D7DED6
Land a half-step off paper, water a cool grey-green. Only ink and the sage marks are licensed to land on them.

What is banned here, and why

Nursery pastels wash out to nothing at the screen brightness this page is genuinely read at, and they gender a room that should not be gendered. Clinical white is banned for the same reason a hospital corridor is the wrong room for this conversation. And the rose is a clay, not a pink: this is a clinician's page, not a shower invitation.

Colours minted during the build

Three inks were minted for the dark footer, which the wave log does not declare. Measured with the same arithmetic so the next wave's sweep can carry them:

  • #D9CDD1 footer body copy on the night band — 10.06:1
  • #E9DDE1 footer links on the night band — 11.75:1
  • #C6B7BC colophon small print on the night band — 8.06:1

Type

Newsreader 500/600 for display. It is a text serif built for reading on screens, so it holds a 46px headline and a 20px card question with the same voice — warm, literate, and specifically not a wellness script. Nothing on this page should look like it was set by a brand consultant.

Public Sans 400/600 for everything else, at 17px on mobile rather than the usual 16. That single point is the most consequential typographic decision on the page: it is the difference between skimmable and legible for someone who has not slept. Numerals are tabular wherever money appears, so the fee ledger does not jitter as the reader changes the consult.

The one moment of typographic bravery is the answer itself — a 46 to 54px Newsreader figure sitting at the top of the fee card, with the plain-language description on its baseline. The price is the largest thing in the card because it is the question.

The proof element

Three controls — consult type, first visit or follow-up, and travel band — resolve to one of eighteen cells. The whole grid:

ConsultLengthTravel bandFirst visitFollow-up
Video45 minany — no travel$110$95
Clinic, Inglewood60 minany — no travel$180$140
Home visit90 min0–15 km · +$0$250$190
Home visit90 min15–30 km · +$30$280$220
Home visit90 min30–50 km · +$60$310$250

Money is held as whole-dollar integers and never divided, so no cell can drift into a floating-point cent. The travel amount is a band, not a per-kilometre meter, and it is drawn and itemised as its own line rather than folded into the fee — which is the difference between a quote and a number somebody made up.

How the drawing works

The comparison is a small SVG bar chart on a fixed $0–$320 money axis, chosen because the dearest reachable cell is $310 and a fixed axis is the only way three options can be honestly compared by length. Each row carries its own price in the gutter, the chosen one is filled in rose, and the home row's travel is a separate sage segment butted onto the end of the base bar.

The chart's viewBox is rebuilt to the container's measured pixel width on every layout, so one user unit is one CSS pixel and an 11px tick label renders at 11px. A fixed viewBox would have rendered those labels at about 6px on a phone — the failure mode that has bitten this collection before.

Animation is wall-clock only: the entrance interpolates bar widths from performance.now() deltas against a 760ms span with an 80ms stagger per row, never a per-frame accumulator. A per-frame accumulator is exactly the bug that has shipped near-empty thumbnails, because headless Chrome services requestAnimationFrame only when a frame is requested. Redraws after a control change run at 260ms via a token that is bumped in the handler before the state swap, so a rapid sequence of taps cannot leave a stale closure animating to a dead target. If document.hidden goes true, or prefers-reduced-motion is set, the chart snaps to its finished frame instead of pausing mid-draw.

The live model is exposed on window.__lactation — every input plus base, travel, total, minutes, the video alternative, the saving, and the swept floor and ceiling — so a harness can assert the arithmetic without reading pixels.

The page argues against its own biggest sale, at full size

The home visit is the dearest thing Firstlatch sells, and the card exists partly to talk a fair share of readers out of it. When you pick the outer-zone home visit the card prints, at body size and not in fine print: a video consult is $200 less than this one, and most first-week latch questions are solved on video in 45 minutes. The $200 is the oracle's figure — $310 against $110 — and the same saving is drawn onto the chart beside the video bar so the argument is in the picture and not only in the sentence.

The rejected options are never hidden: all three consults are priced at the same scale in the same frame at every state. A visitor who can see the option they did not choose, priced beside the one they did, reads the quote as a model rather than a pitch.

The service-area map

Three concentric bands around Inglewood — solid, dashed and dotted — with eleven named places and a drawn coast and river. The band names live in an HTML key beneath the drawing rather than as text on the map, for two reasons: the map's fixed viewBox makes on-map text a per-breakpoint sizing problem, and the ring text was landing on the drawn river, which is a ground the contrast log forbids sage ink on. The key's dashes match the ring strokes exactly, so the reading is unambiguous at any width.

What this page deliberately does not publish

This trade sits next to two real institutions, and the honest move was to publish a mechanism instead of a number.

  • No Medicare item number and no rebate amount. Neither was verifiable at mbsonline.gov.au or servicesaustralia.gov.au during the build, so neither appears anywhere on either page.
  • No private health fund figure. The FAQ says that some funds pay an extras rebate, that eligibility depends on the fund and on the practitioner's provider status, and that you should ring your fund — with no number attached, and with a line saying plainly why the number is missing.
  • No tongue-tie procedure or outcome. The page says we assess and refer. It does not describe a division, and it says in as many words that anyone promising an outcome has not met your baby.
  • No certification number and no endorsement. The practitioner is fictional. The footer's Reg. MID0000000000 · IBCLC L-000000 is a visibly patterned placeholder, and the colophon says the registration lines are invented.

The two real numbers on the page are the ones it would be dishonest to omit: healthdirect on 1800 022 222 and 000, beside the enquiry form, under a heading that says a lactation consultant is not the right first call for a sick baby.

Three iteration passes

Pass 1 · Craft

Every pass drove the form empty and filled, and drove all eighteen fee cells against an independently re-implemented oracle.

  • The drawing was outside the thumbnail. With the controls above the answer, the chart's top measured y≈790 at 1200 wide — the 1200×750 preview showed a headline, a ledger and no drawing. The card was reordered to answer → ledger → chart → controls, which put the chart at 430–596 and the answer at 205. Found by looking at the generated JPEG, not by an assert.
  • Six colliding map labels at 375. Joondalup over Ballajura, Dianella over Ballajura, Bassendean over Mundaring, and a three-way pile of Perth CBD, Fremantle and Maylands. Sixteen labels were cut to eleven and every one was re-placed against a computed box; a mechanical label-collision assert now runs at 375 on both the map and the chart.
  • A forbidden ground. The on-map band text crossed the drawn river, putting sage ink on map water — a pairing contrast-w57.log prints as FORBIDDEN. The band names moved to the HTML key.
  • The disabled chip faded a declared token into an unmeasured hex. opacity:.55 on the remembered travel chip blended its label and its ground to 2.44:1, and the unselected ones to 3.67:1. The disabled state is now drawn with declared tokens — panel ground, muted label, a rose keyline for the remembered value — and an assert requires the computed opacity to be 1.
  • The masthead wrapped at 375. "Book a consult" broke onto two lines and grew the button; below 480 the label is "Book".
  • A mid-word break in the headline. "one-handed" broke after the hyphen. A non-breaking hyphen plus text-wrap:balance fixed it.
  • A computed-colour audit walked every rendered text node on the page and resolved its true ground. Fourteen distinct pairs, all declared, zero below threshold — the only flag was the reversed "45 min" label whose ground is a sibling <rect> rather than an ancestor background, and that pair now has its own geometric assert proving the label sits inside the rose bar.
Pass 2 · Depth
  • The saving was drawn, not just written. A sage figure now sits in the empty track to the right of the video bar — −$200 vs the home visit at full width, −$200 below 640px — so the page's argument against its own dearest job is visible in the picture. It is hidden when video is the pick, because then there is nothing to argue.
  • The ceiling is swept, not typed. The card's closing line — "nothing on this page costs more than $310, a 30–50 km first home visit, and the dearest thing we do" — is produced by an argmax over all eighteen cells at load, and the probe exposes the swept ceiling ($310), floor ($95) and first-visit floor ($110). An extremum claim is arithmetic; it does not get read off a cell chosen by hand.
  • A $95 that could have read as the page arguing with itself. The swept floor is $95 (a follow-up video) while the services grid says "from $110". The grid footnote now states both explicitly: $110 is the cheapest first consult and where the estimate bottoms out for a first visit; $95 is a follow-up and only exists once we have met.
  • The estimate follows the reader into the form. Whatever consult is on screen is pre-selected in the enquiry form — until the reader changes it there themselves, after which the form is theirs and the estimate never overwrites it again.
  • Motion. The three bars now draw in sequence at 80ms apart inside the same 760ms wall-clock span, so the card reads as "here are your three options" rather than as one block appearing.
Pass 3 · Hardening
  • A whole class of tab stops had no focus ring. The focus rule was written as an element list — a, button, input, select, textarea, [tabindex] — and a <summary> matches none of them, so all six FAQ questions focused with the 1px UA outline. Replaced with a bare :focus-visible. A real Tab walk over 26 stops now asserts a ≥3px ring on every one, and that the skip link is the first stop. (getComputedStyle(el, ':focus-visible') returns nothing — pseudo-classes are not pseudo-elements — and blur() does not reset Chrome's sequential-navigation start point, so the walk reloads the page first.)
  • Keyboard drive. Arrow keys move and select within each chip group, disabled chips are skipped, and a click on a disabled travel chip is inert — asserted by state, not by appearance.
  • DPR 2 at 1000px. No overflow, no console errors, and the answer's box never intersects the drawing's at any width.
  • Chanel rule. The chart legend lost its middle item. "The two you did not" carried a panel-on-paper swatch that was effectively invisible, and the per-row prices already say it.
  • Review and step cards now pin their attribution to the card foot instead of letting it float under a short quote.
  • Ten planted defects, ten fired. A second <h1>, a banned LocalBusiness block, a curly-quoted attribute, a real street address, a wrong travel rate, the drawing pushed below the fold, the opacity fade restored, a v-invalid phone pattern, a broken floor, and a hand-typed ceiling — each planted alone, each caught, the file restored byte-for-byte after every one. The v-flag plant reproduced the known behaviour exactly: [0-9 ()+-] is silently discarded, the field then accepts "ring me maybe", and only a console error and a junk-drive catch it.

Final state: 84 checks, 84 pass, zero console errors at 375, 768, 1000 (DPR 2), 1200 and 1440, and a 1200×750 thumbnail that is at rest and visually complete at seven seconds with the drawing in frame.

Local SEO we would implement for the real business

The live page carries only WebPage schema naming itself a demonstration, because publishing fictional business and review data into a real index is dishonest markup. For a real practice we would ship the block below — a MedicalBusiness (the correct LocalBusiness subtype for a clinician; there is no lactation-specific type, and a medical specialty value is not an organisation type) with real reviews collected through the booking system.

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "MedicalBusiness",
  "@id": "https://firstlatch.com.au/#practice",
  "name": "Firstlatch Lactation",
  "description": "IBCLC lactation consultant offering video, clinic and home consults across Perth's inner north and east.",
  "url": "https://firstlatch.com.au/",
  "telephone": "+61 8 XXXX XXXX",
  "priceRange": "$$",
  "image": "https://firstlatch.com.au/img/clinic-room.jpg",
  "address": {
    "@type": "PostalAddress",
    "streetAddress": "<the practice's own street address>",
    "addressLocality": "Inglewood",
    "addressRegion": "WA",
    "postalCode": "6052",
    "addressCountry": "AU"
  },
  "geo": { "@type": "GeoCoordinates", "latitude": -31.92, "longitude": 115.88 },
  "openingHoursSpecification": [{
    "@type": "OpeningHoursSpecification",
    "dayOfWeek": ["Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],
    "opens": "07:30", "closes": "18:00"
  }],
  "areaServed": [
    { "@type": "City", "name": "Perth" },
    { "@type": "AdministrativeArea", "name": "Perth inner north and east" }
  ],
  "availableService": [
    { "@type": "MedicalTherapy", "name": "Latch and positioning consult" },
    { "@type": "MedicalTherapy", "name": "Supply assessment and feeding plan" },
    { "@type": "MedicalTherapy", "name": "Weighted feed review" }
  ],
  "aggregateRating": {
    "@type": "AggregateRating",
    "ratingValue": "4.9", "reviewCount": "87"
  },
  "review": [{
    "@type": "Review",
    "author": { "@type": "Person", "name": "A real, consenting client" },
    "datePublished": "2026-08-14",
    "reviewRating": { "@type": "Rating", "ratingValue": "5", "bestRating": "5" },
    "reviewBody": "Real review text, pulled from the practice's own review feed."
  }]
}
</script>

Around that: a Google Business Profile in the Lactation consultant category with the service area drawn to the same three bands as the map; suburb-level service pages only where the practice genuinely travels, never a template farm of 40 suburbs; FAQPage markup on the six questions already on the page, since they are the queries people actually type at 3am; and a review request fired by the booking system two days after each consult, which is what makes the aggregateRating above true rather than decorative.

What would change for the real client

  • Bookings and notes: Cliniko or Halaxy. The "Book this consult" button would deep-link into the practitioner's Cliniko booking page with the appointment type pre-selected from the estimate, so the consult the reader priced is the consult they land on. Halaxy is the alternative where the practitioner already runs it for invoicing; either way the fee card and the booking calendar read from one price list, so they can never disagree.
  • Claiming: Tyro Health or Medipass. Health-fund claiming at the end of the consult, on the spot, rather than a receipt the client has to chase. This is also why the page publishes no rebate figure — the terminal tells the client what their fund actually paid, which is the only number that was ever true for them.
  • Reminders: an SMS service such as Cliniko's built-in reminders, or Podium or Twilio for a custom flow. A confirmation the moment the booking lands, a reminder the night before with the video link, and a review request two days after. For a client who booked at 3am, the confirmation SMS is the thing that lets them put the phone down.
  • Their photographs. Every visual here is procedural — SVG, gradients and type — which proves the layout carries without stock imagery. A real practice would swap in the clinic room, the scales and the practitioner's own face, which for a solo clinician is the single highest-converting asset on the page.
  • Their availability. "Usually same day or next" becomes the real next opening, read from the booking system. It is the strongest line the page could carry and it is the one thing we will not invent.
  • A privacy note next to the form, because the field asks a parent to describe a medical situation, and a real practice needs to say where that text goes.

Why this converts

  1. The price is answered before it is asked — a $110 figure sits at the top of the fee card in the first screen at every width, so a reader who came only to find out what this costs never has to fill in anything to learn it.
  2. The call is always one thumb away: a tel: link in the masthead, a full-width call button in the hero, and a fixed bar at the bottom of the phone screen that never scrolls away, because this reader has one hand and a crying baby.
  3. The card argues against the practice's own dearest job at full size, printing exactly what the reader would keep by taking the video slot instead — and a business that visibly costs itself $200 in front of you is the one you ring.
  4. The form asks six things and no more, every one of which the practitioner genuinely needs to triage the call, and the last field invites plain panic rather than a tidy summary — which is what a frightened parent can actually write at 3am.
  5. What the page refuses to publish does as much work as what it does: no invented rebate, no promised tongue-tie outcome, and a plain line pointing at healthdirect and 000 for the things that are not a feeding problem — because the trust that earns the booking is built by the sentences that lose the sale.