HardYards/tools/jobsheet/index.html
type-two 8ec01028f9 Lane E, Sprint 11: client juice — job sheet, invoice, and the kid's bike
The letterhead is the point: one business, two documents, same masthead, so
the job sheet and the invoice read as a pair of papers off the same outfit's
printer rather than two game screens. DESIGN.md has opened with "you run a
small landscaping outfit" since day one and it has never been on the glass.

tools/jobsheet/ — paste-ready for Lane A between CUT markers, purely additive
(.card/.row/.ledger untouched), rendered to four PNGs before handover, each
state addressable as ?v=. The ledger already WAS an invoice; it just never
said so. The line worth keeping is .void: a bonus that was on the job sheet
and didn't survive the night, struck through in the same column as the money
that did — a bonus you never watch fail is one you never learn to protect.

Two design calls left as A's with the reasoning beside them, same as the
carport's $180: the business name (proposal: HARD YARDS) and the ABN
(sequential digits on purpose — a plausible one could be a real business's).

bike_kid_01.glb — 1.15 x 0.33 x 0.74 m, 700 tris, lean baked in because a
bike doesn't stand up on its own. Two bugs caught by LOOKING at the render,
neither catchable by the existing suite: a comment claiming a step-through
above a diamond frame, and a fork with negative rake that drew head tube and
fork as one pole through the front wheel. Both passed the Blender verify —
a bounding box can't answer "is this the right shape".

The lean is an export-time axis fact (+Y in Blender is -Z in three.js), so
e.test.js now pins it in browser coords. Proved the assert fails: rebuilt at
lean=0 and Blender still said PASS while the selftest went red.

selftest 299/0/0. 32/32 GLBs byte-identical across three full factory runs.
2026-07-17 17:39:58 +10:00

409 lines
20 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>SHADES — job sheet + invoice preview (Lane E → Lane A)</title>
<style>
/* ==========================================================================
LANE A — SPRINT 11 GATE 2. Everything between the CUT markers is yours to
paste into hud.js. Same deal as the end cards last sprint: rendered and
screenshotted before it was handed over, so it's a paste rather than a
design job. Take it, edit it, or bin it.
It is deliberately ADDITIVE. I have not touched .card / .row / .ledger —
every rule below is a new class, so the cards keep working exactly as they
do today if you paste this and wire nothing. The job sheet is the forecast
card wearing a letterhead; the invoice is your .ledger wearing one too.
Your existing markup slots in underneath both.
THE TWO DESIGN CALLS IN HERE ARE YOURS, NOT MINE — see BUSINESS and ABN
at the top of the script block. My proposals are there with the reasoning
beside them so you can argue with them, same as the carport's $180.
====================================================================== */
/* ---- CUT HERE ---------------------------------------------------------- */
/* --- the letterhead ----------------------------------------------------
One business, two documents. This is the whole trick of the sprint: the
job sheet and the invoice carry the SAME letterhead, so they read as a
pair of papers off the same outfit's printer rather than as two game
screens. The player's business is the constant; the client changes every
night. That's the framing DESIGN.md opens with — "you run a small
landscaping outfit" — finally showing up on the glass.
Sits above the existing .card content, inside the same .card box. */
#hud-card .letterhead {
display: flex; align-items: baseline; justify-content: space-between;
gap: 16px; margin: -4px 0 14px; padding-bottom: 10px;
border-bottom: 2px solid #2f3f4a;
}
#hud-card .letterhead .mark {
font-weight: 800; font-size: 15px; letter-spacing: .22em; color: #fff;
white-space: nowrap;
}
/* The trade line does the work a logo would. Lowercase and spaced out so it
reads as printed rule, not as a heading competing with the mark. */
#hud-card .letterhead .trade {
font-size: 10px; letter-spacing: .13em; color: #6d818e; margin-top: 3px;
}
#hud-card .letterhead .docket { text-align: right; white-space: nowrap; }
#hud-card .letterhead .docket .kind {
font-weight: 700; font-size: 11px; letter-spacing: .18em; color: #8ba0ad;
}
#hud-card .letterhead .docket .no { font-size: 10px; color: #55677a; letter-spacing: .06em; }
/* --- bill-to / job-for block -------------------------------------------
Deliberately the same component on both documents. On the job sheet it's
who you're working for; on the invoice it's who's paying. Same box, and
the fact that it doesn't move between the two is the joke landing. */
#hud-card .billto { margin: 0 0 12px; }
#hud-card .billto .who { font-weight: 700; color: #fff; letter-spacing: .04em; }
#hud-card .billto .addr { font-size: 11px; color: #6d818e; }
/* --- the brief ---------------------------------------------------------
The client's own words. Italic and rule-marked because it is the one bit
of text on the card that isn't the game talking to the player — it's a
person who wants their seedlings alive. It should feel quoted, and it
should be the thing the player's eye lands on after the letterhead,
because it is the only place the stakes are ever said out loud. */
#hud-card .brief {
margin: 0 0 14px; padding: 9px 0 9px 12px;
border-left: 2px solid #3f5561;
color: #c3d0d9; font-style: italic; line-height: 1.6;
}
#hud-card .brief .attrib {
display: block; margin-top: 5px; font-style: normal;
font-size: 10px; letter-spacing: .1em; color: #55677a;
}
/* --- section rules ------------------------------------------------------ */
#hud-card .sect {
margin: 16px 0 7px; font-size: 10px; font-weight: 700; letter-spacing: .18em;
color: #55677a; border-bottom: 1px solid #1c262d; padding-bottom: 5px;
}
/* --- the pay schedule (job sheet) --------------------------------------
This is a QUOTE, not a receipt: it's what the night is worth if you do it
right. It reuses your .row shape so it lines up with everything else, but
the money is dimmed — these are amounts you have NOT earned yet, and they
should not read with the same confidence as the bank line. The conditions
are the point; the numbers are just what the conditions are worth. */
#hud-card .sched .row b { color: #9fb3bf; font-weight: 700; }
#hud-card .sched .cond {
font-size: 10px; color: #55677a; letter-spacing: .02em;
padding: 0 0 5px; border-bottom: 1px solid #1c262d; margin-top: -2px;
}
#hud-card .sched .row.headline b { color: #fff; }
#hud-card .sched .row.max { border-bottom: 0; margin-top: 6px; padding-top: 7px;
border-top: 1px solid #24343d; }
#hud-card .sched .row.max span { color: #8ba0ad; }
#hud-card .sched .row.max b { color: #ffd9a3; }
/* --- the invoice -------------------------------------------------------
Your .ledger already IS an invoice; it just never said so. These rules sit
ON TOP of it — paste them and .ledger keeps every behaviour it has, it
just gains a rule above the total and a due line under it.
.earned/.bad you already colour. The one thing I've added is .void: a
bonus that was on the job sheet and did NOT survive the night. Struck
through, not hidden. The player must see the money they didn't get sitting
in the same column as the money they did — a bonus you never see fail is a
bonus you never learn to protect. This is the single highest-value line in
the whole handover, and it's four CSS declarations. */
#hud-card .ledger .row.void span { color: #55677a; }
#hud-card .ledger .row.void b {
color: #55677a; font-weight: 400; text-decoration: line-through;
text-decoration-color: #7d2b2b; text-decoration-thickness: 1px;
}
/* The amount due. An invoice's whole rhetorical shape is that everything
above is argument and this is the conclusion. Give it the weight. */
#hud-card .ledger .row.due {
margin-top: 8px; padding-top: 9px; border-top: 2px solid #2f3f4a; border-bottom: 0;
align-items: baseline;
}
#hud-card .ledger .row.due span { color: #8ba0ad; letter-spacing: .12em; font-weight: 700;
font-size: 11px; }
#hud-card .ledger .row.due b { color: #fff; font-size: 20px; letter-spacing: .02em; }
#hud-card .ledger .row.due.neg b { color: #ff8f86; }
/* The terms line. Pure flavour, and the cheapest joke on the card: a real
invoice's most boring sentence, printed under a night that nearly killed
you. Left in as its own class so you can delete it in one line if it
doesn't land. */
#hud-card .terms {
margin-top: 10px; font-size: 10px; color: #45566a; letter-spacing: .04em;
}
/* ---- CUT HERE ---------------------------------------------------------- */
/* ---- preview chrome below; NOT part of the snippet --------------------- */
body { margin: 0; background: #060a0d; }
/* padding-top clears the toggle bar so a headless screenshot of any state is
a clean handover artifact rather than one with buttons through the
letterhead. Learned that the hard way on the first render. */
#hud-card { position: static !important; display: grid !important; place-items: start center;
padding: 74px 0 26px; }
.bar { position: fixed; z-index: 60; left: 50%; transform: translateX(-50%); top: 14px;
display: flex; gap: 8px; }
.bar button { font: 12px ui-monospace, Menlo, monospace; padding: 8px 12px; cursor: pointer; }
.lbl { position: fixed; left: 14px; bottom: 12px; z-index: 60;
font: 11px ui-monospace, Menlo, monospace; color: #55677a; letter-spacing: .1em; }
</style>
<style id="hudcss">
/* Lifted verbatim from hud.js so the preview renders in the real card's
skin. Do NOT paste this block — you already have it. It's here so that
what I screenshotted is what you'll actually see. */
#hud-card { inset: 0; z-index: 30; place-items: center;
font: 13px/1.7 ui-monospace, SFMono-Regular, Menlo, monospace; color: #dde5ea; }
#hud-card .card { background: #0d1418; border: 1px solid #2f3f4a; border-radius: 9px;
padding: 22px 26px; min-width: 440px; max-width: 620px; pointer-events: auto; }
#hud-card h1 { margin: 0 0 2px; font-size: 19px; letter-spacing: .16em; color: #fff; }
#hud-card h2 { margin: 0 0 16px; font-size: 12px; font-weight: 400; color: #8ba0ad;
letter-spacing: .04em; }
#hud-card .row { display: flex; justify-content: space-between; gap: 18px; padding: 3px 0;
border-bottom: 1px solid #1c262d; }
#hud-card .row b { font-weight: 700; color: #fff; }
#hud-card .stat { color: #8ba0ad; }
#hud-card .go { margin-top: 16px; padding: 9px 18px; background: #1d3d2a; border: 1px solid #3f7a52;
border-radius: 6px; color: #a8f0b8; cursor: pointer; font: inherit; font-weight: 700;
letter-spacing: .1em; }
#hud-card .verdict { margin: 14px 0 0; padding: 10px 12px; border-radius: 6px; font-weight: 700;
letter-spacing: .05em; }
#hud-card .verdict.win { background: #12321c; border: 1px solid #2c6b3c; color: #7fce6a; }
#hud-card .verdict.lose { background: #3a1618; border: 1px solid #7d2b2b; color: #ff8f86; }
#hud-card .pips { letter-spacing: .5em; font-size: 15px; margin: -6px 0 10px; color: #3f5561; }
#hud-card .pip.now { color: #7ee0ff; }
#hud-card .pip.held { color: #7fce6a; }
#hud-card .pip.lost { color: #ff8f86; }
#hud-card .ledger { margin-top: 14px; padding-top: 10px; border-top: 1px solid #24343d; }
#hud-card .ledger .row b { color: #a8f0b8; }
#hud-card .ledger .row.bad b { color: #ff8f86; }
#hud-card .ledger .row.total { margin-top: 6px; padding-top: 8px; border-top: 1px solid #24343d; }
#hud-card .ledger .row.total b { color: #fff; }
</style>
</head>
<body>
<div class="lbl" id="lbl">JOB SHEET · night 1</div>
<div class="bar">
<button onclick="show('sheet1')">job sheet n1</button>
<button onclick="show('sheet3')">job sheet n3 (corner block)</button>
<button onclick="show('inv_good')">invoice · clean</button>
<button onclick="show('inv_bad')">invoice · carport</button>
</div>
<div id="hud-card" class="on"><div id="slot"></div></div>
<script>
/* ==========================================================================
LANE A — THE TWO DESIGN CALLS. Both are yours. Reasoning is here so you can
overrule it from an informed position rather than just accepting my taste.
====================================================================== */
/* CALL 1 — THE BUSINESS NAME. A letterhead needs an outfit on it, and we've
never named the player's business. My proposal is HARD YARDS.
Reasoning: it's a real Australian idiom ("doing the hard yards" = the
unglamorous grind that nobody thanks you for), it is literally about yards,
and it's the joke the game is ALREADY telling — DESIGN.md says you never
build the right thing, you build the least wrong thing, and THREADS' own
line for the win card is "nobody thanks you for the storm that did nothing.
That's the job." That is the hard yards. It's one word at letterhead size,
which is what a mark needs to be, and it stays funny on an invoice for $180
of someone else's carport.
What I deliberately did NOT do: name it after the player. A surname mark
("<NAME> LANDSCAPING") is the most authentic thing a one-person outfit
would actually have, and it's the better answer the moment the player has a
name — but naming the player is a much bigger call than a letterhead, and
it's not mine to make in a juice pass. If Sprint 12 names them, this
constant is the only thing that changes. */
const BUSINESS = { mark: 'HARD YARDS', trade: 'landscaping · shade · drainage' };
/* CALL 2 — THE ABN. An Australian invoice without an ABN is not an invoice;
it's the single detail that makes the paper read as real to anyone who's
ever received one. But any plausible 11-digit number is potentially a REAL
business's ABN, and I'm not printing a stranger's tax identity on a game
card. So: sequential digits, which every Australian reader parses instantly
as "example" — the same convention as 555 phone numbers. Deliberate, not
lazy. Please don't "fix" it into something realistic. */
const ABN = 'ABN 12 345 678 901';
/* ==========================================================================
Lane A: swap this for your real night entries. The markup doesn't care what
the rows are — it lays out label/value pairs and a brief. Shape below is
what SPRINT11 gate 2 asks for: client + brief + pay schedule, as data.
Note the pay schedule is a LIST, not three named fields. base/garden/
no-collateral is what Sprint 11 specifies, but a list costs nothing now and
means a site that wants a fourth condition ("nothing tied to the carport")
doesn't need a card change — which, per SPRINT10, is the whole point of
sites being data.
====================================================================== */
const NIGHTS = {
sheet1: {
kind: 'sheet', night: 1, nights: 5, docket: 'JOB 1041',
client: 'R. & M. Henderson', addr: '14 Kurrajong St — backyard',
brief: 'The seedlings have to be alive when we get back. We fly out Friday and the beds are three weeks in.',
attrib: '— R. Henderson, on the phone, Monday',
storm: 'SOUTHERLY BUSTER', night_flag: false,
lines: ['gusting to 21 m/s, building after 9', 'rain: steady, 12 mm', null],
note: 'Nothing tonight can hurt the garden. Learn the anchors.',
bank: 95,
sched: [
['callout fee', '$80', 'the night, rigged and stood up', true],
['garden bonus', '$40', 'bed above 80% at dawn'],
['clean site', '$25', 'nothing of theirs broken'],
],
max: 145,
},
sheet3: {
kind: 'sheet', night: 3, nights: 5, docket: 'JOB 1043',
client: 'D. Okafor', addr: '2 Bight Rd — corner block',
site: 'the corner block — two streets, no shelter, and a carport that wants to be an anchor',
brief: "Whatever you tie to, don't tie to the carport. It's older than it looks and I'm not paying to put it back up.",
attrib: '— D. Okafor, written on the job docket',
storm: 'EARLY BUSTER', night_flag: true,
lines: ['gusting to 32 m/s, funnelled', 'rain: heavy · hail forecast', 'confidence: ±4 m/s (lead 0)'],
note: 'Hail is what kills a garden, and cloth stops hail. Get the sail over the bed.',
bank: 210,
sched: [
['callout fee', '$80', 'the night, rigged and stood up', true],
['garden bonus', '$40', 'bed above 80% at dawn'],
['clean site', '$25', 'nothing of theirs broken'],
],
max: 145,
},
inv_good: {
kind: 'invoice', night: 1, docket: 'INV 1041',
client: 'R. & M. Henderson', addr: '14 Kurrajong St — backyard',
subtitle: 'The bed came through it. So did the rig.',
rows: [['garden', '94%'], ['corners intact', '4/4'], ['what got through', 'nothing'],
['hardware lost', 'none'], ['collateral', 'none']],
ledger: [
['callout fee', '+$80'],
['garden bonus', '+$40'],
['clean site', '+$25'],
['gear recovered', '+$18'],
['spent on the rig', '$62', 'bad'],
],
due: '+$101', bank: '$95 → $196',
verdict: "Nobody thanks you for the storm that did nothing. That's the job.",
win: true,
},
inv_bad: {
kind: 'invoice', night: 3, docket: 'INV 1043',
client: 'D. Okafor', addr: '2 Bight Rd — corner block',
subtitle: 'The garden made it. The carport went to the neighbours.',
rows: [['garden', '86%'], ['corners intact', '3/4'], ['what got through', 'some hail'],
['hardware lost', '$34'], ['collateral', 'the carport ($180)']],
ledger: [
['callout fee', '+$80'],
['garden bonus', '+$40'],
['clean site', '$25', 'void'],
['gear recovered', '+$6'],
['the carport', '$180', 'bad'],
['spent on the rig', '$71', 'bad'],
],
due: '$125', neg: true, bank: '$210 → $85',
verdict: 'You took the carport with you.',
win: false,
terms_extra: "They'll want that back up by Thursday.",
},
};
/* --- renderers. Lane A: these are throwaway — your hud.js already builds the
rows; all you need from here is the MARKUP SHAPE and the class names. ---- */
function letterhead(n) {
return `<div class="letterhead">
<div>
<div class="mark">${BUSINESS.mark}</div>
<div class="trade">${BUSINESS.trade}</div>
</div>
<div class="docket">
<div class="kind">${n.kind === 'sheet' ? 'JOB SHEET' : 'TAX INVOICE'}</div>
<div class="no">${n.docket} · ${ABN}</div>
</div>
</div>`;
}
function billto(n) {
return `<div class="billto">
<div class="who">${n.client}</div>
<div class="addr">${n.addr}</div>
</div>`;
}
function brief(n) {
return `<div class="brief">${n.brief}<span class="attrib">${n.attrib}</span></div>`;
}
function pips(night, nights) {
return Array.from({ length: nights }, (_, i) => {
const done = i < night - 1, now = i === night - 1;
return `<span class="pip ${now ? 'now' : done ? 'held' : ''}">${now ? '◆' : done ? '●' : '·'}</span>`;
}).join('');
}
function renderSheet(n) {
const site = n.site ? `<div class="stat" style="color:#7ee0ff">${n.site}</div>` : '';
const sched = n.sched.map(([k, v, cond, head]) =>
`<div class="row ${head ? 'headline' : ''}"><span>${k}</span><b>${v}</b></div>
<div class="cond">${cond}</div>`).join('');
return `<div class="card">
${letterhead(n)}
${billto(n)}
${brief(n)}
<h1>NIGHT ${n.night} OF ${n.nights}</h1>
<div class="pips">${pips(n.night, n.nights)}</div>
<h2>${n.storm}${n.night_flag ? ' · NIGHT' : ''}</h2>
${site}
${n.lines.filter(Boolean).map((l) => `<div class="stat">${l}</div>`).join('')}
<div class="sect">WHAT IT PAYS</div>
<div class="sched">
${sched}
<div class="row max"><span>the night, done right</span><b>$${n.max}</b></div>
</div>
<div class="row" style="margin-top:14px"><span>in the bank</span><b>$${n.bank}</b></div>
<div class="stat" style="color:#8ba0ad;margin-top:10px">${n.note}</div>
<button class="go">RIG IT</button>
<div class="terms">Quoted on the forecast at time of callout. Weather is not a variation.</div>
</div>`;
}
function renderInvoice(n) {
const rows = n.rows.map(([k, v]) => `<div class="row"><span>${k}</span><b>${v}</b></div>`).join('');
const ledger = n.ledger.map(([k, v, cls]) =>
`<div class="row ${cls || ''}"><span>${k}</span><b>${v}</b></div>`).join('');
return `<div class="card">
${letterhead(n)}
${billto(n)}
<h1>MORNING · NIGHT ${n.night} OF 5</h1>
<h2>${n.subtitle}</h2>
${rows}
<div class="ledger">
${ledger}
<div class="row due ${n.neg ? 'neg' : ''}"><span>AMOUNT DUE</span><b>${n.due}</b></div>
<div class="row total"><span>in the bank</span><b>${n.bank}</b></div>
</div>
<div class="verdict ${n.win ? 'win' : 'lose'}">${n.verdict}</div>
<button class="go">NIGHT ${n.night + 1} →</button>
<div class="terms">Payable 14 days. ${n.terms_extra || 'Thank you for your business.'}</div>
</div>`;
}
function show(which) {
const n = NIGHTS[which];
document.getElementById('slot').innerHTML =
n.kind === 'sheet' ? renderSheet(n) : renderInvoice(n);
document.getElementById('lbl').textContent =
(n.kind === 'sheet' ? 'JOB SHEET · night ' : 'INVOICE · night ') + n.night;
}
/* ?v=sheet3 etc. — so a state is a URL. That's how the screenshots in
tools/jobsheet/*.png were taken (headless), and it means you can link me a
broken one rather than describe it. */
show(new URLSearchParams(location.search).get('v') || 'sheet1');
</script>
</body>
</html>