HEADERLINE ← The site Index

How it was madeThe Window: a day of grain moisture, drawn against somebody else's line.

Headerline Contract Harvesting is a fictional two-machine contract harvesting business out of Cunderdin, built for the eastern-wheatbelt grower with a paddock standing and no header free. The page has one job: turn “can you take 500 hectares off” into a booking, with the window, the days and the price already on the screen. Everything on it is drawn from one model — no photographs, no plugin, no back end.

The signature — the Window

One SVG band chart, midnight to midnight, on a fixed 24-hour axis. Three registered layers sit on it: the grain-moisture trace for the kind of day you picked; CBH's 12.5 % receival standard as a dashed line that never moves; and beneath them a verdict strip of 48 half-hour cells, each lit if the sample is under the line and washed amber if it isn't. Under the strip, brackets measure each run — which is how the fire-danger day shows its 7.0 hours as 2.0 + 5.0 with the shire's harvest and vehicle movement ban hatched between them. A row of day blocks counts the job out beneath the axis.

The axis is fixed on purpose. Rescaling it, or the 12.5 % line, would destroy the only thing the chart is for: showing how much of a day the moisture actually gives you. The smallest drawn window — six hours on the humid day — is 25 % of the axis, about 250 px of band at 1200 wide and 83 px at 375, legible at both.

The trace is built from the window rather than fitted to it. Each day pins a night moisture, a driest sample and its hour, and a window [a, b]; the curve is cosine-interpolated through (0, mNight) → (a, 12.5) → (low, mLow) → (b, 12.5) → (24, mNight), monotone within each segment, so it crosses the rule exactly at a and b and nowhere else. The verdict strip then counts its own lit cells, and the page asserts that count equals the pinned half-hours. The window is geometry, not a stored number.

The whole scene is laid out in real pixels — one scene unit is one CSS pixel at every width, so a 14 px label is 14 px at 375 as well as at 1200. The plot re-derives its geometry on resize (shorter trace band and a 6-hourly axis on a phone, day blocks wrapping into rows so the pinned 14-block cap holds), and the scene's height is a function of the measured height of the estimate badge, so the badge can never sit over the drawing. Below 641 px the badge docks beneath the artwork instead.

The numbers

Money is held in integer cents and divided by 100 once at display; the window is held in half-hours, the machine rate in deci-hectares per hour and the yield in tenths of a tonne. That is not fussiness: 400 * 4.6 is 1839.9999999999998 in IEEE doubles, and a $6.50 per-hectare line is exactly the kind of rate that breaks a later comparison.

StepHeld asRest state
WindowhalfHours26 → 13.0 h
Machine ratedeciHaPerHour68 → 6.8 ha/h
Capacityfloor(deci × halfHours / 2 / 10)88 ha/day
Daysceil(area / capacity)6 days
Tonnesfloor(area × yieldTenths / 10)1,000 t
Per hectarecropCents + over×900 + chaser×6507,200 c = $72.00
Billedmax(subtotal, 450000)$36,000

The floor in the capacity line is load-bearing and is what the page bills and draws: 6.8 ha/h across a 13.0 h window is 88.4 hectares, and the page says 88. The $650 float for 60–150 km sits outside the $4,500 minimum job charge (it's a separate movement, not machine time); the yield surcharge, the chaser and the cartage sit inside it. Every per-hectare line multiplies the actual hectares and the cartage line multiplies the actual tonnes — the minimum is a dollar floor, never a hectare floor.

Every figure on the page was re-derived in an independent oracle written from the rate card, which enumerates the full 32,256-cell control grid: 0 non-integer intermediates, 0 totals that aren't a whole dollar, 1,320 cells where the minimum binds, cheapest priced output $4,500 (which equals the services grid's floor), dearest $331,650, longest job 91 days. 63 oracle asserts and 564 headless asserts against the running page, all green.

Where our arithmetic disagreed with the brief

Three places. In each one the page ships the reading that reproduces the pinned endpoints, and the disagreement is written down here rather than quietly resolved on either side.

1. "All three fronts give the same total" is only true at equal chaser state

The brief pins the front chip as price-neutral and asks the pass to assert that all three fronts price identically. Enumerated over the grid, that is false wherever the chaser state differs: a 60 ft front forces the chaser bin on, and the chaser is a separately-priced $6.50/ha line. From the rest state, 50 ft is $36,000 and 60 ft is $39,250 — and the whole $3,250 difference is exactly 500 ha × $6.50, not the front.

