The concept
Fortnight is a payroll bureau in Leederville: pay runs, award interpretation, Single Touch Payroll lodgement, super batches, terminations. The visitor is the owner or office manager of a WA business with roughly eight to forty staff on an award, who has read that underpaying people can now be a criminal matter and does not know whether their own pay run is right. The page has one job — book a payroll health check, or move the next pay run across — and it does that by answering the question underneath the fear with arithmetic instead of adjectives.
Why this converts
The visitor arrives frightened and the page refuses to trade on that, which is the first reason it converts: the fear paragraph is three verified sentences long and is immediately followed by a numbered list of the checks we run, so the emotion is answered with a process rather than amplified into a sale. The proof element sits beside the headline rather than below a grid of service tiles, so within one screen the prospect has put their own staff count and their own hourly cost into the page and watched both sides of the sum appear — a quote they built themselves is a quote they believe. The form asks five things, four of which the caller would have to say out loud anyway, and the sixth control is a checkbox that tells us not to sell them anything yet. The strongest thing on the page is the state where it loses: at four staff on a monthly cycle it prints, in the same type size as the good news, that we would cost that business $33.00 a month more than doing it themselves, names the eleventh employee as the point where that flips, and swaps the call-to-action for ring me when I hit ten. A business that publishes the size at which it is the wrong answer has told the reader everything they need to know about how it will behave once the invoices start.
Palette
Argued from the subject: a payroll is a calendar with money on it, so the site is ink and paper with one recurring warm mark — not a fintech gradient. Every ratio below is quoted with its hex from the same row of contrast-w55.log (129 declared pairs, 0 failures) after the last colour change, never from memory.
Four colours minted during this build, all added to the sweep and measured
| Token or ground | Hex | Where it lands | Measured |
|---|---|---|---|
amberChipHover (ground) | #E2B63C | ink on the pressed amber CTA | 9.34:1 |
paleOnDark | #C6CFD1 | on band / on bandHover | 8.68:1 / 10.39:1 |
amberOnDark | #F4C64A | on band / on bandHover | 8.53:1 / 10.21:1 |
alertRed | #A3301A | on paper / on panel | 6.72:1 / 5.99:1 |
The amber is the pay cycle, so it is rationed: the calendar ticks, the primary call-to-action, the verdict card, and — declared as amberOnDark and measured rather than assumed — links, eyebrows and focus rings inside the dark band. It never carries small type on paper, and it never carries pale type: the sweep proves paperOnDark on amberChip at 1.54:1, so the amber takes ink and only ink. The markAmber deep cut draws the rail ticks and rules at mark grade, which is why the numbered how it works list is set in teal rather than amber: numerals are type, and a 3.24:1 mark colour has no business being read.
Type
Fraunces at 500–700 carries every heading and every money figure, set at a low optical size (opsz 16 on the h1, 40–90 on smaller headings and numerals) so it keeps the slightly bookish, high-contrast cut of a printed ledger rather than the softness Fraunces takes at display sizes. A serif here is a working argument, not decoration: the sale is records kept properly, and it separates this page from the accountant and bookkeeper sites already in the collection. Inter does the body, the labels, the form and the map. Everything numeric — both money columns, the crossover sentence, the rail caption, the price anchors — runs font-variant-numeric: tabular-nums, so figures do not jitter sideways as the slider moves. That is the point of the whole card: only the value should change, never the layout.
The proof element — the pay-run comparison
Three inputs: how many people are on the payroll (1–30), the pay cycle, and the visitor’s own loaded cost for an hour of admin, which is edited inside the sentence that states the model rather than parked in a control panel. The output is two columns at equal weight and a verdict underneath, plus a calendar rail that draws one amber mark for every pay run in a year — 52, 26 or 12 — so the cycle is a picture before it is a number.
The arithmetic, held in integer units
Money is held in twenty-fourths of a cent and divided once at display (the W47 rule: 400 * 4.6 is 1839.9999999999998 in doubles, and a payroll page is the last place to ship a rounding artefact). Twenty-fourths, not cents, because the in-house side divides by both 12 months and 60 minutes:
bureau24(n, f) = 9900 * 24 + n * runsAYear(f) * 650 * 2 inHouse24(n, f, r) = (4 + n) * (runsAYear(f) / 2) * r // r in cents saving24 = inHouse24 - bureau24 display = round(u / 24) cents
Every term is an anchor, not a fudge factor: a $99 monthly base plus $6.50 a payslip on our side; 1.0 hour fixed per pay run plus 0.25 hour per employee at a $46/hour loaded admin cost on theirs. runsAYear is 52, 26 or 12. The (4 + n) term is just (1.0 + 0.25n) hours cleared of its quarter, which is what makes every reachable cell an exact integer for any whole-cent hourly rate the visitor types.
The cells the wave oracle licenses, all reproduced by the shipped page
| Staff | Cycle | Bureau | Your own time | Difference |
|---|---|---|---|---|
| 14 | Fortnightly | $296.17 | $448.50 | −$152.33 · $1,828 a year |
| 8 | Weekly | $324.33 | $598.00 | −$273.67 |
| 26 | Fortnightly | $465.17 | $747.50 | −$282.33 |
| 4 | Monthly | $125.00 | $92.00 | +$33.00 — we cost more |
| 10 | Monthly | $164.00 | $161.00 | +$3.00 |
| 11 | Monthly | $170.50 | $172.50 | −$2.00 |
| 1 | Monthly | $105.50 | — | the smallest bill we send |
Services-grid anchors, all licensed and all cross-checked against the same model: bureau from $99/month + $6.50 a payslip, award interpretation setup from $850, STP catch-up lodgement from $450, back-pay review from $1,450, termination and redundancy calculation $180 each, super batch lodgement included.
The refusal — the page turns work away on screen
At four staff on a monthly cycle
The verdict card flips: the border goes to alertRed, the headline reads “We would cost you $33.00 a month more”, and the body names the crossover in the customer’s own units — at 10 staff we are $164.00 against $161.00; at 11, $170.50 against $172.50. A red tag reading NOT WORTH MOVING — YET appears on the calendar rail, a teal notch appears on the slider at the eleventh employee so the distance to the crossover is visible rather than merely stated, and the call-to-action becomes Ring me when I hit ten, which ticks a matching box on the enquiry form and lights it amber.
Placement is the whole point (W53): the refusal is directly under the two money columns, never in the FAQ, and the build harness asserts both — that the verdict sits below the columns, and that the phrase never appears in the FAQ section. A refusal in the FAQ is a disclaimer; a refusal beside the number is a demonstration of how the business quotes.
The refusal is general, not a hard-coded special case. It fires whenever the saving is zero or negative, which on a monthly cycle at the default rate is every headcount from one to ten. On weekly and fortnightly cycles the bureau is ahead at every size the page quotes — the harness sweeps 1–30 on all three cycles and asserts that, rather than assuming it. Change the hourly cost and the crossover honestly moves with it: at $30 an hour the monthly crossover leaves the slider’s range entirely, and the page keeps refusing.
What this page declined to publish
Three facts about Australian workplace law appear on the page. Each was checked at fairwork.gov.au during this build — the Voluntary Small Business Wage Compliance Code page and the Fair Work Ombudsman’s own note that the new criminal underpayment laws started on 1 January 2025:
- Intentionally underpaying an employee’s wages or entitlements became a criminal offence on 1 January 2025. Verified — published.
- It does not capture honest mistakes. Verified — published.
- A small-business employer who the Fair Work Ombudsman is satisfied has complied with the Voluntary Small Business Wage Compliance Code cannot be referred by the Ombudsman for criminal prosecution. Verified — published.
Everything else was declined. These are the figures a payroll page is expected to carry, and every one of them dates:
- Every award rate and every classification. They change, they differ by award, and the page that quotes one is wrong within the year. The page says instead: ask us and we will look up the current one with you, in front of the award.
- Every penalty rate, overtime loading, shift loading and allowance. Same reason. The FAQ describes what a health check looks at without printing a single multiplier.
- The superannuation guarantee percentage. It has moved repeatedly and will move again. The page mentions super batches and never a rate.
- Every penalty and fine amount attached to the criminal underpayment offence, and every maximum civil penalty. Naming a consequence is fear-selling; naming the shelter is the honest and more useful half, so the page publishes the Compliance Code and not the fine.
- The employee-count threshold that defines a small-business employer for the purposes of the Code. It is stated on the Fair Work page, but it is a legislative threshold and the wave’s carve-out permits no Fair Work threshold on this page, so the copy reads “a small-business employer” and stops there.
- Any characterisation of whether a particular business is exposed. The page never says “you are at risk”. It says “here is the check we run”, which is a claim about our own work and therefore ours to make.
- Any legal advice. Question four of the FAQ says plainly that where the exposure turns on how an award should be read, that is a workplace-relations lawyer’s question and not a bureau’s.
One disagreement with the wave oracle, reported rather than resolved quietly
check-w55.mjs enumerates ten cells for this site plus the six services-grid anchors, and the shipped page reproduces every one exactly. But the comparison is a live model over a 30 × 3 grid with an editable hourly cost, so the page necessarily prints figures the oracle never names — every staff count other than 1, 4, 8, 10, 11, 14 and 26, and the annual difference at every cell except 14 fortnightly ($1,828). Two W54 builders pushed back on exactly this shape and both were right, so it is flagged here rather than shipped quietly. What was done instead: the build harness reimplements the model independently in plain floats, asserts it against all ten licensed cells, then sweeps the whole 1–30 × 3 grid for the two structural claims the concept makes in prose — that the bureau wins at every weekly and fortnightly size, and that the monthly refusal set is exactly 1–10. The three brand facts in the trust line (nine years, 140 pay runs a month) are fictional business copy of the same class as “years operating” on every other site in the collection; they compute with nothing and are not oracle-licensed.
Technique
No libraries, no build step, no raster images, two files, about 60 KB of markup and inline CSS and JavaScript between them.
The calendar rail
An inline SVG on a 620 × 100 viewBox: twelve alternating month cells drawn in paper against the rail’s panel ground, and one markAmber tick per pay run, laid out at 10 + (i + 0.5) × (600 / n) with the tick width and height stepping down as the count rises, so 52 weekly marks read as a comb and 12 monthly marks read as posts. The whole drawing is assembled synchronously and never animated, which is deliberate: a signature whose geometry advances over time has to be wall-clock driven or the 7-second thumbnail catches it mid-flight, and the safest version of that rule is a drawing that has no time in it at all. The single page-load choreography is a 0.62 s CSS stagger on four hero elements, finite, with no JavaScript gate — so a script failure can never leave the page at opacity zero, and a resize can never re-enter it.
Toggling SVG children
Two features depend on it: the NOT WORTH MOVING — YET tag on the rail, and the far-suburb group on the service-area map at phone widths. Both are toggled with setAttribute('hidden','') and removeAttribute('hidden') against the page’s [hidden]{display:none!important} guard, never with el.hidden = true. The IDL property is an HTMLElement reflection: on an SVGElement it lands as a plain expando, sets no attribute, changes no style, and leaves every state assert green while the pixels are wrong. The state probe exposes railFlagShown and mapCropped so the harness tests the attribute rather than the intent.
The service-area map
A 700 × 470 SVG: land, an Indian Ocean with a drawn coast, the Swan River as a stroked path, a dashed teal ellipse for the suburbs we sit down in, and fourteen labelled points. CSS cannot set a viewBox, so below 700 px the map re-cuts to 286 132 414 264 in JavaScript from a matchMedia listener — a crop, not a font-size bump, which keeps every label at a legible rendered size instead of scaling type against a fixed frame. Every map label is body ink (#14181B, 14.96:1 on land and 12.72:1 on water); the palette’s mark-grade tokens never label anything.
Local SEO we would implement for the real business
This page ships no business structured data. The bureau is fictional, and fictional LocalBusiness or Review markup in a real index is dishonest markup, so the live page carries a single WebPage node that says it is a demonstration and nothing else. What follows is the schema we would ship for a real client — a code sample on this page, not a live block anywhere:
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "AccountingService",
"@id": "https://example.com.au/#bureau",
"name": "Fortnight Payroll Bureau",
"description": "Payroll bureau in Leederville: pay runs, award interpretation, STP lodgement and super batches for WA employers.",
"url": "https://example.com.au/",
"telephone": "+61 8 XXXX XXXX",
"priceRange": "$$",
"address": {
"@type": "PostalAddress",
"streetAddress": "the client's real address",
"addressLocality": "Leederville",
"addressRegion": "WA",
"postalCode": "6007",
"addressCountry": "AU"
},
"geo": { "@type": "GeoCoordinates", "latitude": -31.93, "longitude": 115.84 },
"areaServed": [
{ "@type": "City", "name": "Perth" },
{ "@type": "State", "name": "Western Australia" }
],
"openingHoursSpecification": [{
"@type": "OpeningHoursSpecification",
"dayOfWeek": ["Monday","Tuesday","Wednesday","Thursday","Friday"],
"opens": "08:00", "closes": "17:00"
}],
"hasOfferCatalog": {
"@type": "OfferCatalog",
"name": "Payroll services",
"itemListElement": [
{ "@type": "Offer",
"itemOffered": { "@type": "Service", "name": "Pay run bureau" },
"priceSpecification": {
"@type": "UnitPriceSpecification",
"price": "99.00", "priceCurrency": "AUD",
"unitText": "per month plus $6.50 per payslip" } },
{ "@type": "Offer",
"itemOffered": { "@type": "Service", "name": "Award interpretation setup" },
"priceSpecification": {
"@type": "PriceSpecification",
"price": "850.00", "priceCurrency": "AUD",
"valueAddedTaxIncluded": true } }
]
},
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "4.9", "reviewCount": "37"
},
"review": [{
"@type": "Review",
"author": { "@type": "Person", "name": "a real, consenting client" },
"reviewRating": { "@type": "Rating", "ratingValue": "5", "bestRating": "5" },
"reviewBody": "the words they actually wrote, quoted with permission"
}]
}
</script>
Alongside it, for a real client: a verified Google Business Profile with the payroll and bookkeeping categories set and the service area drawn to match the map on the page; the reviews fed from the profile rather than typed into the HTML, so the aggregateRating above is a reflection of something real and not an assertion; a FAQPage block generated from the six questions already on the page; suburb-level landing pages only where the business genuinely serves them differently; and the phone number, hours and address kept identical across the site, the profile and every directory, because a bureau whose own details disagree with each other is not the strongest advertisement for a bureau.
What we would change for a real client
The form posts nowhere — it validates and confirms locally and says so on the page, because faking a network round-trip on a demonstration is the one dishonest thing a demonstration can do. For a real bureau it would post to their inbox and their CRM, and the confirmation would carry a genuine callback window. The comparison card would read the client’s own fee schedule rather than a hard-coded one. And the integrations a payroll client actually asks about, in the order they ask: Xero and MYOB for the ledger and the payroll itself, Employment Hero and KeyPay where the client has already moved onto a dedicated payroll platform, and Deputy for rostering and timesheets feeding the run. A real build would sit inside whichever of those the client already pays for, with our own user account, so the data and the logins stay theirs — which is exactly what the page promises in the FAQ.
The three iteration passes
Each pass drove the page in headless Chrome, not just read it: the comparison was run to every licensed cell and the output asserted, the form was submitted empty, submitted with junk in the phone field and submitted filled, the sticky bar was measured, and the 1200 × 750 seven-second thumbnail was captured and looked at.
-
Pass 1 — Craft
- The verdict fell out of the thumbnail. At 1200 × 750 the card’s strongest line — “You keep $152.33 a month” — began at y ≈ 752, one pixel below the frame. Fixed by moving the loaded-hourly-cost control out of the control stack and into the sentence that states the model, which reclaimed 87 px and reads better besides, plus tighter card padding. Both money columns, the rail and the verdict are now wholly inside the frame in both states, asserted.
- Two map labels overlapped. “North Perth” and “Midland” rendered as North Perth Midland at desktop — invisible to every DOM assert, found by looking. The label layout was relaid and a rectangle-intersection collision assert added over all fourteen labels at four widths; it has run on every build since.
- Map labels measured 9.3 rendered px at ~900 px wide, because the two-column map grid engaged at 900 and shrank a 700-unit viewBox into a ~480 px column. The grid breakpoint moved to 1000 px and the phone treatment became a viewBox crop rather than a font-size bump. Smallest rendered label is now asserted at ≥ 10 px at 375, 640, 900 and 1200.
- Amber discipline. The numbered how it works list was set in
markAmber, which is a 3.24:1 mark token being asked to carry type; re-inked teal. The verdict card’smarkAmberborder sat on an amber fill at a forbidden 2.35:1 and was removed. The amber genuinely needed on the dark band was declared asamberOnDark, added to the sweep and measured at 8.53:1 rather than assumed. - Copy. The hero eyebrow repeated the masthead word for word; rewritten to name the three cycles. The trust line orphaned “not ours” onto its own line at 1200; shortened. The subline was cut from four lines to two, which also bought back the 375 px fold.
- 375 px fold. The trust line finished at y 614 against a sticky bar whose top edge is y 599 — the bar was physically covering it. The h1, the primary call-to-action and the first trust line now each finish above the bar with 54 px to spare, asserted.
-
Pass 2 — Depth
- Each column now shows its own working. A hairline-separated line under each figure: $99 base + $6.50 a payslip against 1 h a run + 15 min a head, at $46, the second updating with the visitor’s own rate. It adds no new number to the page — every term is already a pinned anchor — and it turns the card from a verdict into a derivation, which is the second read.
- The crossover is marked on the slider. When a crossover exists inside the slider’s range, a teal notch and a small numeral appear beneath the track at that headcount, so a four-person business can see how far away the eleventh employee is instead of only reading it. It disappears the moment the crossover leaves the range — at $30 an hour on a monthly cycle, for instance — rather than clamping to the end and lying.
- The rail became a calendar. Twelve alternating month cells replaced a plain baseline; before that, 26 evenly spaced ticks read as a barcode rather than as a year. The month letters were already there and were doing nothing until the cells gave them a grid to sit on.
- The regulation panel was re-laid in two columns at desktop, where it had been a narrow column of text in a wide dark band with half the band empty.
- Form rhythm. Every field gained a one-line hint above its input, so the two-column pairs align on the same baselines; previously only the phone field had one, below the input, and it pushed the second row of fields out of alignment.
-
Pass 3 — Hardening
- The phone
patternwas compiled under thevflag and driven with junk.[0-9 \(\)\+\-]{8,20}escapes the parentheses and the hyphen, because underva bad class is silently discarded rather than rejected and the field then accepts anything. The harness both compiles the attribute withnew RegExp(p, 'v')and types “ring me maybe” into the field to provecheckValidity()rejects it. - Reduced motion is honoured in CSS — the entrance keyframes are replaced with a settled state rather than merely shortened — and read through
matchMediain JavaScript before any programmatic scroll, so the refusal call-to-action jumps rather than glides for a visitor who has asked for that. - Thumbnail is rest. Every numeric field on
window.__payrollis re-read eight seconds after the seven-second capture instant and asserted identical. There is no animation loop on the page at all, which is the strongest form of that guarantee. - One ornament removed. The rail drew a baseline rule at y 66 and a frame around the month cells whose bottom edge is also y 66 — the same line, twice. The rule went.
- A street-noun scan across both pages for
street|road|avenue|terrace|parade|crescent|drive|highway|way|court|close|lane. Two hits on the index, both the ordinary English word “way” (“what the pay run costs, both ways”; “the way you have been running it”), and the hits on this page are the schema sample’sstreetAddresskey and this sentence. No street name appears anywhere on either page; the address is suburb-level, Leederville WA 6007. - A curly-quoted-attribute scan on both files — a word character followed by
=and a typographic double quote — because the body copy legitimately uses typographic quotes and the guide is where that defect has historically shipped. Zero hits. - The harness was negative-tested rather than believed. Seven defects were planted into the page one at a time — a second
<h1>, a liveLocalBusinessJSON-LD block, a curly-quoted class attribute, a real street name in the footer, the SVG rail flag switched fromsetAttributeto the IDLhiddenproperty, a phonepatternwith only the hyphen escaped, and aconsole.erroron load. All seven made the matching check fail, and the file was restored and compared byte for byte after each. A green sweep that has never been watched to fire is not evidence; 158 of 158 only means something afterwards.
- The phone