Decision page: full-fidelity comps, raise cycler, declined sizing, canon order, full card anatomy (#545)

* Polish the decision page: raise cycler, declined height, canon order, full card anatomy

Field feedback from the first real rolls of the verdict-routed hand:

- Several raises stacked on the assigned card blew it out of proportion.
  More than one raise now renders as a compact cycler: one visible, a
  counter, click or Enter advances. A single raise stays inline.
- Declined cards inherited the row's stretch alignment, so a narrow card
  stood at the tallest contender's height, a strange stilt beside the
  hand. They now size to their content.
- Deck order becomes a gradient of standing: contenders, then the canon,
  then declined dead last. The canon between full alternates and the
  demoted row reads as the familiar door rather than the last resort
  after the rejects.
- Root cause of bare-bones challenger and canon cards in the field: the
  --schema example only gave the assigned card palette, materials, and
  risk, and models author payloads by imitating the example, so the
  "same anatomy on every card" instruction lost to it every time. The
  example now carries full anatomy on every card and the schema note says
  a card with no palette chips is an authoring gap, not a data gap.

AI-assisted change.

Co-Authored-By: Claude Code <noreply@anthropic.com>

* Decision cards carry full-fidelity comps instead of sketches

Field verdict on the sketch contract: the sketches came back too simple
to inform the choice, and generation takes the same time at any
fidelity, so the deliberately-unfinished frame paid comp cost for sketch
quality. The decision card's image is now that direction's north-star
comp, produced under visualize.md's comp discipline (structure-led
prompt, real name and content, no invented commercial claims), saved
under .impeccable/mocks/ with its prompt sidecar. Fairness between cards
comes from equal fidelity in each card's own grammar rather than shared
unfinishedness.

The chosen card's comp is never spent by the choice: on a comp-led build
it enters the comp round as compositional option one (visualize.md now
generates two variations beside it; a round arriving with no decision
comp still renders all three), and on a code-led build it returns at the
finish review as the critique reference. Produce order still front-loads
a re-roll's spend onto the cards read first.

serve-question keeps the sketch field's wire name for payload
compatibility; docs, schema paths, shimmer labels, and the answer
directive (CHOSEN COMP) speak comp.

AI-assisted change.

Co-Authored-By: Claude Code <noreply@anthropic.com>

* fix: address PR review bot findings on the comp round

- Producer still forced sketches (cursor, high): the asset producer's
  Decision Sketches contract still mandated deliberately unfinished matte
  sketches, so the parallel path would keep shipping sketch-era images.
  The section is now Decision Comps: full-fidelity north-star comp,
  structure-led prompt, equal commitment across siblings, no invented
  claims, sidecar written.
- Mocks collided with the approval check (cursor, high): decision comps
  now live under .impeccable/mocks/decision/, visualize.md scopes the
  no-approval finding to comp-round output, new-work.md states the
  unchosen hand implies no approval, and the code-led finish packet names
  the chosen decision comp as the critique reference in the approved-comp
  slot.
- Raise cycler announces (greptile, both P1s): a visually hidden
  aria-live region reads out the newly active raise and its position on
  advance; initial render stays quiet.
- Declined width in the vertical deck (cursor, medium): align-self:
  flex-start shrank declined cards to content width in the portrait
  column layout, where the cross axis is horizontal; they stretch there
  and keep content height in the row layout.

AI-assisted change.

Co-Authored-By: Claude Code <noreply@anthropic.com>

* fix: raise cycler tooltip and label name both input modes

Copilot: the tooltip said Click while the control also answers Enter and
Space; the title and a new aria-label now say activate/press Enter.

AI-assisted change.

Co-Authored-By: Claude Code <noreply@anthropic.com>

* fix: finish reviewer exempts decision comps from the approval check

cursor[bot] follow-through: the reviewer's Persistence check still
treated any comps under .impeccable/mocks/ as approval-gated, and the
reviewer never reads visualize.md by design, so code-led and spent-hand
rounds could draw a false skipped-approval finding. The check now scopes
to comp-round comps, exempts .impeccable/mocks/decision/ as the direction
round's dealt hand, and defines how a code-led build's decision comp is
judged in the approved-comp slot: the critique reference, under the
no-approved-comp fidelity rules plus what the image dared that the build
did not.

