Build notes · commercial kitchen hire
A dark page that still passes every contrast pair it uses.
Nightshift Kitchens is a fictional commercial kitchen in Osborne Park, hired by the hour, around the clock. The audience is a caterer, market stallholder, cloud kitchen or meal-prep operator who cannot cook at home for sale and is currently booking through somebody else’s platform. The page has one job: price the block, show what is free tonight, and make booking direct obviously better than booking through an intermediary.
Palette, and why a kitchen at 4am is dark
Every hex and every ratio below is pasted from contrast-w58.log — the sweep was run first and the prose written from the file, not the other way round. A stainless kitchen at four in the morning is a dark room with hot metal in it: the ground is night, the copper is the heat, the steel blue is the cold room.
Two things the log decides, and the page obeys rather than arguing with. First, ink on primary is FORBIDDEN — white type on the copper button measures 2.26:1, so every copper fill on this page carries #14171A. Second, muted and primary are FORBIDDEN on the band #0D0F11, which only licenses onDark. The how-it-works section, the Sapience band and the footer were originally drawn on the band; all three moved to surface or the ground so that every token they use is a pair the log declares. The band survives in exactly two places where no small type lands on it: the mobile call bar and the water on the service-area map.
One contradiction worth reporting back: ink and onDark are the same hex (#EDE9E2), so the log prints ink on band 15.87:1 as FORBIDDEN and onDark on band 15.87:1 as PASS — the identical measurement, licensed under one name and refused under the other. The page takes the strict reading and only ever names onDark on the band.
Colours minted during the build
Three, each measured rather than eyeballed: the form placeholder ink #8A9197 at 4.96:1 on surface and 5.68:1 on the ground; the disabled stepper glyph #7C848A at 4.81:1 on the ground (a disabled control that still has to be readable); and the copper hover fill #E59A5E, used only as a button fill under dark text. The warm and cool panel grounds (#231C15 for the minimum notice, #171E23 for the standing-rate argument) carry ink at 14.00:1 and 13.98:1 and copper/steel headings at 6.15:1 and 7.24:1.
The harness does not take any of that on trust. A sweep walks every rendered text run on the page, resolves its effective background by climbing ancestors until it finds an opaque one, and fails on anything under 4.5:1. It reports zero.
Type
Syne 700/800 for headings — wide, modern, a little food-startup, and it holds up at 48px on a dark ground where a thin grotesque would smear. DM Sans 400/500/700 for everything else, with tabular figures switched on globally because this page is almost entirely money and hours, and a ledger whose digits jitter reads as guesswork.
The typographic moment is the answer: the inc-GST total set at about 47px in Syne 800 at −0.035em, in copper, directly under a line that discloses the four-hour minimum. The hierarchy is deliberate — the disclosure is above the number, not in a footnote below it.
The proof element
One card does two jobs: it prices a block, and it shows what is actually free. Four controls (hours, casual or standing, cold-room shelves, dry shelves) and a first-booking toggle for the bond.
The arithmetic
Casual $42/h ex GST, standing weekly $38/h ex GST, a four-hour minimum, a $220 one-off refundable bond, cold-room shelves $18/week, dry store shelves $12/week. Anchored on the published Perth market rate of $38/h + GST with a four-hour minimum and a $215 bond.
Every rate is held in integer cents and divided once at display, because 42 * 6 * 1.1 in IEEE doubles is not a price. GST is round(ex / 10). Nothing rounds twice.
| Case | Ex GST | GST | Inc GST | Bond |
|---|---|---|---|---|
| 6 h casual, first booking | $252 | $25.20 | $277.20 | $220 |
| 2 h casual — billed as 4 | $168 | $16.80 | $184.80 | $220 |
| 5 h/week casual, a year | $10,920 | $1,092 | $12,012 | — |
| 5 h/week standing, a year | $9,880 | $988 | $10,868 | — |
| … plus one cold and one dry shelf | $11,440 | $1,144 | $12,584 | — |
The 2-hour case is the one the page is built around. The card does not quietly bill four hours; it says so above the total, and it names the money: “the 2 you are not using are $84 of room you have already paid for — bring the next batch.” $84 is 2 × $42, printed, not implied.
The seven-night strip
Twenty-eight four-hour blocks — tonight plus six nights, four blocks a night — each free or taken as a pure function of the ISO date through a fixed FNV-1a hash: h32(iso + '#' + blockIndex) % 1000 < threshold[blockIndex]. There is no Math.random anywhere on the page, so the grid is identical on reload, in the 1200×750 thumbnail and under the harness. The harness asserts that identity across a full reload.
The thresholds are the argument, not decoration: 36% for 6pm–10pm, 56% for 10pm–2am, 80% for 2am–6am and 62% for 6am–10am. The 2am row is labelled quietest and it is genuinely the freest, which is what makes the headline — “The bench is free at 4am. Most of ours are.” — a statement the drawing backs rather than a slogan.
Below 480px of drawn width the grid transposes: seven columns of four becomes seven rows of four, in a 300-unit viewBox instead of a 520-unit one, so no label on it ever renders under about 9px. A fixed viewBox would have put 6px type on a phone, which is the single most common way a drawn proof element dies on the device most of this traffic arrives on. The service-area map takes the other route to the same place: below 620px of drawn width its viewBox crops to the inner metro — identical geometry, larger scale, no re-tuned type — and every suburb is still named in the chip list beside it. A sweep across fifteen viewport widths from 320 to 1920 prints the smallest rendered label on both drawings; from 375 up, the minimum is 9.4px.
Every free cell is a real button — role="button", tabindex="0", Enter and Space bound, an aria-label naming the block — and tapping one puts that block on the enquiry form. The form is not pre-filled at rest, deliberately, so an empty submit still exercises validation honestly.
Where the page argues against its own sale
Three places, all of them adjacent to the number rather than buried in the FAQ.
- The four-hour minimum is disclosed before the total, in every state. Under four hours it names the padded money; at four or more it says plainly that nothing is padded. There is no state in which the reader meets the total without having met the minimum first.
- The standing rate is offered the moment it wins. The card prints the twelve-month comparison against itself — at the rest state, $11,856 standing against $13,104 casual, $1,248 kept by the customer — and the button next to it moves them onto the cheaper rate. At the spec’s 5-hour case that is $9,880 against $10,920, a $1,040 saving. The business does the margin arithmetic against itself in public because the customer would find it in month three anyway.
- The one-stall-a-month steer, given its own full-width panel between the card and the services grid rather than a line of small print. If somebody is trading once a month, a standing block is more room and more money than their product needs, and the page says so. It ends in something they can still do today: one four-hour casual block at $168 plus GST, no standing booking, no notice period.
The sentence that carries that last steer is not ours. It is the Department of Health’s, quoted verbatim beside the panel: “Local government enforcement agencies are responsible for enforcing the Food Act 2008, Food Regulations 2009 and the Australian and New Zealand Food Standards Code including Chapter 3: Food Safety Standards.” A first-person “you might not need us” reads as marketing modesty; the regulator’s own words read as a fact the operator chose not to hide.
What this page declined to publish
The carve-out for this build forbids any claim about the Food Act 2008 (WA), food business registration or notification, council approval, food-handler certification, or what may lawfully be produced in a home kitchen, unless it was verified at health.wa.gov.au or a named local government. Two sentences were verified at source and are quoted verbatim with attribution. Everything below was considered and left off:
- Whether a hirer must register or notify, and with whom — the page states the mechanism and sends the reader to their own local government.
- Any registration or notification fee, timeframe or form name.
- Any food-handler or food-safety-supervisor requirement.
- Any claim that this kitchen’s own status covers a hirer’s business. The page says the opposite, in the FAQ, in as many words: “that status is ours, it covers this address, and it does not transfer to your business.”
- Anything at all about cooking at home for sale.
The hero calls the premises “a registered commercial kitchen” because that is an in-world description of the fictional business, in the concept brief. It is never used to describe a requirement, a process or a benefit that transfers to the hirer.
Iteration log
Pass 1 — Craft
Drove the form empty (6 required fields flagged, confirmation stayed hidden) and filled (confirmation appeared, pricing the block at $252 plus GST), and drove the card through all four worked cases, asserting the probe each time. Found and fixed:
- The wordmark vanished at 375.
.mark span{display:none}below 380px was written for the tagline and matched the wrapper span too, so the brand name disappeared entirely on a phone — every state assert green, the masthead empty. Classes now name what they mean, and the phone number becomes a 46px copper icon button below 520px so the wordmark keeps its width. - The trust line ran 17px under the sticky call bar at 375×667. The fold is the viewport minus the bar. Hero rhythm tightened (padding 34→22, sub margin 16→13, trust 20/16→14/12) and the bar trimmed to 63px. The h1, the primary CTA and the first trust line now each finish above the bar, asserted individually.
- The hero’s left column was 700px of empty at 1200 while the proof card ran on. Added the six-line gear list — real copy the audience actually wants — and moved the one-stall steer out of the card into a full-width panel of its own, which both shortened the card and gave the refusal the design budget it deserves.
- The bond control was a single full-width copper toggle that read as a second primary CTA; split into a two-chip yes/no pair. The ledger printed “Ex GST” as a duplicate of the hours line when no shelves were selected; that row now appears only when there is something to subtotal.
- The masthead mark was an unreadable tangle at 30px; redrawn as a pot on a bench.
Pass 2 — Depth
Re-drove every case, plus the strip, plus a full contrast walk of the rendered DOM. Changed:
- The strip was 6px type at 375, and the map was 4.7px. Measured, not guessed, by a sweep over fifteen viewport widths that prints the smallest rendered label on each drawing. The grid now transposes below 480px of drawn width; the map crops its viewBox below 620px. Both redraw on a debounced resize. The first attempt at the map scaled its type by 1.28 inside the same frame instead of cropping, which promptly produced seven label collisions and one clipped ring label — the collision assert caught all eight, and cropping (same geometry, bigger scale) has none of them by construction.
- Three SVG label collisions, all invisible to state asserts and all found by looking: “Joondanna” over “Osborne Park”, “Herdsman” over “THE BENCH”, and “Wembley” over “Leederville”. The harness now cross-checks every pair of map label bounding boxes and every label against the viewBox; the first run of that check reported all three.
- “THE QUIET ROW” was clipped off the left edge of the strip — the left gutter was 74 units and the label needed 84. Gutter to 96, label to “QUIETEST ROW”, and a viewBox-clipping assert added so it cannot come back.
- The section band, the Sapience band and the footer were drawn on
bandwhile usingmutedandprimary, both of which the log lists FORBIDDEN there. Moved tosurfaceand the ground. The band now appears only where nothing butonDarksits on it. - Free strip cells became keyboard-reachable buttons that hand their block to the enquiry form; a “take the next free block” button does the same for the nearest one. Herdsman Lake was removed from the map — an unlabelled black blob next to a label reading “Herdsman” was worse than no lake.
- A
<footer>inside the quotation block inherited the page footer’s band background and border, painting a dark slab behind the attribution line. Scoped.
Pass 3 — Hardening
375 / 768 / 1440, reduced motion, and a planted-defect run against the harness itself.
- Junk in the phone field.
pattern="[0-9 \(\)\+\-]{8,20}"compiles under thevflag; the unescaped form does not, and av-invalid pattern is silently discarded rather than rejected, leaving the one field that has to be right accepting “ring me maybe”. Both the compile and the behaviour are asserted:checkValidity()on junk returns false. - Thumbnail is rest. The real 1200×750 capture is taken at 7s, then every numeric probe field is re-read 8s later and compared. Nothing moves. There is no frame accumulator on this page at all — the only animation is a CSS opacity pulse on the next-free ring, and it never drops below 0.62, so the signature is alive at any capture instant.
- Comes to rest visible. Controls driven, then the page left completely alone for 1.8s, then every animated group asserted opaque. Resize only re-measures the bar and re-checks the strip mode; it never re-enters the entrance.
- Reduced motion: the pulse class is not applied at all (asserted: zero
.pulsenodes), the entrance is off, and the card still computes $277.20. - 375:
scrollWidth375, no element crosses the right edge, wordmark and call button do not collide, sticky bar owns a realtel:and a#enquireanchor with 45px+ targets, and the mobile reading order is asserted monotonic — hero, services, proof, steer, how, reviews, map, FAQ, form. - Ornament removed under the Chanel rule: the compass rose on the service-area map. It was the smallest text on the drawing, it was the thing that clipped first when the viewBox cropped, and on a map whose entire job is “how far am I from the kitchen” it told the reader nothing. The map’s background rect also moved to the ground token rather than a minted mid-tone.
- Fifteen viewport widths from 320 to 1920 swept for horizontal overflow, console errors, and the smallest rendered label on both drawings. Zero overflow, zero errors, and from 375 up nothing under 9.4px.
Final harness: 123 assertions, 0 failures, across 1200, 375, 768, reduced motion, the guide page and the real 1200×750 thumbnail. Among them: every cent figure is an integer across all 1,176 reachable cells of the model; the price is monotonic in hours; standing never costs more than casual; and the cheapest reachable output ($168 ex GST) is exactly the services-grid floor.
That green was then tested rather than believed. Fourteen defects were planted into the page one at a time — a second h1, a LocalBusiness JSON-LD block, a curly-quoted attribute, a real street number, a console.error on load, the four-hour minimum removed, the hash replaced with Math.random, the strip and then the answer pushed out of the thumbnail frame, the v-invalid phone pattern restored, the GST divisor changed, the regulator quote paraphrased, white type put back on the copper button, and the confirmation panel shipped visible. All fourteen fired, and the file was restored byte-for-byte and verified after each one. A sweep that has never been watched fail is not evidence.
Why this converts
A caterer looking for a bench has one question — what does tonight cost and is tonight free — and this page answers both in the first screen, beside the headline, before they have scrolled or filled in anything. The four-hour minimum is disclosed above the total rather than after it, which costs the business nothing and buys the single thing a first-time hirer is actually scanning for: evidence that the number will not move once they are on the phone. The page then does the margin arithmetic against itself in public, printing the twelve-month standing-rate saving and offering a button that moves the customer onto the cheaper rate, because a quote that argues for the customer reads as a model rather than a pitch. The enquiry form asks for seven things and no more, five of which the availability grid can fill in with one tap, so the distance between “that block looks good” and a submitted enquiry is a single click and a phone number. And the hardest-working element on the page is the one that turns work away — the panel that tells a once-a-month stallholder they may not need a registered kitchen at all, quoting the regulator rather than the business, and still ending with something bookable today.
Conversion decisions
- The proof card sits in the hero’s second column at 1024px and up, while the mobile stack keeps the conventional order. The 1200×750 thumbnail is the sales asset; a thumbnail showing a headline and a menu of tiles sells nothing. The card’s answer sits inside the first 750px and at least 120px of the availability grid is in frame — both asserted, in pixels, not by eye.
- Two CTAs above the fold, one of them a phone call. This trade decides at 1am. The header carries a
tel:link, the hero carries the number in full, and the mobile bar owns a realtel:plus a form anchor. - Seven form fields. Name, phone, suburb, hours, block, what you cook, and an optional business name and email. “What do you cook” is required because it is the field that lets the kitchen say no on the first call instead of after an induction — a shorter form that books the wrong hirer is a worse form.
- The confirmation is local and says so. It re-prices the block from what the visitor typed, promises a callback inside four working hours, and states plainly that nothing was sent anywhere because this is a demonstration. No fake network round trip.
- Price anchors are on the tiles, not behind a form. Six services, each with an honest “from $”, and the estimate’s cheapest reachable output ($168 ex GST for the four-hour minimum at $42/h) is exactly the grid floor.
Local SEO we would implement for the real business
This page emits only WebPage schema naming itself a demonstration, because fictional structured data in a real index is dishonest markup. For a real commercial kitchen we would ship the following — and only once every fact in it was true.
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "FoodEstablishment",
"additionalType": "https://schema.org/SelfStorage",
"name": "Nightshift Kitchens",
"description": "Registered commercial kitchen in Osborne Park, hired by the hour, 24/7.",
"url": "https://nightshiftkitchens.com.au/",
"telephone": "+61 8 XXXX XXXX",
"priceRange": "$$",
"image": "https://nightshiftkitchens.com.au/img/cook-line.jpg",
"address": {
"@type": "PostalAddress",
"streetAddress": "Unit N, NNN Hutton Street",
"addressLocality": "Osborne Park",
"addressRegion": "WA",
"postalCode": "6017",
"addressCountry": "AU"
},
"geo": { "@type": "GeoCoordinates", "latitude": -31.898, "longitude": 115.815 },
"openingHoursSpecification": [{
"@type": "OpeningHoursSpecification",
"dayOfWeek": ["Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"],
"opens": "00:00", "closes": "23:59"
}],
"areaServed": [
{ "@type": "City", "name": "Perth" },
{ "@type": "AdministrativeArea", "name": "City of Stirling" }
],
"makesOffer": [
{ "@type": "Offer", "name": "Casual kitchen hire",
"priceSpecification": { "@type": "UnitPriceSpecification",
"price": "42.00", "priceCurrency": "AUD",
"unitCode": "HUR", "valueAddedTaxIncluded": false } },
{ "@type": "Offer", "name": "Standing weekly block",
"priceSpecification": { "@type": "UnitPriceSpecification",
"price": "38.00", "priceCurrency": "AUD",
"unitCode": "HUR", "valueAddedTaxIncluded": false } }
],
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "4.8", "reviewCount": "37"
}
}
</script>
Alongside it: a Google Business Profile in the Commercial kitchen category with the loading-door photo set and the real opening hours; a FAQPage block built from the six questions already on this page; one location page per suburb only where there is a genuine reason for one (there is not, for a single-site kitchen — a fourteen-suburb doorway farm would be the wrong call and we would say so); and the aggregateRating block wired to a live review feed rather than typed, because a hand-typed rating is exactly the kind of markup that earns a manual action.
What would change for a real client
- The availability grid becomes a real calendar. Shared-kitchen operators in this market run on Nexudus, OfficeRnD or Skedda for room and resource booking; a smaller single-site kitchen very often runs Google Calendar plus a Calendly or SimplyBook.me front end, and the food-specific platforms are The Food Corridor and Kitchen Door. The hash on this page is a stand-in for one API call: swap
buildNights()for the operator’s free/busy feed and every other line of the card is unchanged. - Payments and the bond. Stripe for the card-on-file and the pre-authorised bond hold, Xero for the recurring invoice on standing blocks with GST handled as a tax rate rather than as arithmetic in a page, and GoCardless or Stripe direct debit for weekly hirers. The bond becomes a hold and a release, not a line item.
- Access control. The fob in the copy is a real integration — Salto KS, Kisi or Brivo — issuing a credential that is live only for the booked block, which is also what makes the clean-down check enforceable.
- Their photos and their reviews. The cook line, the cold room and the loading door, shot at night; the reviews pulled live from the Google Business Profile rather than typed, which is what lets the
aggregateRatingblock ship honestly. - Their compliance pack. A real operator would link their own food business documentation and their insurance position here. We would still not tell the hirer what their own business must hold — that stays a link to the hirer’s local government, because it is the only answer that is true for every hirer.
Techniques
- One self-contained HTML file, all CSS and JS inline, no libraries, no build step, no raster images. Index page about 78KB, this guide about 37KB.
- Every visual is procedural: the availability grid, the drive-time map and the wordmark are inline SVG assembled synchronously in JavaScript; the card’s top edge is a CSS gradient; the favicon is an SVG data-URI.
- The desktop layout is a three-row CSS grid on the hero block — hero and proof side by side, steer full width, services full width — placed by explicit line numbers rather than by id rules, so no descendant selector can out-specify the column placement.
- SVG toggling, where it happens, uses the
hiddenattribute against a[hidden]{display:none!important}guard, never the HTMLElement IDL property, which silently does nothing on an SVG element. - The live state model is exposed at
window.__kitchenhire— every input, every derived figure in cents and formatted, the seven nights with their free-block counts, and the next free block — so the QA harness asserts against the model rather than scraping pixels.