${surface.examples.length ? chips(surface.examples) : ''}`)
@@ -318,15 +359,38 @@ function buildBrand(s, name) {
const brand = s.context?.brand || {};
const interview = s.context?.interview || {};
const parts = [heading(3, 'Brand', 'Identity, voice, references, taste boundaries.', name)];
+ /* Personality: the confirmed sentence when the agent passed it; the three
+ words alone over the pointer to the durable copy when only they arrived;
+ the plain pointer otherwise. */
parts.push(block('Personality', brand.personality
? callout(brand.words?.join(' · ') || 'Voice', escapeHtml(brand.personality), true)
- : fromChat('Three words, voice, and tone were confirmed', 'PRODUCT.md · Brand Personality')));
+ : (Array.isArray(brand.words) && brand.words.length
+ ? callout(brand.words.join(' · '), 'Three words, voice, and tone were confirmed in chat, before the browser questionnaire. PRODUCT.md · Brand Personality is the durable copy.', true)
+ : fromChat('Three words, voice, and tone were confirmed', 'PRODUCT.md · Brand Personality'))));
+ if (Array.isArray(brand.commitments) && brand.commitments.length) {
+ parts.push(block('Commitments', list(brand.commitments.map(escapeHtml))));
+ }
if (Array.isArray(interview.references) && interview.references.length) {
- parts.push(block('Named references', chips(interview.references)
+ /* Q4 references arrive as plain strings from old cues.json files and as
+ { name, takeaway } objects from new ones; a mixed list renders each
+ entry in its own form. Strings stay the bare pills they were. */
+ const cards = interview.references.filter((ref) => ref && typeof ref === 'object' && ref.name);
+ const plain = interview.references.filter((ref) => typeof ref === 'string');
+ const inner = (cards.length ? `
${cards.map((ref) => `
+
+
${escapeHtml(ref.name)}
${ref.takeaway ? `
+
${escapeHtml(ref.takeaway)}
` : ''}
+ `).join('')}
` : '') + (plain.length ? chips(plain) : '');
+ parts.push(block('Named references', inner
+ note('Q4 of the seed interview: brands, products, printed objects — not adjectives.')));
}
if (interview.antiReference) {
- parts.push(block('Anti-reference', callout('Not this', escapeHtml(interview.antiReference))
+ /* Q5 stays a bare string in old files and gains a why as { name, why }. */
+ const anti = typeof interview.antiReference === 'object'
+ ? callout('Not this', `${escapeHtml(interview.antiReference.name || '')}${
+ interview.antiReference.why ? ` · ${escapeHtml(interview.antiReference.why)}` : ''}`)
+ : callout('Not this', escapeHtml(interview.antiReference));
+ parts.push(block('Anti-reference', anti
+ note('Q5 of the seed interview. A hard constraint on every palette and pair that followed.')));
}
if (Array.isArray(s.context?.assets) && s.context.assets.length) {
diff --git a/picker/scripts/palette-picker.js b/picker/scripts/palette-picker.js
index 013dce5d7..cca09325f 100644
--- a/picker/scripts/palette-picker.js
+++ b/picker/scripts/palette-picker.js
@@ -1186,23 +1186,29 @@ function setActiveRole(role) {
}
/* The contrast alert: a danger badge on the neutral swatch, with its
- explanation in the sibling tooltip. Re-read from renderPreview() on
- every render, so every path that can change which color sits in the
- neutral slot (inputs, tints, rings, card switch, reset, reorder)
- lands here. One block-level line per failed check, mid-tone first. */
+ explanation in the sibling tooltip. Both strips carry one. This strip's
+ badge is re-read from renderPreview() on every render, so every path
+ that can change which color sits in the neutral slot (inputs, tints,
+ rings, card switch, reset, reorder) lands here; the strategy strip's
+ badge is re-read by paintStrategyBands() against the committed fields
+ that strip is painted from. One block-level line per failed check,
+ mid-tone first. */
const alertBadge = $('[data-contrast-alert]', panel);
const alertTip = $('[data-contrast-tip]', panel);
-function syncNeutralAlert() {
- const issues = cards.length ? neutralContrastIssue(state().colors) : [];
- alertBadge.hidden = issues.length === 0;
- alertTip.replaceChildren(...issues.map((text) => {
+function paintContrastAlert(badge, tip, issues) {
+ badge.hidden = issues.length === 0;
+ tip.replaceChildren(...issues.map((text) => {
const line = document.createElement('span');
line.textContent = text;
return line;
}));
}
+function syncNeutralAlert() {
+ paintContrastAlert(alertBadge, alertTip, cards.length ? neutralContrastIssue(state().colors) : []);
+}
+
function setColor(role, hex, detached = true) {
const itemState = state();
itemState.colors[role] = hex.toUpperCase();
@@ -1526,7 +1532,14 @@ function recommitPalette() {
/* The strategy screen's band is a reading of the committed palette rather than
of the deck, which is also what makes it correct after a reorder on either
- screen: both end in the fields this paints from. */
+ screen: both end in the fields this paints from. Its neutral carries the
+ same contrast alert the editable strip does, judged against the same
+ committed colors this strip is painted from, so a reorder made here that
+ drags a mid-tone into the neutral slot is reported here, on the swatch
+ the visitor just dropped. */
+const strategyAlertBadge = $('[data-contrast-alert]', strategyBands);
+const strategyAlertTip = $('[data-contrast-tip]', strategyBands);
+
function paintStrategyBands() {
const committed = roleMap((role) => $(`[name="palette-${role}"]`).value);
if (Object.values(committed).some((hex) => !hex)) return;
@@ -1536,6 +1549,7 @@ function paintStrategyBands() {
band.style.setProperty('--band-ink', contrastInk(committed[role]));
$('output', band).textContent = committed[role];
}
+ paintContrastAlert(strategyAlertBadge, strategyAlertTip, neutralContrastIssue(committed));
}
/* Everything screen 03 spends on something other than the answer: the block's
diff --git a/picker/styles/design-context.css b/picker/styles/design-context.css
index 450870675..5b7cf19ce 100644
--- a/picker/styles/design-context.css
+++ b/picker/styles/design-context.css
@@ -679,6 +679,90 @@ body.dcx-open { overflow: hidden; background: linear-gradient(180deg, var(--ks-l
.dcx-callout--accent { border-color: var(--accent-line); background: var(--accent-wash); }
+/* ============================================================
+ Context enrichment: the success line, paired callouts, the
+ needs/trust columns, the platform pill, reference cards, and
+ the operating-context prose. Each rule exists for a block
+ that renders only when the agent passed the matching context
+ field; every value is a dcx or ks token already in this file.
+ ============================================================ */
+.dcx-callout p.dcx-callout-success {
+ margin-top: 10px;
+ padding-top: 10px;
+ border-top: 1px solid var(--ks-rule);
+}
+
+/* Two callouts sharing a row: the emotional journey's arrival and
+ leaving beats, and positioning's not-this / this cells. */
+.dcx-callout-pair {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr));
+ gap: 12px;
+}
+
+/* Two sibling blocks sharing a row (needs beside trust triggers). The
+ children stay real .dcx-block elements so the subnav still finds them. */
+.dcx-cols {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(min(320px, 100%), 1fr));
+ gap: 0 clamp(24px, 4vw, 48px);
+ align-items: start;
+}
+
+/* The purpose callout with the platform pill at its shoulder. */
+.dcx-purpose {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) auto;
+ gap: 14px;
+ align-items: start;
+}
+
+.dcx-platform-pill {
+ font-family: var(--ks-mono);
+ font-size: 0.7rem;
+ letter-spacing: 0.16em;
+ text-transform: uppercase;
+}
+
+/* Named references as cards, the .dcx-pick anatomy with a takeaway. */
+.dcx-ref-cards {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr));
+ gap: clamp(14px, 2vw, 22px);
+}
+
+.dcx-ref-cards + .dcx-chips { margin-top: 12px; }
+
+.dcx-ref-card {
+ border: 1px solid var(--ks-rule);
+ background: var(--panel-bg);
+ padding: clamp(18px, 2.5vw, 26px);
+}
+
+.dcx-ref-name {
+ margin: 0 0 8px;
+ font-family: var(--ks-font-display);
+ font-weight: 300;
+ font-size: 1.3rem;
+ color: var(--ks-text);
+}
+
+.dcx-ref-takeaway {
+ margin: 0;
+ color: var(--ks-text-muted);
+ font-size: 0.92rem;
+ line-height: 1.6;
+}
+
+/* Operating context: one paragraph at reading measure. */
+.dcx-prose {
+ margin: 0;
+ max-width: 62ch;
+ color: var(--ks-text-muted);
+ font-size: 0.96rem;
+ line-height: 1.6;
+}
+
/* Numbered principle list. */
.dcx-principles {
list-style: none;
diff --git a/picker/styles/picker.css b/picker/styles/picker.css
index 353415ae9..139b3284c 100644
--- a/picker/styles/picker.css
+++ b/picker/styles/picker.css
@@ -1235,6 +1235,26 @@ body.picker-page {
height: 19px;
}
+/* The compact band's contrast alert. The editable band parks the badge in
+ its free lower right corner; here that corner belongs to the hex readout,
+ so the badge mirrors the grip in the free upper right one. It ducks under
+ the grip (z-index 1 against the grip's 2) so the transient "Drag to
+ reorder" tip, which reads to the right of the grip, is not punched
+ through by the glyph while it shows. The explanation floats above the
+ strip: inside it the short band could not clear the badge, and the strip
+ is its block's last row, so the room is above. */
+.picker-bands--compact .picker-band-alert {
+ top: 3px;
+ right: 3px;
+ bottom: auto;
+ z-index: 1;
+}
+
+.picker-bands--compact .picker-band-alert-tip {
+ right: 3px;
+ bottom: calc(100% + 9px);
+}
+
.picker-bands--compact .picker-band-foot {
height: 30px;
}
diff --git a/skill/reference/visual-cues.md b/skill/reference/visual-cues.md
index aad37d135..7c6636ab0 100644
--- a/skill/reference/visual-cues.md
+++ b/skill/reference/visual-cues.md
@@ -457,20 +457,42 @@ In the same write, add a top-level `context` object carrying the chat half of th
```json
"context": {
- "product": { "name": "[product name]", "purpose": "[one-sentence purpose from PRODUCT.md]" },
- "audience": { "primary": "[who]", "secondary": "[who]", "emotion": "[emotional goal on landing]", "needs": ["[need]"] },
- "brand": { "words": ["[word]"], "personality": "[one sentence from PRODUCT.md Brand Personality]" },
+ "product": {
+ "name": "[product name]",
+ "purpose": "[one-sentence purpose from PRODUCT.md]",
+ "success": "[the success definition from PRODUCT.md Product Purpose, one line]",
+ "platform": "[bare value from PRODUCT.md Platform: web, ios, android, or adaptive]",
+ "positioning": { "not": "[what it is not, from PRODUCT.md Positioning]", "this": "[what it is instead]" },
+ "clarities": ["[one line per item of PRODUCT.md's what-must-be-clear-first list]"],
+ "operatingContext": "[one line from PRODUCT.md Operating Context]"
+ },
+ "audience": {
+ "primary": "[who]", "secondary": "[who]",
+ "emotion": "[emotional goal on landing]",
+ "leaving": "[what they should leave with, from the purpose and success definition]",
+ "needs": ["[need]"],
+ "trust": ["[trust trigger, from PRODUCT.md Evidence on Hand and Users]"],
+ "inclusion": ["[who must not be excluded, from PRODUCT.md Accessibility and Inclusion]"]
+ },
+ "brand": {
+ "words": ["[word]"],
+ "personality": "[one sentence from PRODUCT.md Brand Personality]",
+ "commitments": ["[one line per commitment from PRODUCT.md Brand Commitments]"]
+ },
"assets": ["[asset name: what Step 2 read off it]"],
"interview": {
"colorStrategy": "[Q1 pick]", "hueAnchor": "[Q1 anchor]",
"typeDirection": "[Q2 pick]", "motionEnergy": "[Q3 pick]",
- "references": ["[Q4, all three]"], "antiReference": "[Q5]"
+ "references": [{ "name": "[Q4 reference, one entry per name]", "takeaway": "[one clause: what this reference lends the design]" }],
+ "antiReference": { "name": "[Q5]", "why": "[one clause: why this is the wrong direction]" }
}
}
```
Quote the user's answers, not paraphrases of them; the document labels interview fields as the questions they answered. A missing block renders as a pointer to where that truth lives (PRODUCT.md), so an old `cues.json` without `context` still produces a complete document.
+The optionality is field by field, and the document omits the block of any field that does not arrive, so fill a field only when its PRODUCT.md section or interview answer exists. A legacy PRODUCT.md without Positioning, Platform, Operating Context, or Brand Commitments yields a context without those fields, never an invented value. `product.clarities` carries PRODUCT.md's "What must be clear first" list under a shorter key. `interview.references` and `interview.antiReference` also accept their older shapes, plain strings, which render as the bare pills and single-name callout they always did.
+
Five of the questions are then answered per surface rather than once for the whole run, because the answer that suits a marketing page rarely suits the tool it sells: `color-strategy`, `motion-energy` (how much movement there is), `boundary-style` (how sections are separated), `corner-style` (how round shapes are), and `depth-style` (how far off the page things sit). Each of the five comes back twice over. The bare key holds the leading surface's answer, which is the first chosen tile in tile order and the one every later screen previews. Alongside it is one `-` key for every surface chosen, `` being `persuade`, `operate`, `read`, or `experience`. Surfaces the user never opened are included too, holding the default for their kind; a surface nobody chose returns nothing at all.
`motion-energy` is the one exception to that shape, because the question is only put to two of the four surfaces. A landing page and a portfolio are watched, so how much they move is a house decision; a tool and a document are worked in, and their movement follows the interface. So the motion keys cover the chosen surfaces among `persuade` and `experience` only, and the bare key holds the first of those two in tile order rather than the run's leading surface: on an app UI plus portfolio run, `motion-energy` is the portfolio's answer. **When a run chooses neither of those surfaces the question is never asked, and no `motion-energy` key comes back at all.** Read it as absent rather than defaulted, and say nothing about movement in DESIGN.md; a default written as a decision is a decision the user never made.