LedgerlineBuild notes

Generative Assets · industry demonstration 412

How the Ledgerline page was made

A lead-generation page for a fictional four-person tax practice in Osborne Park, built to be recognised by a Perth accountant in five seconds and booked on by a stressed business owner in thirty.

The concept

Ledgerline Tax & Advisory is an invented registered tax agent practice in Osborne Park: four people, about 180 small-business clients, fixed monthly fees. The visitor it is built for owns a Perth trade or services company turning over $150k to $2m and is late, annoyed and quietly worried — a BAS behind, a company return not started, an accountant who never rings back.

The page has one job: a booked twenty-minute call. Professional-services leads are the most valuable category on our demand ledger, so the form leads and the phone follows — the opposite of the trade pages in this collection, where the call button wins.

Why this converts

The fee is on the page, so the visitor who was only ever going to ask “what do you charge” gets an answer in eight seconds instead of a form and a wait — and arrives at the call already agreeing with the number.

The primary button goes to the form rather than the phone, because a practice would rather have a booked twenty minutes than an interrupted one, and the phone still sits one tap away in the header and in the mobile sticky bar for the person who wants it now.

The form asks six things and requires five — enough to decide who takes the call, not enough to feel like an audit — and the question that actually sorts the enquiries, roughly how far behind are you, carries a line of copy that takes the shame out of answering it honestly.

The trust strip under the headline is three plain facts (a fixed fee agreed in writing, four people and about 180 clients, the file review is free) rather than a row of badges, because the fear this visitor arrives with is a surprise invoice and each of those three answers it.

And the estimate hands its own result to the form, so the enquiry lands with the scenario and the figure attached and the practice opens the call already knowing what the page quoted.

The proof element — a fixed-fee estimate

Three plain-language controls: what you trade as, turnover a year, people on the payroll. They resolve to one monthly figure ex GST, its annual equivalent, and a four-line list of exactly what that fee covers, rewritten per scenario so the list is never generic.

The arithmetic is a base plus two additions, held in whole dollars so no cell can produce a floating-point cent:

All figures ex GST. Every cell below was driven in a headless browser and checked against an oracle written separately from the page.
ControlValuesAdds
Entity (base)Sole trader / company / company + trust$95 / $185 / $285
TurnoverUnder $150k / $150k–$500k / $500k–$2m+$0 / +$60 / +$150
PayrollNone / 1–4 / 5–15+$0 / +$45 / +$110
ScenarioA monthA year
Floor — sole trader, under $150k, no payroll$95$1,140
Rest state — company, $150k–$500k, 1–4 staff$290$3,480
Sole trader who has taken on staff$200$2,400
Ceiling — group, $500k–$2m, 5–15 staff$545$6,540

The floor and the services-grid anchor are the same number. The cheapest thing the estimate can produce is $95 a month, and the services grid opens at “from $95 a month”. If those two disagreed the page would be arguing with itself in front of exactly the person who reads numbers for a living.

The twelve-month strip

Beside the figure, twelve identical bars — July to June — because the argument of a fixed fee is that every month is the same height. Their height is the fee mapped onto the estimate’s own range, and the dashed rule above them is labelled Top of this estimate — $545 a month. At the ceiling the bars reach it exactly; at rest the gap between the bars and that rule is the second read, and it says something a practice rarely says out loud: this is as expensive as we get.

The rule and its label are HTML, not SVG. Drawn inside the fixed viewBox at 8.5 user units the label rendered at about 7 px on a 375 px phone; set in real CSS pixels it measures 11 px at every width from 375 to 1440.

The handoff

A button under the estimate carries the scenario to the enquiry form: it writes the configuration and both figures into a banner above the fields, selects the matching entity option and seeds the notes box. It is deliberately not applied on page load — a cold visit starts with an empty form, so an empty submit still validates honestly, and nobody arrives to find a machine has answered questions on their behalf. Change the estimate after carrying it and the banner follows.

Palette