The claim is exactly true at equal chaser state, which is why the brief's own pinned drama works: at 3.5 t/ha the chaser is already forced by the yield, so 500 ha of wheat on a fire-danger day runs 20 days on a 40 ft front and 13 days on a 60 ft, and prices $48,250 either way. Shipped: the pinned on-page sentence is unchanged, and a front comparison strip under the controls prints each front's own day count and total, saying "same money either way" when they're equal and naming the forced chaser as the reason when they aren't. Both readings are asserted in the harness.

2. The badge regime and the receival declaration both apply to the badge

The brief puts this site in the constant-frame badge regime — the badge carries line 1 and line 2 only, with the full note templates on the ledger and the plain-text mirror — and separately requires the receival declaration to appear wherever the total does. The badge prints the total, so both rules land on it. Shipped: the badge carries line 1, line 2 and the pinned short declaration ("Receival is CBH's call."), and the three notes (forced chaser, ban, long job) stay off the badge entirely, on the ledger and the mirror in their full form at every width. The declaration is a standing statement, not a note, which is why it earns the third line.

3. The fire-danger window is one moisture window with a ban carved out of it

The brief writes the fire-danger window as "10:00–12:00 + 17:00–22:00 = 14 half-hours". The page derives the same 14 half-hours a different way: the moisture window is a continuous 10:00–22:00 and the shire's ban removes 12:00–17:00 from the middle of it — 24 cells less 10 cells. Same window, same 7.0 hours, but it draws the honest mechanism: the ban is a legal blackout over grain that is otherwise fit to take, not a second dew. It is also why the ban is drawn as a hatched column through the whole chart and not just as a gap in the strip.

Two smaller notes, neither a disagreement. The longest job's maximum (91 days) is reached at 3.5 and 4.0 t/ha — they share the heavy machine-rate band — and the dearest cell ($331,650) is reached on a 40 ft front as well as a 60 ft, because at 4.0 t/ha the yield has already forced the chaser. Neither maximum is unique.

Palette

Argued from the hours the job actually happens in: a green-black paddock with the header lights on, the amber of a sodium yard light and a dew hour you can't take grain in, and the cold blue of the hour the sample comes back under twelve-five. Ratios below are measured with WCAG arithmetic from the shipped tokens in the running page, not from the spec.

Field night #101A15page ground · the light ink reads 14.53:1 on it
Card #18241Epanels and cards · light ink 13.10:1 · hairline #2C3B33
Header-light ink #E3EAE4body ink · 14.53:1 ground, 13.10:1 card
Muted ink #9DAFA4captions and axis labels · 7.71:1 ground, 6.95:1 card
Dew amber #E0A03Ablocked hours, the ban, CTAs · 7.84:1 as text; buttons take the night ink at 7.84:1
Window blue #68B0D6the harvestable band, the trace, day blocks · 7.43:1 as text; chips take the night ink at 7.43:1
Harvestable band #274A5Cscene fill · light ink 7.73:1
Blocked wash #4A3A1Cscene fill · light ink 8.97:1 · ban hatch #5A4620 at 7.36:1

Both accents clear 4.5:1 as text on their own grounds unminted, so neither needed the split ink/line pair. One measured fact governed the labelling: window blue on the harvestable band fill measures 3.95:1, so no blue text is ever set on that fill — every label crossing a scene fill is the light ink, and the map's town labels carry a night-ground halo through paint-order: stroke.

Held off the AI defaults deliberately: it is a dark site rather than cream-and-terracotta; it carries two argued mid-chroma accents on a green-black ground rather than one acid note on neutral near-black; and it has no hairline rules at all — the structure is the hour axis.

Type

Space Grotesk 700 for display — a technical grotesque with just enough irregularity to read like an instrument face rather than a corporate one. Commissioner 400/500/600 as the workhorse: a humanist sans that stays legible at 16.5 px on a phone in a paddock. IBM Plex Mono 500 in exactly one role — times, moistures, rates and hectares (23:00 · 9.8 % · 2.0 t/ha) — so a figure always looks like a reading off a machine.

The typographic moment is the rest state's harvestable window set enormous over the chart — 13.0 h at 64 px with HARVESTABLE beneath it in tracked mono, with the estimate and the day count stamped beside it. The scale is deliberate rather than 16/24/32: 11.5 / 13 / 14.5 / 16.5 / 18.5 / 22 / 29 / 39 / 44 / 64.