AI-assisted change.

Co-Authored-By: Claude Code <noreply@anthropic.com>

* fix: the critique reference is its own reviewer input, not the approved-comp slot

cursor[bot]: passing the code-led decision comp through the approved-comp
slot dragged in that slot's obligations (inventory-first reading, the
fidelity matrix, Truth's shipped-asset demand for every image-native
region), which contradicts code-led's premise. The input contract now
names it a separate labeled critique-reference input that nothing binding
"the approved comp" touches, and Fidelity defines its treatment where the
no-approved-comp rules live: provocation, not spec; no matrix, citations,
or asset obligations; its dares enter material_fixes as ordinary fixes.

AI-assisted change.

Co-Authored-By: Claude Code <noreply@anthropic.com>

---------

Co-authored-by: Claude Code <noreply@anthropic.com>
This commit is contained in:
Paul Bakaus
2026-08-08 15:47:11 -07:00
committed by GitHub
co-authored by Claude Code
parent d65a08b064
commit dbff0880e6
7 changed files with 89 additions and 33 deletions
+75 -21
View File
@@ -42,10 +42,12 @@
* // raise lines under the identity row
* "risk": "one line: the honest risk", // optional
* "body": "fallback prose when the structured fields are absent",
* "sketch": ".impeccable/sketches/assigned.webp", // optional; may not exist
* // yet: the page shimmer-waits and polls the
* // slot until the file lands, so serve first
* // and generate after
* "sketch": ".impeccable/mocks/decision/assigned.webp", // optional; the card's
* // full-fidelity direction comp (the field
* // keeps the sketch era's wire name). May not
* // exist yet: the page shimmer-waits and
* // polls the slot until the file lands, so
* // serve first and generate after
* "hero": "https://... or /abs/path.webp", // optional inspiration image;
* // rides picture-in-picture when a sketch exists
* "board": "https://... or /abs/path.webp" // optional secondary image
@@ -147,7 +149,7 @@ function printAnswer(raw) {
console.log("CHOSEN CARD: open the chosen world's board and hero images now, before any code. When your harness only reads files, or runs sandboxed, download them INTO the workspace and open the relative path; a sandboxed viewer rejects absolute paths outside it. They set the craft bar the build must reach.");
}
if (a.sketch) {
console.log('CHOSEN SKETCH: the decision sketch at that path may seed one comp probe; the comp round still renders its full set, because a sketch chose the direction, not the composition.');
console.log('CHOSEN COMP: the decision comp at that path is compositional option one. On a comp-led build the comp round adds two variations beside it; on a code-led build it returns at the finish review as the critique reference. Never regenerate it from scratch.');
}
if (a.optionId === 'canon') {
console.log('CANON CHOSEN: the user picked the category standard on purpose. Ask once for two or three products this should sit alongside; their craft level becomes the quality bar. Execute the canon at full commitment, conventions embraced without irony or smuggled quirk.');
@@ -173,17 +175,17 @@ if (hasFlag('schema')) {
title: 'Choose the visual world',
question: 'The roll assigned Fillmore Handbill. Keep it, take an alternate, or re-roll.',
options: [
{ id: 'assigned', label: 'Fillmore Handbill', kicker: 'THE ROLL', lineage: '1966-71 Fillmore psychedelic handbills', thesis: 'The gig poster that treats every release like a one-night stand.', palette: ['#e8452c', '#f5d64c', '#1b2a52', '#f3ead8'], materials: ['letterpress', 'split-fountain ink'], viewport: 'A full-bleed dated bill with the product name in warped display type.', risk: 'Reads nostalgic when the type is set timidly.', raised: [{ from: 'challenger-microfiche', raise: 'The bill now owns its whole viewport as one continuous printed sheet.' }], sketch: '.impeccable/sketches/assigned.webp', hero: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill-hero.webp', board: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill.webp' },
{ id: 'model-pick', label: 'The Broadside Ballad', kicker: 'MY PICK', lineage: 'street-sold ballad sheets', thesis: 'Every release printed as the days ballad sheet.', risk: 'Also the direction most runs in this category land on.', sketch: '.impeccable/sketches/model-pick.webp' },
{ id: 'challenger-teletext', label: 'Teletext Service', verdict: 'competitive', lineage: 'broadcast teletext magazines', thesis: 'The catalog as a broadcast index: pages, not sections.', case: 'Fuses cleanly: releases map to numbered pages; loses narrowly on clarity.', sketch: '.impeccable/sketches/challenger-teletext.webp', hero: 'https://impeccable.style/worlds/cards/broadcast-programming-teletext-service-hero.webp' },
{ id: 'challenger-microfiche', label: 'Microfiche Reader', verdict: 'declined', lineage: 'library microfiche stations', case: 'Fuses poorly: listeners do not identify with archival retrieval.', kept: 'Total environmental commitment.', hero: 'https://impeccable.style/worlds/cards/archives-microfiche-reader-hero.webp' },
{ id: 'assigned', label: 'Fillmore Handbill', kicker: 'THE ROLL', lineage: '1966-71 Fillmore psychedelic handbills', thesis: 'The gig poster that treats every release like a one-night stand.', palette: ['#e8452c', '#f5d64c', '#1b2a52', '#f3ead8'], materials: ['letterpress', 'split-fountain ink'], viewport: 'A full-bleed dated bill with the product name in warped display type.', risk: 'Reads nostalgic when the type is set timidly.', raised: [{ from: 'challenger-microfiche', raise: 'The bill now owns its whole viewport as one continuous printed sheet.' }], sketch: '.impeccable/mocks/decision/assigned.webp', hero: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill-hero.webp', board: 'https://impeccable.style/worlds/cards/posters-covers-sleeves-fillmore-handbill.webp' },
{ id: 'model-pick', label: 'The Broadside Ballad', kicker: 'MY PICK', lineage: 'street-sold ballad sheets', thesis: 'Every release printed as the days ballad sheet.', palette: ['#1f1c18', '#efe5d0', '#a33327'], materials: ['woodcut', 'rag paper'], viewport: 'One tall sheet, the newest release as todays ballad.', risk: 'Also the direction most runs in this category land on.', sketch: '.impeccable/mocks/decision/model-pick.webp' },
{ id: 'challenger-teletext', label: 'Teletext Service', verdict: 'competitive', lineage: 'broadcast teletext magazines', thesis: 'The catalog as a broadcast index: pages, not sections.', palette: ['#0000c0', '#ffff00', '#00c000', '#ffffff'], materials: ['block mosaic', 'phosphor glow'], viewport: 'P100 index page, releases as numbered rows.', case: 'Fuses cleanly: releases map to numbered pages; loses narrowly on clarity.', risk: 'Reads retro-novelty when the grid is not strict.', sketch: '.impeccable/mocks/decision/challenger-teletext.webp', hero: 'https://impeccable.style/worlds/cards/broadcast-programming-teletext-service-hero.webp' },
{ id: 'challenger-microfiche', label: 'Microfiche Reader', verdict: 'declined', lineage: 'library microfiche stations', palette: ['#101418', '#9fb4c0'], materials: ['film grain', 'backlit glass'], case: 'Fuses poorly: listeners do not identify with archival retrieval.', kept: 'Total environmental commitment.', hero: 'https://impeccable.style/worlds/cards/archives-microfiche-reader-hero.webp' },
],
reroll: { registers: ['safer', 'bolder'] },
canon: true,
canonCard: { label: 'The category standard', thesis: 'What this category ships, executed impeccably.', viewport: 'The arrangement a visitor expects, at full craft.', sketch: '.impeccable/sketches/canon.webp' },
canonCard: { label: 'The category standard', thesis: 'What this category ships, executed impeccably.', palette: ['#ffffff', '#111827', '#2563eb'], materials: ['clean grid', 'product photography'], viewport: 'The arrangement a visitor expects, at full craft.', risk: 'Indistinguishable from the competition by design.', sketch: '.impeccable/mocks/decision/canon.webp' },
steer: true,
}, null, 2));
console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/sketch accept URLs or local paths; sketch slots may point at files that do not exist yet (serve first, generate after; the page polls until they land, so never block serving on generation). hero on a challenger is the inspiration it draws from and renders picture-in-picture beside the sketch, never as the promise of the build. verdict routes rendering: "wins" and "competitive" challengers keep full cards, "declined" ones render demoted after them (narrow, quiet, art as a labeled thumb, "Adopt anyway"), with their kept line on the front; the page reorders declined cards to the end on its own. raised on the assigned card renders each donation as a named raise line. Salience parity: when the assigned card declares no sketch (no image generation this round), catalog art on every card demotes to a labeled thumb, so what looks important is the verdicts call, never rendering luck. canonCard renders the standing exit as a subordinate card with the same anatomy; without it, canon stays a quiet footer action. Include canon only for visual-direction rounds; never present it as your own recommendation. The pick card is a kicker convention, not a field: kicker "MY PICK" on your top-ranked grounded candidate, one at most, never in the lead slot. Keep thesis and each fact to one short sentence: the card front shows thesis, identity, and a two-line risk, while first viewport and the case read on the card back behind the Details chip, so long facts cost the reader a flip, not the page its scanability. A card with no imagery at all has no back; its full read renders on the front, so a text-only round loses nothing. Sketch aspect follows the surface: portrait at device viewport for native or mobile-first surfaces, landscape otherwise; the page adapts its cards to either. reroll accepts true or { "registers": ["safer", "bolder"] }: the register buttons steer the next hand along the familiar-to-bold axis, the answer carries "register", and you re-run concept-seed with --register <value> for the next round; offer the registers on direction rounds, and never pre-select one. followup: true keeps the table open after a pick for a second round via --update (direction first, then the execution contract); send the next payload immediately, the page is waiting on it.');
console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board/sketch accept URLs or local paths; sketch slots may point at files that do not exist yet (serve first, generate after; the page polls until they land, so never block serving on generation). hero on a challenger is the inspiration it draws from and renders picture-in-picture beside the sketch, never as the promise of the build. verdict routes rendering: "wins" and "competitive" challengers keep full cards, "declined" ones render demoted after them (narrow, quiet, art as a labeled thumb, "Adopt anyway"), with their kept line on the front; the page reorders declined cards to the end on its own. raised on the assigned card renders each donation as a named raise line. Salience parity: when the assigned card declares no sketch (no image generation this round), catalog art on every card demotes to a labeled thumb, so what looks important is the verdicts call, never rendering luck. canonCard renders the standing exit as a subordinate card with the same anatomy; without it, canon stays a quiet footer action. Include canon only for visual-direction rounds; never present it as your own recommendation. The pick card is a kicker convention, not a field: kicker "MY PICK" on your top-ranked grounded candidate, one at most, never in the lead slot. Every card gets the full anatomy, challengers, canon, and declined included: thesis, palette, materials, viewport, risk; the seed already hands you each challengers system rules, so a card with no palette chips is an authoring gap, not a data gap. Keep thesis and each fact to one short sentence: the card front shows thesis, identity, and a two-line risk, while first viewport and the case read on the card back behind the Details chip, so long facts cost the reader a flip, not the page its scanability. A card with no imagery at all has no back; its full read renders on the front, so a text-only round loses nothing. The sketch slot carries the cards full-fidelity direction comp (the field keeps its wire name for compatibility). Comp aspect follows the surface: portrait at device viewport for native or mobile-first surfaces, landscape otherwise; the page adapts its cards to either. reroll accepts true or { "registers": ["safer", "bolder"] }: the register buttons steer the next hand along the familiar-to-bold axis, the answer carries "register", and you re-run concept-seed with --register <value> for the next round; offer the registers on direction rounds, and never pre-select one. followup: true keeps the table open after a pick for a second round via --update (direction first, then the execution contract); send the next payload immediately, the page is waiting on it.');
process.exit(0);
}
@@ -337,16 +339,19 @@ function loadRound(json) {
sketchSrc: sketchSrc(option.sketch),
});
options = parsed.options.map(decorate);
// The verdict routes rendering: full cards first, declined cards demoted to
// the end of the deck in their own payload order. The reorder happens here
// so a payload that interleaves them still renders the weighing's shape.
// The verdict routes rendering: full cards first, then the canon, then the
// declined cards dead last in their own payload order. The reorder happens
// here so a payload that interleaves them still renders the weighing's
// shape, and the deck reads as a gradient of standing: contenders, the
// familiar door, then the demoted row.
const declined = options.filter((o) => o.verdict === 'declined');
if (declined.length) options = [...options.filter((o) => o.verdict !== 'declined'), ...declined];
options = options.filter((o) => o.verdict !== 'declined');
// The standing exit as a full card: same anatomy, reserved id, rendered
// subordinate by the page. Without it, canon stays the quiet footer action.
if (parsed.canonCard && typeof parsed.canonCard === 'object') {
options = [...options, { ...decorate(parsed.canonCard), id: 'canon', isCanon: true }];
}
options = [...options, ...declined];
}
try { loadRound(raw); } catch (error) { console.error(`serve-question: ${error.message}`); process.exit(1); }
const detachedKey = hasFlag('detached-serve') ? arg('key') : null;
@@ -392,10 +397,21 @@ function page() {
if (idBits.length) rows.push(`<div class="identity">${idBits.join('')}</div>`);
// Donations from declined challengers render as named raise lines: the
// assigned card arrives already raised by the hand it beat, and the raise
// is readable, because a raise nobody can read did not happen.
// is readable, because a raise nobody can read did not happen. One raise
// renders inline; several become a compact cycler (click advances), so a
// generous hand cannot blow the card out of proportion.
if (Array.isArray(option.raised) && option.raised.length) {
const nameOf = (id) => options.find((o) => o.id === id)?.label || String(id ?? '');
rows.push(`<div class="raises">${option.raised.slice(0, 4).map((r) => `<p class="raise"><span class="fact-label">Raised by ${esc(nameOf(r.from))}</span>${esc(r.raise || r.kept || '')}</p>`).join('')}</div>`);
const raiseLines = option.raised.slice(0, 6).map((r) => `<p class="raise"><span class="fact-label">Raised by ${esc(nameOf(r.from))}</span>${esc(r.raise || r.kept || '')}</p>`);
if (raiseLines.length > 1) {
rows.push(`<div class="raises raises-cycle" role="button" tabindex="0" title="Click or press Enter to see the next raise" aria-label="Raised by the hand; activate to see the next raise">
<div class="raises-head"><span class="fact-label">Raised by the hand</span><span class="raises-count" data-raises-count>1/${raiseLines.length}</span></div>
${raiseLines.join('')}
<span class="sr-live" aria-live="polite"></span>
</div>`);
} else {
rows.push(`<div class="raises">${raiseLines[0]}</div>`);
}
}
// Demoted art stays reachable as a labeled thumb: the catalog world
// explains where the direction comes from without buying it back the
@@ -438,7 +454,7 @@ function page() {
if (thumbOnly(option)) return '';
if (faceSketch(option)) {
return `<div class="media sketching" data-sketch="${esc(option.sketchSrc)}">
<div class="shimmer"><span class="sketch-note">sketching&hellip;</span></div>
<div class="shimmer"><span class="sketch-note">rendering&hellip;</span></div>
<img class="sketch" alt="" hidden>
${inspiration}
<div class="chips">${expandChip}${details}</div>
@@ -568,6 +584,10 @@ function page() {
.nav.next { right: auto; left: 50%; top: auto; bottom: 6px; transform: translate(-50%, 0); }
.fade-prev { top: 0; left: 0; right: 0; bottom: auto; width: auto; height: 72px; background: linear-gradient(180deg, var(--ks-lacquer), transparent); }
.fade-next { top: auto; left: 0; right: 0; bottom: 0; width: auto; height: 72px; background: linear-gradient(0deg, var(--ks-lacquer), transparent); }
/* In the vertical deck the cross axis is horizontal: flex-start would
shrink a declined card to content WIDTH, not height, so it stretches
like every other card and its height is already its own. */
.grid > .card.declined { align-self: stretch; }
}
.card { position: relative; perspective: 1400px; transform: rotate(var(--fan, 0deg)); transition: transform .25s cubic-bezier(.16, 1, .3, 1); }
.card:hover { transform: rotate(0deg) translateY(-4px); }
@@ -660,7 +680,10 @@ function page() {
and quieter, its catalog art rides as a labeled thumb in the body, and
the action reads "Adopt anyway". Adoptable, never deleted: the demoted
row is the hand's proof of judgment. */
.grid > .card.declined { flex: 0 0 clamp(15rem, 21vw, 21rem); }
/* Narrow AND short: without align-self the stretch default drags a thin
declined card to the tallest contender's height, a strange stilt of a
card beside the full hand. */
.grid > .card.declined { flex: 0 0 clamp(15rem, 21vw, 21rem); align-self: flex-start; }
.card.declined .face { background: var(--ks-graphite); }
.card.declined:hover .face { border-color: var(--ks-text-faint); }
.card.declined h2 { font-size: 1rem; color: var(--ks-text); }
@@ -677,6 +700,18 @@ function page() {
.raises { display: flex; flex-direction: column; gap: 4px; margin: 2px 0; }
.raise { font-size: .78rem; color: var(--ks-text-muted); line-height: 1.45; border-left: 2px solid var(--ks-patina); padding-left: 8px; }
.raise .fact-label { color: var(--ks-patina); }
/* Several raises cycle instead of stacking: one visible at a time, a
counter for the rest, the whole block advances on click. */
.raises-cycle { cursor: pointer; border-radius: 6px; }
.raises-cycle .raise { display: none; border-left: none; padding-left: 0; }
.raises-cycle .raise.active { display: block; }
.raises-cycle { border-left: 2px solid var(--ks-patina); padding-left: 8px; }
.raises-head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
.raises-head .fact-label { color: var(--ks-patina); }
.raises-count { font-family: var(--ks-mono); font-size: .58rem; letter-spacing: .14em; color: var(--ks-text-faint); }
.raises-count::after { content: " \\203A"; }
.raises-cycle:hover .raises-count { color: var(--ks-patina); }
.sr-live { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
/* The standing exit as a card: present with full anatomy, never dressed as a
contender. Graphite instead of kinpaku, and it never takes the lead ring. */
.card.canon .face { border-color: var(--ks-rule); background: var(--ks-graphite); }
@@ -776,6 +811,25 @@ function page() {
b.closest('.card').classList.toggle('flipped');
}));
// Raise cycler: click (or Enter) advances to the next donation.
document.querySelectorAll('.raises-cycle').forEach(cycle => {
const raises = [...cycle.querySelectorAll('.raise')];
const count = cycle.querySelector('[data-raises-count]');
let at = 0;
const live = cycle.querySelector('.sr-live');
const show = (announce) => {
raises.forEach((raise, i) => raise.classList.toggle('active', i === at));
if (count) count.textContent = (at + 1) + '/' + raises.length;
// Screen readers hear the raise they just advanced to; the initial
// render stays quiet so page load does not narrate every card.
if (announce && live) live.textContent = 'Raise ' + (at + 1) + ' of ' + raises.length + ': ' + (raises[at]?.textContent || '');
};
show(false);
const advance = (e) => { e.stopPropagation(); at = (at + 1) % raises.length; show(true); };
cycle.addEventListener('click', advance);
cycle.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); advance(e); } });
});
// Deal from the stack: cards begin piled at the grid's center, blurred,
// then travel to their seats with a stagger.
const cards = [...document.querySelectorAll('.card')];
@@ -823,7 +877,7 @@ function page() {
const note = m.querySelector('.sketch-note');
const started = Date.now();
// A live elapsed count is the difference between "working" and "frozen".
const tick = setInterval(() => { if (note) note.textContent = 'sketching · ' + Math.round((Date.now() - started) / 1000) + 's'; }, 1000);
const tick = setInterval(() => { if (note) note.textContent = 'rendering · ' + Math.round((Date.now() - started) / 1000) + 's'; }, 1000);
const settle = () => { clearInterval(tick); m.classList.remove('sketching', 'stand-in'); m.querySelector('.shimmer')?.remove(); m.querySelector('.stand-in-label')?.remove(); };
const standIn = () => {
const pip = m.querySelector('.pip img');
@@ -834,7 +888,7 @@ function page() {
clearInterval(tick);
const label = document.createElement('p');
label.className = 'stand-in-label';
label.textContent = 'inspiration · sketch pending';
label.textContent = 'inspiration · comp pending';
m.appendChild(label);
};
const tryLoad = () => {