Cool ledger, no cream. The banned AI default of cream, high-contrast serif and terracotta would have made this page look like every other accountant template; a blue-grey paper with a single verdigris accent reads as columns and reconciliation instead. Every ratio below is measured, not estimated.

Paper
#F5F7F8
the ground — ink on it is 15.09:1
Panel
#E6EBEE
alternating bands, cards, map land — ink 13.5:1
Ink / dark band
#0E2233
body type; reversed, paper on it is 15.09:1
Muted
#45596B
secondary copy — 6.75:1 on paper, 6.04:1 on panel
Verdigris
#146B5F
CTA fills, marks, figures — 5.93:1 on paper, 5.3:1 on panel; paper reversed on it 5.93:1
Verdigris deep
#0F5148
derived, not in the wave sweep: button hover fill and the invalid-field label — measured 8.53:1 both directions against paper
Light verdigris
#7FD3C4
9.28:1 on the dark band, and used nowhere else — on paper it measures 1.63:1
Field border
#66788A
input borders and map dots — 4.23:1 on paper as a mark
Hairline
#AFBCC5
rules only, never type — 1.81:1 on paper

The one rule that shapes the whole page. The light verdigris is a dark-band token and only a dark-band token. That is why the estimate’s result sits on ink: it is the only ground on which the page’s brightest colour can carry the largest number.

Type

Newsreader 500/600 for headings. A paper-and-ledger serif that does not become the cream-and-terracotta cliche on a cool blue-grey ground, and it lets the headline carry one italic line — — fixed monthly, Osborne Park. — which is the page’s single typographic move.

Public Sans 400–700 for body, forms and every figure, with tabular numerals on throughout. A fee table with wandering digits reads amateur to an accountant faster than to anyone else, and the estimate’s $290 is set in Public Sans 700 at a size nothing else on the page comes near.

Body is 17 px at 375 px and inputs are 16 px, which is the size below which iOS zooms a form field on focus.

Conversion decisions

Why the form leads and the phone follows

On the trade pages in this collection the call button wins, because a blocked drain is decided in sixty seconds. An accountant is not. This visitor is comparing two or three practices over a week, often at night, and the thing they most want to avoid is being sold to on the phone by someone they have not decided about yet. So the hero button reads Book a 20-minute call and goes to the form; the number sits beside it as an outline button, in the masthead, in the form aside and in the sticky bar, so nobody who wants to ring is ever more than one tap away.

Why the fee is on the page at all

Most practices hide it, and the reason given is that every client is different. That is true and it is also why the estimate carries a sentence saying what it assumes and what would move it. Publishing a range you will stand behind converts better than a contact form, because it removes the one question that stops the enquiry being sent.

Why these six fields

Name, phone and email are the callback. Entity decides who in a four-person practice takes the call. Roughly how far behind are you is the field that earns its place: it sorts a tidy switcher from a three-year catch-up before anyone picks up the phone, and its options include Honestly, I don’t know because that is a real answer and refusing it costs enquiries. The sixth box is optional. Nothing asks for turnover — the estimate already offered a way to say it without being asked.

The confirmation

Nothing is sent anywhere; the panel is drawn in the browser and says so. It commits to a call before the end of the next business day, greets the sender by first name, and gives an out for the genuinely urgent case — a director penalty notice, a garnishee — because a page that promises the same response time to everyone is not believable to the person who needs it today.

Local SEO we would implement for the real business

The live page emits WebPage, WebSite and CreativeWork only, and says in the schema that it is a demonstration. Ledgerline does not exist, so shipping a business or a review graph for it would be dishonest markup in a real index.