Conversion decisions

  • The chart is the hero, not below it. At 1200 the page is a two-column hero: the promise and the CTAs on the left, the whole Window — trace, verdict strip, badge and day blocks — on the right. The entire signature and the price badge sit inside the first 750 px, which is the viewport the collection's thumbnail is captured at. That is asserted, not eyeballed.
  • The price lives on the artwork. A ledger below the fold doesn't make the capture, so the estimate badge sits in a reserved band on the chart itself, and the figure also appears in the readout rail beside the big hours figure.
  • Two CTAs, in the trade's order. A grower with a paddock standing rings; a grower planning a program fills a form. "Get a date on the board" leads, "Ring the shed" sits beside it, and the mobile sticky bar carries both — a tel: link and an in-page anchor to the form, both 48 px tall.
  • Form fields are the ones a booking actually needs — name, phone, nearest town, hectares standing, crop, roughly when, and a free-text box that prompts for access, powerlines and distance to the bin. No email, because a wheatbelt contractor rings back. The date is optional; everything else validates on submit with a visible message under the field.
  • The estimator hands off to the form. "Send this run to the form" pre-fills hectares, crop and a plain-English summary of the run, which turns a demo interaction into a warm lead. It fires only on that click, so the form is genuinely empty on load.
  • The controls that don't change the price say so. The day chip and the front chip both change how long we're on your place and never the per-hectare rate, and each carries its sentence on the page. The chaser bin locks on above 3 t/ha or behind a 60 ft front, with the reason printed under the switch.

Honesty

The business never promises a grade, a receival outcome or a yield. The calculator prices the machine and the operator and nothing else; the receival declaration prints wherever the total does — badge, readout rail, ledger and the plain-text mirror — and 12.5 % is named as CBH's instrument every time it appears. The yield figure is the grower's to set, and the page says the header's monitor is the one that counts. No claim about grain quality, grade, or what the grower will be paid appears anywhere, and the page says plainly that it doesn't give agronomic advice.

The real institutions stay real, because that is the honest choice: CBH receival standards and the shire-declared Harvest and Vehicle Movement Bans made under the Bush Fires Act 1954 (WA). The fictional parts are visibly fictional: the phone number uses the reserved (08) 9000 0xxx pattern, the ABN and registration are placeholder-patterned, the address is suburb-level, and the three reviews are demo copy that is never marked up as structured review data. The only live JSON-LD on the site is a WebPage naming it a demonstration.

Local SEO we'd implement for the real business

The demonstration page deliberately ships none of this, because fictional structured data in a real index is dishonest markup. For a real operator we'd ship the block below. There is no agricultural-contractor subtype in schema.org, so the honest top-level type is LocalBusiness — inventing a subtype would be the same dishonesty in a different place.

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "LocalBusiness",
  "name": "Headerline Contract Harvesting",
  "description": "Contract grain harvesting across the eastern wheatbelt.",
  "url": "https://headerline.com.au/",
  "telephone": "+61 8 9000 0397",
  "priceRange": "$$",
  "image": "https://headerline.com.au/img/header-cunderdin.jpg",
  "address": {
    "@type": "PostalAddress",
    "streetAddress": "Lot 41 Quairading Road",
    "addressLocality": "Cunderdin",
    "addressRegion": "WA",
    "postalCode": "6407",
    "addressCountry": "AU"
  },
  "geo": { "@type": "GeoCoordinates", "latitude": -31.652, "longitude": 117.240 },
  "openingHoursSpecification": [{
    "@type": "OpeningHoursSpecification",
    "dayOfWeek": ["Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"],
    "opens": "05:00", "closes": "23:00"
  }],
  "areaServed": [
    { "@type": "City", "name": "Cunderdin" }, { "@type": "City", "name": "Meckering" },
    { "@type": "City", "name": "Tammin" },    { "@type": "City", "name": "Kellerberrin" },
    { "@type": "City", "name": "Quairading" },{ "@type": "City", "name": "Merredin" }
  ],
  "hasOfferCatalog": {
    "@type": "OfferCatalog",
    "name": "Contract harvesting",
    "itemListElement": [
      { "@type": "Offer", "name": "Contract harvesting, wheat or barley",
        "priceSpecification": { "@type": "UnitPriceSpecification",
          "price": "68.00", "priceCurrency": "AUD", "unitCode": "HAR" } },
      { "@type": "Offer", "name": "Chaser bin and operator",
        "priceSpecification": { "@type": "UnitPriceSpecification",
          "price": "6.50", "priceCurrency": "AUD", "unitCode": "HAR" } }
    ]
  },
  "aggregateRating": { "@type": "AggregateRating", "ratingValue": "4.9", "reviewCount": "38" },
  "review": [{
    "@type": "Review",
    "author": { "@type": "Person", "name": "A real named grower, with permission" },
    "reviewRating": { "@type": "Rating", "ratingValue": "5", "bestRating": "5" },
    "reviewBody": "Fourteen hundred hectares in eleven days, second header on it from day four."
  }]
}
</script>

