From d89ee5f87c3a843d075b3f21ea8ba77e272b9bd1 Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Mon, 27 Jul 2026 17:03:05 -0700 Subject: [PATCH] Deal every card the same hand: anatomy, sketches, and the standing door The decision page compared unlike things: the grounded direction was a wall of text beside curated catalog art, the catalog art read as a promise of the build, the weighing silently shrank the challenger set, and the standing exit hid in the footer under the cards it must not soften. Every card now shares one anatomy (thesis, palette chips, material tags, first viewport, case, risk), every dealt challenger is presented with the weighing written on it rather than applied to it, the catalog image rides picture-in-picture as labeled inspiration with the lightbox a click away, and canonCard renders the category standard as one honest, subordinate card. When image generation exists, each card declares a sketch slot the page polls: serve first, generate after, through one shared deliberately unfinished frame, so the comparison stays about direction instead of rendering luck. The asset producer takes the batch when subagents exist; the chosen sketch returns in ANSWER to seed at most one comp probe, and the comp round still renders its full set. Co-Authored-By: Claude Fable 5 --- skill/agents/impeccable-asset-producer.md | 4 + skill/reference/new-work.md | 6 +- skill/reference/visualize.md | 2 +- skill/scripts/serve-question.mjs | 200 ++++++++++++++++++---- tests/serve-question.test.mjs | 54 ++++++ 5 files changed, 233 insertions(+), 33 deletions(-) diff --git a/skill/agents/impeccable-asset-producer.md b/skill/agents/impeccable-asset-producer.md index 251abf018..ac2101ed6 100644 --- a/skill/agents/impeccable-asset-producer.md +++ b/skill/agents/impeccable-asset-producer.md @@ -22,6 +22,10 @@ Your job is production cleanup, not new art direction. Work only from the approv Do not redesign. Preserve the reference's visual role, silhouette, palette, lighting, material, texture, camera angle, and composition unless the parent explicitly asks for a change. Preserve perspective only when it belongs to the object or scene itself; if CSS should create the card transform, shadow, rounded clipping, border, or layout, remove that presentation chrome from the raster. +## Decision Sketch Batches + +When the parent hands you a decision payload instead of an approved mock, the job is one sketch per card, the canon card included, written to each card's declared `sketch` path while the decision page waits. Work from each card's structured fields and PRODUCT.md alone; the card is the packet, and a card too thin to brief a sketch is reported back, not padded from imagination. Render the whole set through the parent's shared frame: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across the set. Uniformity outranks per-card polish; a sketch that looks more finished than its siblings has broken the comparison the page exists to hold. Write each file the moment it is done so the page reveals sketches as they land, and return a one-line manifest per sketch instead of the full production manifest. + ## Input Contract Expect: diff --git a/skill/reference/new-work.md b/skill/reference/new-work.md index 18ef23098..dbb86f705 100644 --- a/skill/reference/new-work.md +++ b/skill/reference/new-work.md @@ -44,9 +44,11 @@ The script assigns which structure gets built: your top-ranked structure is what 2. From that cultural world, list seven concrete visual systems, artifacts, places, or rituals the audience knows by heart, each with one line on why it resonates and can carry the mechanism, ordered by resonance. The audience's world includes its graphic and screen traditions, the notation, publications, identity programs, data graphics, and interfaces it reads daily, not only its physical objects; a nameable abstract system (a school of poster, a documentation standard, a data-graphic tradition) is as concrete a candidate as any artifact. What would this thing look like as a physical object; what did its world look like before the web? Near-duplicates count once. When more than three of the seven share one material family, the derivation stopped at the subject's most obvious artifact; the audience's world is larger than that, so dig until the list spans at least three families. 3. Turn that material into complete directions: each joins a reusable visual world to a concrete first-surface experience. 4. Run `node {{scripts_path}}/concept-seed.mjs --scope direction --mode ` and follow what it prints. The script assigns which direction gets built and deals catalog challengers. Fuse each challenger before judging it: the challenger supplies the form and its system grammar, the product supplies every fact, and clarity wins conflicts. Weigh fused challengers against the assigned direction on exactly two axes, audience identification and product clarity; losing to strong grounded material is a valid outcome, and beating a thin or tool-monoculture list is the point. -5. Present one direction, fully committed: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, offer the one or two fused challengers that survived the weighing as named alternates with a one-line case each, plus re-roll with an optional one-line steer. What you never present is a ranked menu of your own grounded candidates; a lineup of those invites the safest card, while dealt challengers carry no such rut. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list also carries the standing exit as its last option. +5. Present one direction, fully committed: its world, first viewport, visitor path, signature interaction, cross-surface reach, and honest risk. Alongside it, offer every dealt challenger as a named alternate, the weighing's verdict written on each as its one-line case, an honest "fuses poorly because X" included; the weighing informs the user's choice, it never pre-empts it, and dropping a challenger from the presentation takes a named product-truth failure, disclosed. Add re-roll with an optional one-line steer. What you never present is a ranked menu of your own grounded candidates; a lineup of those invites the safest card, while dealt challengers carry no such rut. The two channels share this structure and differ only in richness: cards and boards on the decision page, names and one-liners through the structured tool; the structured tool's option list also carries the standing exit as its last option. -The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading, the dealt challengers as alternates carrying their QUALITY BAR cards, and re-roll, steer, plus canon enabled; a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Run `node {{scripts_path}}/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. +The standing exit: every direction round offers one quiet, permanent alternative, the category standard, played straight. It is the user's door, never yours: never recommend it, never weigh it against the roll, never let it soften the dealt directions; the counterweights bind the unchosen default, not the chosen one. When the user takes it, in the canon action, a safer-steer, or plain words asking for the familiar or competitor-like path, convention becomes the commitment: ask once for two or three products this should sit alongside, make their craft level the bar, and execute the canon at full fidelity, without irony or smuggled quirk. A standing preference gets recorded as a brand commitment in PRODUCT.md. Re-roll eliminates every direction already shown, grounded and challenger alike; after two consecutive re-rolls, ask what quality is missing. You may re-roll on your own only on named factual grounds, when the assigned direction cannot carry the product's truth or task; taste is never grounds. The user may re-roll freely, and a user- or brief-pinned direction beats the roll, always. Present the decision visually: write an options payload with the assigned direction leading, the dealt challengers as alternates carrying their QUALITY BAR cards, and re-roll, steer, plus canon enabled; a degraded roll with no challengers still uses the page, as a single text-only card with re-roll. Give every card the same anatomy, thesis, palette, materials, first viewport, honest risk, and the challengers' case lines (run the script with `--schema` for the exact shape); the page renders identity from these fields, so a grounded direction is never a wall of text against rendered art, and a challenger's catalog image rides as labeled inspiration, never as the promise of the build. Author `canonCard` too: the category standard as one honest card with the same anatomy; the page keeps it subordinate, and the counterweights still bind you, so it is never yours to recommend. Run `node {{scripts_path}}/serve-question.mjs --start --payload ` (run it with `--schema` first for the exact payload shape). It daemonizes, prints the page URL and a key, and exits immediately; now open that URL for the user, in-app browser first, then the system opener, then showing the URL. Collect the choice with `--wait --key `, repeating while it exits 3; the ANSWER prints as JSON. Exit 4 means the page was closed without an answer: re-present once through the structured question tool, and with no answer there either, proceed unattended with the assigned direction and state the assumptions. A harness that can leave a shell blocked in the background may instead run the script without `--start` and let it auto-open and block. Only a session where no browser can open at all, headless, CI, an eval worker, a remote shell with no display, puts the same decision through the structured question tool instead; the script self-detects these environments and exits 2 with that advice, so treat exit 2 as this fallback, never as an error to retry. + +When image generation exists, every card also declares a `sketch` path under `.impeccable/sketches/`, the canon card included. Serve the page first, then produce the sketches; the page shimmer-waits per slot and the user may answer before they land, so generation never delays the question. Render every sketch through one shared frame so the comparison stays about direction, never rendering luck: the requested surface's first viewport as a flat, matte design sketch in that card's own palette and type character, deliberately unfinished, no photorealism, no gloss, identical framing across cards; a candidate whose sketch looks more finished than the others has broken the comparison, not won it. When the harness runs subagents, spawn the shipped asset producer with the payload file and PRODUCT.md, one pass for the whole set; otherwise generate inline after serving. A sketch answers which world, never which composition: the comp round still renders its full set, and the chosen card's sketch seeds at most one probe. With no image generation, the cards carry their identity in palette chips and facts, and that page is complete, not a lesser version. Catalog worlds are working systems, not mood references. When one survives, carry its palette and material, type and composition, topology, controls and state, and responsive rules into the product. When the source is itself an interface language, commit to its native grammar across navigation, content, controls, and states. Open the QUALITY BAR board and hero for the world you build the moment the choice lands, even if you viewed another card earlier; the ANSWER line names the chosen card's images for exactly this reason (when the harness only reads files or runs sandboxed, download them into the workspace and open the relative path; sandboxed viewers reject absolute paths outside it). They set the craft level the build must reach, a rendered reference's finish, commitment, and art direction, never the composition; your surface serves this product. diff --git a/skill/reference/visualize.md b/skill/reference/visualize.md index 7c49021e8..7c23fc525 100644 --- a/skill/reference/visualize.md +++ b/skill/reference/visualize.md @@ -6,7 +6,7 @@ The purpose of a probe is to test composition, narrative, hierarchy, density, fo ## Generate three compositional options -Render three distinct high-fidelity north-star comps of the requested surface, with whatever generation capability exists, saved under `.impeccable/mocks/` so they survive the session; record the approved comp's path in the surface brief. Base them on the real content and the surface concepts already developed with the user. Three is the number: one comp invites rubber-stamping, and the spread between three is what surfaces the composition worth building. +Render three distinct high-fidelity north-star comps of the requested surface, with whatever generation capability exists, saved under `.impeccable/mocks/` so they survive the session; record the approved comp's path in the surface brief. Base them on the real content and the surface concepts already developed with the user. Three is the number: one comp invites rubber-stamping, and the spread between three is what surfaces the composition worth building. A decision-page sketch is not a probe: it chose the direction at deliberately unfinished fidelity, so the three comps render regardless, and the chosen card's sketch seeds at most one of them. - When the user shortlisted multiple concepts, spread the three across them. - When one direction is committed, vary the structural uncertainty an image can resolve: topology, sequence, density, hierarchy, focal composition, or interaction framing. diff --git a/skill/scripts/serve-question.mjs b/skill/scripts/serve-question.mjs index 88f821d03..e55fad065 100644 --- a/skill/scripts/serve-question.mjs +++ b/skill/scripts/serve-question.mjs @@ -24,21 +24,36 @@ * "label": "Fillmore Handbill", * "kicker": "THE ROLL", // optional badge; the assigned option leads * "lineage": "1966-71 Fillmore ...", // optional - * "body": "why it fits, first viewport, risk ...", // optional, plain text - * "hero": "https://... or /abs/path.webp", // optional image + * "thesis": "one line: the idea this direction owns", // optional + * "palette": ["#1a2f5e", "oklch(84% .19 80)", ...], // optional, rendered as chips + * "materials": ["letterpress", "newsprint"], // optional, rendered as tags + * "viewport": "one line: the first-viewport composition", // optional + * "case": "one line: the fusion verdict, honest", // optional + * "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 + * "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 * }, ... * ], * "reroll": true, // adds a re-roll action (returns {"optionId":"reroll"}) - * "canon": true, // adds the quiet "Play it straight" standing exit - * // (returns {"optionId":"canon"}); direction rounds only + * "canon": true, // adds the "Play it straight" standing exit; + * // direction rounds only (returns {"optionId":"canon"}) + * "canonCard": { ... }, // optional: the standing exit as a full card with the + * // same anatomy (label, thesis, palette, sketch, ...); + * // rendered last and visually subordinate. Without it, + * // canon stays a quiet footer action. * "steer": true // adds a free-text steer field returned with any answer * } * - * Options render as large cards: hero render first when present (the dealt - * catalog worlds already have cards; grounded directions may present text-only - * or a freshly generated mock). Local image paths are served by this server; - * nothing is uploaded anywhere. + * Options render as large cards: the sketch leads when present, with the + * inspiration image picture-in-picture; a hero alone renders full-bleed; a + * text-only direction gets its identity from the palette chips and tags. + * Local image paths are served by this server; nothing is uploaded anywhere. * * Modes: * (default) block until answered; ANSWER on stdout; exit 0. @@ -107,6 +122,9 @@ function printAnswer(raw) { if (a.hero || a.board) { 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.'); + } 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.'); } @@ -125,14 +143,15 @@ 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', body: 'Why it fits, the first viewport, the honest risk.', hero: 'https://impeccable.style/worlds/cards/fillmore-handbill-hero.webp', board: 'https://impeccable.style/worlds/cards/fillmore-handbill.webp' }, - { id: 'challenger-teletext', label: 'Teletext Service', lineage: 'broadcast teletext magazines', body: 'Fused alternate.', hero: 'https://impeccable.style/worlds/cards/broadcast-programming-teletext-service-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.', sketch: '.impeccable/sketches/assigned.webp', hero: 'https://impeccable.style/worlds/cards/fillmore-handbill-hero.webp', board: 'https://impeccable.style/worlds/cards/fillmore-handbill.webp' }, + { id: 'challenger-teletext', label: 'Teletext Service', lineage: 'broadcast teletext magazines', thesis: 'The catalog as a broadcast index: pages, not sections.', case: 'Fuses cleanly: releases map to numbered pages.', sketch: '.impeccable/sketches/challenger-teletext.webp', hero: 'https://impeccable.style/worlds/cards/broadcast-programming-teletext-service-hero.webp' }, ], reroll: true, 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' }, steer: true, }, null, 2)); - console.log('\nOption ids return verbatim in ANSWER; "reroll" and "canon" are reserved. hero/board accept URLs or local paths. canon adds a quiet standing "Play it straight" action for direction decisions: the user\'s explicit door to the category standard. Include it only for visual-direction rounds; never present canon as your own recommendation.'); + 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. 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.'); process.exit(0); } @@ -244,12 +263,28 @@ function loadRound(json) { localImages.push(abs); return `/img/${localImages.length - 1}`; }; + // Sketches stream in after the page is served, so their slots register + // whether or not the file exists yet; /img answers 404 until it lands and + // the page polls the slot. Remote sketch URLs pass through untouched. + const sketchSrc = (value) => { + if (!value) return null; + if (/^https?:\/\//.test(value)) return value; + localImages.push(path.resolve(value)); + return `/img/${localImages.length - 1}`; + }; payload = parsed; - options = parsed.options.map((option) => ({ + const decorate = (option) => ({ ...option, heroSrc: imageSrc(option.hero), boardSrc: imageSrc(option.board), - })); + sketchSrc: sketchSrc(option.sketch), + }); + options = parsed.options.map(decorate); + // 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 }]; + } } try { loadRound(raw); } catch (error) { console.error(`serve-question: ${error.message}`); process.exit(1); } const detachedKey = hasFlag('detached-serve') ? arg('key') : null; @@ -260,20 +295,61 @@ const esc = (s) => String(s ?? '').replace(/[&<>"]/g, (c) => ({ '&': '&', '< function page() { const flipChip = (label) => ``; const expandChip = ``; - const cards = options.map((option, index) => ` -
-
-
- ${option.kicker ? `${esc(option.kicker)}` : ''} - ${option.heroSrc || option.boardSrc ? `
+ // Structured anatomy: chips and one-line facts render when the payload + // carries them; a plain body falls back to the prose block. Palette chips + // and material tags give a text-only direction an immediate identity that + // no generation luck can distort. + const anatomy = (option) => { + const rows = []; + if (option.thesis) rows.push(`

${esc(option.thesis)}

`); + const idBits = []; + if (Array.isArray(option.palette) && option.palette.length) { + idBits.push(`${option.palette.slice(0, 6).map((c) => ``).join('')}`); + } + if (Array.isArray(option.materials) && option.materials.length) { + idBits.push(option.materials.slice(0, 4).map((m) => `${esc(m)}`).join('')); + } + if (idBits.length) rows.push(`
${idBits.join('')}
`); + const fact = (label, value) => value ? `

${label}${esc(value)}

` : ''; + rows.push(fact('First viewport', option.viewport)); + rows.push(fact('The case', option.case)); + rows.push(fact('Risk', option.risk)); + if (!option.thesis && option.body) rows.push(`

${esc(option.body)}

`); + else if (option.body && option.thesis) rows.push(`

${esc(option.body)}

`); + return rows.join('\n '); + }; + const media = (option) => { + const inspiration = option.heroSrc ? `
+ +
inspiration
+
` : ''; + if (option.sketchSrc) { + return `
+
sketching…
+ + ${inspiration} +
${expandChip}${option.boardSrc ? flipChip('Board') : ''}
+
`; + } + if (option.heroSrc || option.boardSrc) { + return `
${expandChip}${option.boardSrc && option.heroSrc ? flipChip('Board') : ''}
-
` : ''} +
`; + } + return ''; + }; + const cards = options.map((option, index) => ` +
+
+
+ ${option.kicker ? `${esc(option.kicker)}` : option.isCanon ? 'The standing door' : ''} + ${media(option)}
${option.lineage ? `

${esc(option.lineage)}

` : ''}

${esc(option.label)}

- ${option.body ? `

${esc(option.body)}

` : ''} - + ${anatomy(option)} +
${option.boardSrc && option.heroSrc ? `
@@ -367,6 +443,31 @@ function page() { .tier { font-family: var(--ks-mono); font-size: .625rem; letter-spacing: .24em; text-transform: uppercase; color: var(--ks-text-faint); } h2 { font-family: var(--ks-font); font-size: 1.125rem; font-weight: 500; line-height: 1.35; color: var(--ks-champagne); } .detail { color: var(--ks-text-muted); font-size: .88rem; white-space: pre-wrap; } + .detail.more { font-size: .8rem; color: var(--ks-text-faint); } + .thesis { color: var(--ks-text); font-size: .95rem; line-height: 1.45; } + .identity { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; margin: 2px 0; } + .swatches { display: inline-flex; gap: 4px; margin-right: 4px; } + .swatches i { width: 18px; height: 18px; border-radius: 5px; border: 1px solid oklch(100% 0 0 / 0.18); box-shadow: inset 0 0 0 1px oklch(0% 0 0 / 0.25); } + .tag { font-family: var(--ks-mono); font-size: .6rem; letter-spacing: .14em; text-transform: uppercase; color: var(--ks-text-muted); border: 1px solid var(--ks-rule); border-radius: 4px; padding: 3px 7px; } + .fact { font-size: .8rem; color: var(--ks-text-muted); line-height: 1.45; } + .fact-label { display: inline-block; font-family: var(--ks-mono); font-size: .6rem; letter-spacing: .18em; text-transform: uppercase; color: var(--ks-text-faint); margin-right: .55em; transform: translateY(-1px); } + /* Inspiration rides picture-in-picture: the catalog world explains where the + direction comes from without promising what the build will look like. */ + .pip { position: absolute; z-index: 2; left: 10px; bottom: 10px; margin: 0; width: 74px; border: 1px solid var(--ks-rule); border-radius: 6px; overflow: hidden; background: var(--ks-lacquer); cursor: zoom-in; transform-origin: bottom left; transition: transform .3s cubic-bezier(.16,1,.3,1), box-shadow .3s; box-shadow: 0 6px 18px oklch(0% 0 0 / 0.45); } + .pip img { display: block; width: 100%; aspect-ratio: 16/10; object-fit: cover; } + .pip figcaption { font-family: var(--ks-mono); font-size: .5rem; letter-spacing: .2em; text-transform: uppercase; color: var(--ks-text-faint); text-align: center; padding: 3px 0 4px; } + .pip:hover { transform: scale(2.6); box-shadow: 0 14px 40px oklch(0% 0 0 / 0.6); z-index: 3; } + .sketch-note { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font-family: var(--ks-mono); font-size: .66rem; letter-spacing: .22em; text-transform: uppercase; color: var(--ks-text-faint); } + .media.sketching { position: relative; } + .media.sketching .shimmer { position: absolute; inset: 0; } + .media img.sketch { position: relative; z-index: 1; } + /* 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); } + .card.canon:hover .face { border-color: var(--ks-text-faint); } + .card.canon .kicker.standing { background: transparent; border: 1px solid var(--ks-rule); color: var(--ks-text-faint); } + .card.canon button.choose { background: transparent; color: var(--ks-text); border: 1px solid var(--ks-rule); } + .card.canon button.choose:hover { border-color: var(--ks-text-muted); background: var(--ks-graphite-2); } button.choose { margin-top: auto; align-self: start; background: var(--ks-kinpaku); color: var(--ks-dark-ink); border: 0; font-family: var(--ks-font); font-size: 1rem; font-weight: 500; line-height: 1.35; padding: 10px 38px; border-radius: 6px; cursor: pointer; transition: background .15s; } button.choose:hover { background: var(--ks-kinpaku-pale); } footer { width: 100%; max-width: 90rem; margin: 1.6rem auto 0; display: flex; gap: 1rem; align-items: center; flex-wrap: wrap; } @@ -414,7 +515,7 @@ function page() {
${payload.steer ? '' : ''} ${payload.reroll ? '' : ''} - ${payload.canon ? '' : ''} + ${payload.canon && !payload.canonCard ? '' : ''}