For a real practice we would ship the block below, alongside a verified Google Business Profile, consistent name/address/phone across every citation, and one page per service and per suburb rather than one page carrying all of it.

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "AccountingService",
  "name": "<the practice>",
  "image": "https://example.com.au/img/office.jpg",
  "url": "https://example.com.au/",
  "telephone": "+61 8 XXXX XXXX",
  "priceRange": "$$",
  "address": {
    "@type": "PostalAddress",
    "streetAddress": "<street>",
    "addressLocality": "Osborne Park",
    "addressRegion": "WA",
    "postalCode": "6017",
    "addressCountry": "AU"
  },
  "geo": { "@type": "GeoCoordinates", "latitude": -31.90, "longitude": 115.81 },
  "openingHoursSpecification": [{
    "@type": "OpeningHoursSpecification",
    "dayOfWeek": ["Monday","Tuesday","Wednesday","Thursday","Friday"],
    "opens": "08:30", "closes": "17:00"
  }],
  "areaServed": [
    { "@type": "City", "name": "Perth" },
    { "@type": "AdministrativeArea", "name": "Western Australia" }
  ],
  "hasOfferCatalog": {
    "@type": "OfferCatalog",
    "name": "Compliance and advisory",
    "itemListElement": [
      { "@type": "Offer", "itemOffered": { "@type": "Service", "name": "Company compliance" } },
      { "@type": "Offer", "itemOffered": { "@type": "Service", "name": "BAS preparation and lodgement" } }
    ]
  },
  "aggregateRating": {
    "@type": "AggregateRating",
    "ratingValue": "<from the real review feed>",
    "reviewCount": "<from the real review feed>"
  },
  "review": [{
    "@type": "Review",
    "author": { "@type": "Person", "name": "<a real, consenting client>" },
    "reviewRating": { "@type": "Rating", "ratingValue": "5" },
    "reviewBody": "<their own words, published with permission>"
  }]
}
</script>

AccountingService is the precise schema.org type here — a subtype of FinancialService and therefore of LocalBusiness — and it is worth using the precise one rather than the generic parent. The rating and review fields are left as placeholders on purpose: they get filled from a real feed with real permission, never written by hand.

What would change for the real client

Xero is almost certainly already there, and the page is written around that: the practice works in the client’s file, on the client’s subscription. Behind it we would wire Dext for receipt and bill capture, and FYI (or Karbon) for document management and job workflow, so “your call returned the same business day” is a workflow rule rather than a promise. The enquiry form would post into that job workflow and open a lead job, not an inbox.

The rest is content the real practice owns: their photographs of four actual people instead of the drawn map, their own review feed instead of demo copy, their real tax agent number and ABN instead of the placeholder patterns, and their calendar — Calendly, or a booking widget on their practice-management system — behind the twenty-minute call, so the form becomes a booked slot rather than a callback promise.

Everything else on this page is production-ready as it stands: one file, no framework, no image requests, no cookie banner because there is nothing to consent to.

Techniques

One self-contained HTML file. No libraries, no build step, no raster images: every visual is CSS, inline SVG or type. Two Google Fonts, a data-URI favicon, and about 60 KB on the wire before compression.

The estimate is a set of native radio inputs in three fieldsets, styled as chips, so it is keyboard-operable and screen-reader-legible for free — arrow keys move between chips and reprice the card. The result region is aria-live="polite". The twelve-month strip is a single inline SVG whose bar geometry is written as attributes and eased with a CSS transition on y and height; the settled geometry was checked against the attributes to be certain the stylesheet never overrides them.

There is no animation loop. The entrance is a two-step CSS stagger that runs once on load and settles, which means the page is genuinely at rest by the time any screenshot is taken — no frame counter, nothing to drift. window.__accountant exposes the estimate’s inputs and outputs as plain numbers for verification.

Three iteration passes

This build inherited an unverified draft from a session that was interrupted mid-task: an index.html on disk, no guide, no passes run, nothing checked. The first job was to read it against the spec and re-derive its arithmetic before changing a line of it. The structure and most of the copy survived; the list below is what did not.

Pass 1