Alongside it: a verified Google Business Profile with the harvest-hours override, seasonal posts through September–January, the service-area towns as real destinations rather than a keyword list, and the reviews fed from the profile so the aggregateRating is always something a person actually wrote.

The three passes

Pass 1 — Craft

  1. The signature missed the thumbnail entirely. At 1200×750 the first capture showed three lines of headline and no chart — the hero stack pushed the Window below 750 px. Rebuilt the hero as a two-column grid at ≥1024 with the whole panel in the right column; the chart, badge and day blocks now finish around 720 px, asserted at ≤752.
  2. Every scene label rendered at about 4.7 px on a phone. The chart was drawn in a fixed 1200-unit viewBox, so at a 335 px container a 17-unit label scaled to 4.7 px. Rewrote the scene to lay out in real CSS pixels at every width, with per-breakpoint geometry (a 6-hourly axis and wrapped day-block rows on a phone). This was the largest change in the build and it fixed legibility, moiré and the day-block crush at once.
  3. 375 masthead collision. The wordmark overflowed its squeezed flex box and struck through the phone chip. The rect probe missed it because the flex box shrank while its children spilled — the fixed probe now measures the wordmark's children. Also found a source-order specificity bug: .btn's display:inline-flex was beating .mast-cta{display:none}, leaking the desktop CTA onto mobile.
  4. The docked badge docked upwards. At ≤640 px the badge is in normal flow, and it preceded the SVG in the DOM — so it rendered above the artwork instead of beneath it. Moved after the <svg>; at ≥641 it is absolutely positioned, so DOM order costs nothing.
  5. The badge overlapped the drawing at DPR 2. layout() measured the badge before renderText() had changed its text — the reserve was computed for a shorter badge. Reordered to scene → text → layout, and the harness now asserts a ≥8 px clearance rectangle at 375/1000/1200 and DPR 2.
  6. The moisture tint was ambiguous. The fill ran between the trace and the rule on both sides of the line. It now marks only the part of the day the sample sits under 12.5 %.
  7. Cropped about a quarter of dead space out of the service-area map's viewBox; paired the form fields into two columns at ≥641; re-inked the moisture axis to 20 / 16 / 12.5 / 8 with the receival value in the light ink so the dashed rule is identified where it meets the axis.
  8. Measured every token pair from the shipped CSS rather than the spec: 16 rows, lowest 4.84:1 (amber on the blocked wash), body ink 14.53:1. No token needed re-minting.