Craft

  • Re-derived the whole model. Drove all 27 estimate cells in a headless browser against an oracle written separately from the page. All 27 matched, including the $95 floor, the $290 / $3,480 rest state, the $545 / $6,540 ceiling and the $200 sole-trader-with-staff case. No pricing changed.
  • The proof element was out of frame. At 1200×750 the result card ended at 788.8 px and the twelve-month strip at 771.8 — the thumbnail would have shown three rows of chips and no answer. Took 72 px out of the card’s vertical rhythm (hero padding 46→36, a one-line lead, tighter fieldset, legend and result padding) and re-cut the strip. The result card now ends at 716.7 px.
  • The headline was set in a column half the width available. h1{max-width:15ch} applied at every width, so a 62-character headline wrapped into a 218 px column on a 339 px phone — four lines plus a two-line tail, and the first trust line landed at y 687 on a 667 px viewport. Freed the cap below 961 px and trimmed the subline; the trust line now sits well above the fold and above the sticky bar.
  • A dead error state. The submit handler added a .bad class to invalid fields and no CSS defined it — the failure was in the accessibility tree and nowhere on screen. Added a 2 px ink border and a deep-verdigris label, both measured.
  • Removed a duplicate display declaration on the chip label that set block and then flex.

Pass 2

Depth

  • Gave the strip’s headroom a meaning. The empty space above the bars was doing nothing, so it became the estimate’s ceiling: a dashed rule labelled Top of this estimate — $545 a month, which the bars reach exactly at $545 and fall well short of at rest. Verified at all three endpoints — $95 draws 10 units, $290 draws 27.3, $545 draws 50 and tops out flush with the rule.
  • Moved that label out of the SVG. Drawn at 8.5 user units it rendered about 7 px at 375. Reset as an HTML flex row so it sets in real CSS pixels; measured at 11 px from 375 to 1440, and the month initials raised to 12.5 units so they clear 10 px on a phone.
  • Built the estimate→form handoff. One button carries the configuration and both figures into a banner above the form, selects the matching entity option and seeds the notes field. Driven end to end: carried the $545 / $6,540 group scenario, confirmed the entity select landed on Company and trust, then changed the estimate and confirmed the banner re-read $95 / $1,140. Confirmed it does not fire on load, so the empty-submit path still validates honestly.
  • Eased the bars. A 300 ms transition on y and height, then checked that the settled getBBox equals the attributes to the hundredth of a unit — a stylesheet rule on those properties would silently discard the attribute, and a transition must not.

Pass 3

Hardening

  • The service-area map argued with itself. Joondalup, Ellenbrook and Rockingham were drawn outside the dashed ring while all three were listed under “On-site suburbs”, and the ring struck through Canning Vale while its legend collided with Ellenbrook. Replaced the ellipse with a closed boundary that encloses every drawn suburb inside the frame, moved the legend clear of the line, and reworded it to describe the region it actually sits in.
  • Withdrew two claims about regulation. The include list said payroll super was “lodged quarterly” and a service card promised trust minutes “before 30 June”. Both read as statements about what the law requires, and neither is something this page needs. They now describe what the practice does — the super run prepared and lodged for you, minutes drafted before year end rather than chased in September. Nothing on the page states an ATO deadline, a lodgement concession or a registration requirement.
  • Five widths, measured. 375, 560, 768, 1000 and 1440: no horizontal overflow at any of them, the wordmark clears the header actions at every width, sticky-bar targets are 50 px, and the primary CTA is 48.
  • Reduced motion. Entrance groups render opaque immediately, the bar transition drops to zero and smooth scrolling is off — all three verified under an emulated prefers-reduced-motion, not just written into a media query.
  • The quiet checks. Zero console errors and zero failed requests across every drive; both pages scanned for curly-quoted attributes (none); the extracted script parsed with node --check; the live JSON-LD parsed and confirmed to be WebPage, WebSite and CreativeWork only; every (08) 9xxx on the page confirmed as the reserved 9000 exchange.
  • Removed one ornament. The estimate’s lead paragraph had been two lines of throat-clearing; it is one line now, and the sentence it lost (“the number we’d put in the engagement letter”) moved down to the honesty note where it does more work.