Pass 2 — Depth

  1. Run brackets under the verdict strip — the second read. Each continuous run of harvestable half-hours gets a bracket labelled with its own length, derived from the strip's geometry rather than from a stored number. On a steady day that reads 13.0 h; on the fire-danger day it reads 2.0 h and 5.0 h, which is the whole argument of the page in two labels.
  2. The ban became a column, not a gap. The harvest and vehicle movement ban is hatched through the full height of the chart and named on it, because it blacks out grain that is otherwise fit to take. The strip keeps its own outlined plate.
  3. The front comparison strip — three cards printing 40 / 50 / 60 ft with each front's day count and total, tappable to change the front. On the pinned fire-danger scenario it prints 20 days and 13 days against the same $48,250 and says "same money either way"; where the chaser state differs it names the forced chaser as the reason instead of pretending equality.
  4. Motion. The entrance ramps now ease (easeOutCubic on group opacity, quadratic on the trace's dash draw) so the scene's lights come up rather than fading in. The trace uses butt caps and stays hidden until its drawn length exceeds zero, so no round-cap dot appears at the start.
  5. Micro-interaction: when the estimate changes, the badge figure and the readout total tick over — a 220 ms rise from 15 % opacity. It fires only on a real change of value, and not at all under reduced motion.
  6. The driest-sample tag now places itself above the dot where the dip leaves room and below it where it doesn't, so it stays inside the trace band on all four days.

Pass 3 — Hardening

  1. The screenshot-resize gate. Entrance choreography runs on a cold-load firstDraw flag with a settle-timer backstop, and the resize handler redraws but never re-enters it — taking a clipped screenshot changes device metrics and fires resize, which is exactly how a page photographs itself empty. The standing assert drives the controls, waits with nothing poking the page, and then requires every animated group opaque.
  2. Label collisions, at native resolution, on every day. Found and fixed two: the 12.5 % caption struck through the ban caption on the fire-danger day (they now sit on separate lines in the gap band between the trace and the strip), and the "dew's back" tag crowded the trace at 375 (it drops to the bare time on a phone). Every day chip was screenshot at 375, 1000 and 1200 and looked at.
  3. Chanel rule: removed the decorative crop strip under the verdict band. It carried no information the run brackets don't now carry better.
  4. 375: masthead measured child-by-child, sticky bar fixed to the bottom edge with 48 px targets and both a tel: link and an in-page anchor, above-fold h1 + CTA + trust signal all inside the viewport, zero overflow offenders, badge docked below the artwork.
  5. Reduced motion renders a fully-settled scene with no rAF loop at all, and the chips still drive it. Keyboard drives the chips, the steppers and both switches (Enter and Space asserted). The rAF loop pauses on document.hidden and uses no accumulating delta.
  6. Rapid-fire drive: twelve control clicks dispatched in one evaluate with no settle between them — no page errors, final state still matches the oracle, and every group opaque afterwards. The redraw token is bumped in the handler before the state swap lands.
  7. Proved the phone field's pattern by driving checkValidity() on a deliberately invalid value rather than reading the attribute, since Chrome's v-flag regexes reject literal specials at validation time and leave the field silently validating nothing. Confirmed no backdrop-filter on any conversion chrome, and that the [hidden] guard really hides SVG elements.
  8. Final copy read: cut a duplicated clause from the cartage card, tightened the map legend to say that beyond 150 km is off the map rather than implying an unlabelled dot, and checked every number in the copy against the model.

What would change for the real client

  • Their rate card, once. The whole model is one object of integer cents at the top of the script — crop rates, the heavy-crop step, the chaser and cartage lines, the float bands and the minimum. Changing a season's rates is one edit, and the chart, the ledger, the badge and the mirror all follow.
  • Their machine data. The hectares-per-harvestable-hour table is where a contractor's real experience lives. We'd build it from their own paddock records — most operators already have the hours and hectares in their header's monitor exports.
  • The board becomes a real booking. The form posts to whatever they already run — Tradify, ServiceM8 or AroFlo for the job card; Figured or Xero for the invoice; a simple Make.com or n8n hop if they'd rather it landed in a shared inbox with the estimator's summary attached.
  • The yield and moisture maps. The files the page promises are already coming off a John Deere Operations Center or Trimble/Case AFS account; we'd hand them over from a per-client folder rather than a USB on the last afternoon.
  • Their reviews and their photos. The reviews feed from the Google Business Profile, and the procedural artwork gives way to their own machines — although the Window stays, because no photograph of a header can answer "how many hours will you get tomorrow".
  • Live ban status. The fire-danger day is a chip here; for a real operator it would read the shire's current harvest and vehicle movement ban declaration, which is the single most useful thing this page could do in January.

Techniques

No libraries. Inline SVG, CSS gradients and vanilla JavaScript in one self-contained file — the concept is a measured technical drawing, and a drawing you can label, dimension and assert beats a WebGL scene you can only look at. No raster images anywhere: the service-area map, the band chart and every mark on the page are procedural.

The scene is assembled synchronously once — 48 verdict cells, 14 day blocks, two run brackets, the trace, the markers — and every subsequent state change only sets attributes. Entrance choreography is driven from the animation loop itself rather than by post-hoc DOM insertion or class toggles, which is the only construction that reliably paints in a headless renderer. A single requestAnimationFrame loop moves one thing: a clipped gradient sweeping along the harvestable cells, on a 6.4 s cycle with the band itself always at full opacity, so the signature is visibly alive at any capture instant without any phase arithmetic.

State lives on window.__harvesting — the scenario, every computed figure in its integer unit, the badge lines, the active notes and a settled flag — so verification gates on state rather than on elapsed seconds.