This commit is contained in:
Abdul Wahab
2026-09-01 10:02:39 +05:00
parent e0ddad3198
commit ca4f39e01d
4 changed files with 712 additions and 10 deletions
+188 -5
View File
@@ -39,6 +39,38 @@ const iconRows = iconPacks.packs.map((pack) => ({
meta: `${pack.grid} grid / ${pack.weight} / ${pack.license}`,
}));
// The four kinds of surface a design system can be asked to serve, straight
// from the skill's modes. Values travel as `surface-modes` (multi-select);
// the agent pre-checks what PRODUCT.md suggests via cues.json, and the
// visitor corrects it. Persuade is the markup default so the answer can
// never arrive empty on runs whose cues carry no hint.
const surfaces = [
{
value: 'persuade',
title: 'Persuade',
goal: 'Grab attention and move people to act.',
examples: ['Landing pages', 'Marketing', 'Campaigns', 'Pricing'],
},
{
value: 'operate',
title: 'Operate',
goal: 'Feel familiar so work gets done fast.',
examples: ['App UI', 'Dashboards', 'Admin', 'Settings'],
},
{
value: 'read',
title: 'Read',
goal: 'Get out of the way of understanding.',
examples: ['Docs', 'Articles', 'Guides', 'Changelogs'],
},
{
value: 'experience',
title: 'Experience',
goal: 'Be the work itself.',
examples: ['Portfolios', 'Galleries', 'Showcases'],
},
];
// The five questions that are answered by looking at the same wireframe page.
// Each one only rewrites the artboard slots it owns, so by the last screen the
// page is showing every decision made before it. Motion is the one question
@@ -251,6 +283,157 @@ const questions = [
</div>
</section>
<section class="picker-screen" data-screen="01b" data-step="Surfaces" aria-hidden="true" aria-labelledby="picker-modes-title">
<div class="picker-container">
<div class="picker-modes">
<h2 id="picker-modes-title" class="picker-question-title">What are we designing?</h2>
<fieldset class="picker-modes-grid">
<legend>Surfaces</legend>
<label class="picker-mode-tile">
<input type="checkbox" name="surface-modes" value="persuade" aria-label="Landing pages, marketing, campaigns, and pricing" checked />
<span
class="picker-mode-preview picker-preview"
aria-hidden="true"
style="--pv-primary: #195856; --pv-secondary: #5F7C7A; --pv-tertiary: #3A958B; --pv-neutral: #EDEFEF; --pv-n-ink: var(--pk-ink-dark);"
>
<span class="pv-desktop">
<span class="pv-nav">
<span class="pv-logo"></span>
<span class="pv-nav-bars"><i></i><i></i><i></i></span>
<span class="pv-pill"></span>
</span>
<span class="pv-hero">
<span class="pv-hero-copy">
<span class="pv-eyebrow"></span>
<span class="pv-headline"><i></i><i></i></span>
<span class="pv-copy-bar"></span>
<span class="pv-actions"><i></i><i></i></span>
</span>
<span class="pv-image"></span>
</span>
</span>
<span class="pv-phone">
<span class="pv-phone-top">
<span class="pv-logo"></span>
<span class="pv-avatar"></span>
</span>
<span class="pv-phone-body">
<span class="pv-mini-card"><i></i><span></span></span>
<span class="pv-mini-card"><i></i><span></span></span>
<span class="pv-mini-chart"><i></i><i></i><i></i><i></i><i></i></span>
</span>
<span class="pv-tabbar"><i></i><i></i><i></i><i></i></span>
</span>
</span>
<span class="picker-mode-copy">
<span class="picker-mode-pills"><i>Landing pages</i><i>Marketing</i><i>Campaigns</i><i>Pricing</i></span>
<span class="picker-mode-goal">You're <strong>convincing</strong> someone to act. Lead with proof and a clear next step.</span>
</span>
</label>
<label class="picker-mode-tile">
<input type="checkbox" name="surface-modes" value="operate" aria-label="App UI, dashboards, admin, and settings" />
<span class="picker-mode-preview" aria-hidden="true">
<span class="pm-desktop pm-split">
<span class="pm-side"><i></i><i></i><i></i><i></i></span>
<span class="pm-main">
<span class="pm-cards"><i></i><i></i><i></i></span>
<span class="pm-rows"><i></i><i></i><i></i><i></i></span>
</span>
</span>
<span class="pm-phone">
<span class="pm-phone-top"><i class="pm-dot"></i><i class="pm-bar pm-faint" style="--w:30%"></i></span>
<span class="pm-phone-body">
<span class="pm-cards pm-cards-2"><i></i><i></i></span>
<span class="pm-rows"><i></i><i></i><i></i></span>
</span>
<span class="pm-tabbar"><i></i><i></i><i></i><i></i></span>
</span>
</span>
<span class="picker-mode-copy">
<span class="picker-mode-pills"><i>App UI</i><i>Dashboards</i><i>Admin</i><i>Settings</i></span>
<span class="picker-mode-goal"><strong>Familiarity</strong> comes first. Keep paths obvious and the layout predictable.</span>
</span>
</label>
<label class="picker-mode-tile">
<input type="checkbox" name="surface-modes" value="read" aria-label="Docs, articles, guides, and changelogs" />
<span class="picker-mode-preview" aria-hidden="true">
<span class="pm-desktop pm-landing">
<span class="pm-topbar"><i class="pm-dot"></i><span class="pm-links"><i></i><i></i></span></span>
<span class="pm-article">
<i class="pm-bar pm-strong" style="--w:62%"></i>
<i class="pm-bar pm-faint" style="--w:34%"></i>
<i class="pm-bar" style="--w:100%"></i>
<i class="pm-bar" style="--w:100%"></i>
<i class="pm-bar" style="--w:90%"></i>
<i class="pm-bar" style="--w:97%"></i>
<i class="pm-bar" style="--w:58%"></i>
</span>
</span>
<span class="pm-phone">
<span class="pm-phone-body">
<i class="pm-bar pm-strong" style="--w:74%"></i>
<i class="pm-bar pm-faint" style="--w:40%"></i>
<i class="pm-bar" style="--w:100%"></i>
<i class="pm-bar" style="--w:100%"></i>
<i class="pm-bar" style="--w:88%"></i>
<i class="pm-bar" style="--w:96%"></i>
<i class="pm-bar" style="--w:64%"></i>
</span>
</span>
</span>
<span class="picker-mode-copy">
<span class="picker-mode-pills"><i>Docs</i><i>Articles</i><i>Guides</i><i>Changelogs</i></span>
<span class="picker-mode-goal">The job is to <strong>read and understand</strong>. Let type and structure do the work.</span>
</span>
</label>
<label class="picker-mode-tile">
<input type="checkbox" name="surface-modes" value="experience" aria-label="Portfolios, galleries, and showcases" />
<span class="picker-mode-preview" aria-hidden="true">
<span class="pm-desktop">
<span class="pm-mosaic">
<span class="pm-media"></span>
<span class="pm-media"></span>
<span class="pm-media"></span>
</span>
</span>
<span class="pm-phone">
<span class="pm-phone-body pm-fill">
<span class="pm-media"></span>
<span class="pm-media"></span>
<i class="pm-bar pm-faint" style="--w:46%"></i>
</span>
</span>
</span>
<span class="picker-mode-copy">
<span class="picker-mode-pills"><i>Portfolios</i><i>Galleries</i><i>Showcases</i></span>
<span class="picker-mode-goal">You're <strong>showcasing</strong> work, not pitching it. Let browsing feel like exploring the work up close.</span>
</span>
</label>
</fieldset>
<p class="picker-modes-note">
Previews only help you decide. They will <strong>not be the final design</strong>.
</p>
<div class="picker-actions-stack">
<button class="ks-button ks-button-primary" type="button" data-modes-next data-advance="next">
Continue
<span class="ks-button-arrow" aria-hidden="true">
<svg viewBox="0 0 16 8" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="square">
<path d="M0 4h14M10 0l4 4-4 4"></path>
</svg>
</span>
</button>
</div>
</div>
</div>
</section>
<section class="picker-screen" data-screen="02" data-step="Palette" aria-hidden="true" aria-labelledby="picker-palette-title">
<div class="picker-container">
<div class="picker-palette">
@@ -707,10 +890,10 @@ const questions = [
<div class="picker-progress" aria-hidden="true">
<div class="picker-progress-track">
<i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i>
<i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i>
</div>
<p class="picker-progress-label">
<span class="picker-progress-index"><b data-progress-index>1</b> / 10</span>
<span class="picker-progress-index"><b data-progress-index>1</b> / 11</span>
<span class="picker-progress-name" data-progress-name></span>
</p>
</div>
@@ -812,9 +995,9 @@ const questions = [
)];
// Neither the start screen nor the review is a question, so the stepper
// counts from screen 02 and fills on the last one, icons. Review holds it
// full rather than counting past the end.
const STEPS = 10;
// counts from the surfaces screen and fills on the last one, icons.
// Review holds it full rather than counting past the end.
const STEPS = 11;
const progress = $('.picker-progress');
const setProgress = (index, name) => {
const step = Math.min(Math.max(index, 1), STEPS);
+21
View File
@@ -1264,9 +1264,30 @@ document.addEventListener('picker:screenchange', (event) => {
}
});
// Screen 01b: the surface tiles. Multi-select with a floor of one, so the
// answer can never arrive empty; the continue button holds that line while
// the markup's default (persuade) keeps scripted runs moving.
const modeInputs = [...document.querySelectorAll('input[name="surface-modes"]')];
const modesNext = document.querySelector('[data-modes-next]');
const syncModesNext = () => {
if (modesNext) modesNext.disabled = !modeInputs.some((input) => input.checked);
};
for (const input of modeInputs) input.addEventListener('change', syncModesNext);
syncModesNext();
try {
const get = (url) => fetch(url).then((response) => response.ok ? response.json() : Promise.reject());
const [cueData, seedData] = await Promise.all([get('/cues.json'), get('/palettes.json')]);
// The agent's reading of PRODUCT.md arrives as cues.modes and pre-checks
// the surface tiles. Applied only when it names at least one real tile, so
// a bad hint cannot uncheck everything.
if (Array.isArray(cueData.modes)) {
const wanted = new Set(cueData.modes);
if (modeInputs.some((input) => wanted.has(input.value))) {
for (const input of modeInputs) input.checked = wanted.has(input.value);
syncModesNext();
}
}
cards = [
...cueData.cues.map((id) => ({ id, type: 'cue', palette: cueData.palette[id] })),
...seedData.seeds.map((seed) => ({ ...seed, type: 'seed' })),
+501 -5
View File
@@ -275,6 +275,7 @@ body.picker-page {
/* The art belongs to the start screen and the finish. Every question in
between is judged against a plain ground, because a photograph behind a
palette or a wireframe is a second opinion nobody asked for. */
.picker-shell:has(#picker-form[data-current="01b"]) .picker-hero-art,
.picker-shell:has(#picker-form[data-current="02"]) .picker-hero-art,
.picker-shell:has(#picker-form[data-current="03"]) .picker-hero-art,
.picker-shell:has(#picker-form[data-current="04"]) .picker-hero-art,
@@ -300,13 +301,13 @@ body.picker-page {
transition: opacity 180ms var(--ks-ease);
}
/* Ten steps hold the same total width five did: the row is a measure of how
far along the interview is, and a wider one would start competing with the
question. The ticks divide whatever that width is. */
/* Eleven steps hold the same total width five did: the row is a measure of
how far along the interview is, and a wider one would start competing with
the question. The ticks divide whatever that width is. */
.picker-progress-track {
width: 232px;
display: grid;
grid-template-columns: repeat(10, minmax(0, 1fr));
grid-template-columns: repeat(11, minmax(0, 1fr));
gap: 5px;
}
@@ -325,7 +326,8 @@ body.picker-page {
.picker-progress[data-step="7"] .picker-progress-track i:nth-child(-n + 7),
.picker-progress[data-step="8"] .picker-progress-track i:nth-child(-n + 8),
.picker-progress[data-step="9"] .picker-progress-track i:nth-child(-n + 9),
.picker-progress[data-step="10"] .picker-progress-track i {
.picker-progress[data-step="10"] .picker-progress-track i:nth-child(-n + 10),
.picker-progress[data-step="11"] .picker-progress-track i {
background: var(--ks-kinpaku);
}
@@ -1222,6 +1224,500 @@ body.picker-page {
background: var(--pv-primary);
}
/* ── Screen 01b: which surfaces ──────────────────────────────
Four tiles, one per mode the skill recognizes, chosen before any visual
question so every later answer knows what kind of page it is dressing.
Multi-select: a product is allowed to be an app with a landing page and
docs. The previews are deliberately monochrome, drawn entirely from the
theme's own neutral ramp; color has not been chosen yet, and a gray
wireframe cannot pre-sell one. */
.picker-modes {
display: grid;
gap: clamp(12px, 2svh, 26px);
justify-items: center;
padding-block: clamp(16px, 2.6svh, 36px) clamp(12px, 2svh, 32px);
}
.picker-modes-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
/* Both rows take the taller row's height, so a goal or chip row that wraps
in one tile cannot leave the other three sitting shorter than it. */
grid-template-rows: repeat(2, minmax(0, 1fr));
gap: 14px;
/* Narrow enough that a proportional preview fills most of its tile. A wider
grid leaves the frames marooned in empty space. */
width: min(100%, 760px);
margin: 0;
padding: 0;
border: 0;
}
.picker-modes-grid legend {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0 0 0 0);
clip-path: inset(50%);
white-space: nowrap;
}
.picker-mode-tile {
position: relative;
display: grid;
grid-template-rows: auto 1fr;
gap: clamp(7px, 1.1svh, 14px);
padding: clamp(9px, 1.4svh, 16px);
background: var(--ks-lacquer-raised);
border: 1px solid var(--ks-rule);
cursor: pointer;
transition:
border-color 180ms var(--ks-ease),
background-color 180ms var(--ks-ease);
}
.picker-mode-tile input {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0 0 0 0);
clip-path: inset(50%);
white-space: nowrap;
}
.picker-mode-tile:hover {
border-color: color-mix(in oklab, var(--ks-patina) 45%, var(--ks-rule));
}
.picker-mode-tile:has(input:checked) {
background: color-mix(in oklab, var(--ks-patina) 9%, var(--ks-lacquer-raised));
border-color: var(--ks-patina);
}
.picker-mode-tile:has(input:focus-visible) {
outline: 2px solid var(--ks-patina-deep);
outline-offset: -2px;
z-index: 1;
}
/* The tile's little pair of frames: same anatomy as the palette screen's
preview, drawn in the neutral ramp. Bars take their width from --w so each
mode's silhouette is written in the markup, not in a pile of one-off
classes. The three bar weights below are the whole drawing vocabulary. */
.picker-mode-preview {
--pm-strong: var(--ks-text-muted);
--pm-mid: var(--ks-text-mute-deep);
--pm-faint: color-mix(in oklab, var(--ks-text-mute-deep) 45%, var(--ks-graphite));
--pm-fill: var(--ks-graphite-2);
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 10px;
/* The height is the budget and the width follows it, at the same 16 / 7 the
palette screen's preview uses. Letting the tile set the width instead
stretched the desktop card to about five times its own ratio. */
height: clamp(104px, 13svh, 152px);
aspect-ratio: 16 / 7;
width: auto;
max-width: 100%;
justify-self: center;
}
.pm-desktop,
.pm-phone {
min-width: 0;
min-height: 0;
overflow: hidden;
display: grid;
background: var(--ks-graphite);
border: 1px solid var(--ks-rule);
border-radius: 3px;
}
.pm-phone {
height: 100%;
aspect-ratio: 9 / 19;
grid-template-rows: auto 1fr auto;
border-radius: 7px;
}
.pm-phone > .pm-phone-body:first-child {
grid-row: 1 / -1;
}
.pm-landing {
grid-template-rows: auto 1fr;
}
.pm-bar {
display: block;
width: var(--w, 100%);
height: 5px;
background: var(--pm-mid);
border-radius: 2px;
}
.pm-strong {
height: 8px;
background: var(--pm-strong);
}
.pm-faint {
background: var(--pm-faint);
}
.pm-dot {
width: 5px;
aspect-ratio: 1;
background: var(--pm-strong);
border-radius: 50%;
}
.pm-topbar {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 8px;
border-bottom: 1px solid var(--ks-rule);
}
.pm-links {
display: flex;
gap: 4px;
margin-left: auto;
}
.pm-links i {
width: 10px;
height: 3px;
background: var(--pm-mid);
border-radius: 2px;
}
.pm-media {
min-width: 0;
min-height: 0;
background: var(--pm-fill);
border-radius: 2px;
}
/* Operate: rail plus work area, the one silhouette nobody mistakes for a
landing page. */
.pm-split {
grid-template-columns: 26% minmax(0, 1fr);
}
.pm-side {
display: grid;
align-content: start;
gap: 7px;
padding: 9px 8px;
border-right: 1px solid var(--ks-rule);
}
.pm-side i {
height: 5px;
background: var(--pm-mid);
border-radius: 2px;
}
.pm-side i:first-child {
background: var(--pm-strong);
}
.pm-main {
min-height: 0;
display: grid;
grid-template-rows: auto 1fr;
gap: 7px;
padding: 8px;
}
.pm-cards {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 6px;
}
.pm-cards-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.pm-cards i {
height: 26px;
background: var(--pm-fill);
border: 1px solid var(--ks-rule);
border-radius: 2px;
}
.pm-rows {
display: grid;
align-content: start;
gap: 7px;
}
.pm-rows i {
height: 5px;
background: var(--pm-faint);
border-radius: 2px;
}
/* Read: one measured column of text and nothing competing with it. */
.pm-article {
min-height: 0;
display: grid;
gap: 7px;
align-content: center;
justify-items: center;
padding: 8px 20%;
}
/* Experience: the pictures are the page. */
.pm-mosaic {
min-height: 0;
display: grid;
grid-template-columns: 1.5fr 1fr;
grid-template-rows: repeat(2, minmax(0, 1fr));
gap: 5px;
padding: 7px;
}
.pm-mosaic .pm-media:first-child {
grid-row: 1 / -1;
}
.pm-phone-top {
display: flex;
justify-content: space-between;
align-items: center;
gap: 6px;
padding: 6px 7px;
border-bottom: 1px solid var(--ks-rule);
}
.pm-phone-body {
min-height: 0;
display: grid;
align-content: start;
gap: 5px;
padding: 7px;
}
.pm-fill {
align-content: stretch;
grid-template-rows: 1fr 1fr auto;
}
.pm-tabbar {
display: flex;
justify-content: space-evenly;
align-items: center;
padding: 5px 0;
border-top: 1px solid var(--ks-rule);
}
.pm-tabbar i {
width: 4px;
aspect-ratio: 1;
background: var(--pm-mid);
border-radius: 50%;
}
.pm-tabbar i:first-child {
background: var(--pm-strong);
}
/* Persuade carries the palette screen's preview component itself, class and
all, so it inherits that component's every color derivation. The tile also
sets the four palette roles inline, because the palette screen has not been
reached yet and the component would otherwise fall back to the picker's own
dark chrome. Only the sizes below change: the component is authored for a
box roughly three times this tall, and every rule here is scoped to the
tile so screen 02 renders exactly as it did. */
.picker-mode-preview .pv-desktop {
border-radius: 3px;
}
.picker-mode-preview .pv-phone {
border-radius: 7px;
}
.picker-mode-preview .pv-nav {
gap: 5px;
padding: 4px 7px;
}
.picker-mode-preview .pv-logo {
width: 5px;
}
.picker-mode-preview .pv-nav-bars {
gap: 4px;
}
.picker-mode-preview .pv-nav-bars i {
width: 9px;
height: 2px;
}
.picker-mode-preview .pv-pill {
width: 15px;
height: 6px;
}
.picker-mode-preview .pv-hero {
gap: 7%;
padding: 3px 8% 6px;
}
.picker-mode-preview .pv-hero-copy {
gap: 3px;
}
.picker-mode-preview .pv-eyebrow {
width: 30%;
height: 3px;
}
.picker-mode-preview .pv-headline {
gap: 3px;
margin-block: 1px;
}
.picker-mode-preview .pv-headline i {
height: 5px;
}
.picker-mode-preview .pv-copy-bar {
height: 3px;
}
.picker-mode-preview .pv-actions {
gap: 4px;
margin-top: 3px;
}
.picker-mode-preview .pv-actions i {
width: 18px;
height: 7px;
}
/* The frame is wide and short here, so a 4 / 3 plate would stand taller than
the whole desktop: the image takes its height from the hero row instead of
setting it. Its gradient and border are the component's own. */
.picker-mode-preview .pv-image {
align-self: stretch;
aspect-ratio: auto;
}
.picker-mode-preview .pv-image::after {
top: 4px;
right: 4px;
width: 5px;
}
.picker-mode-preview .pv-phone-top {
padding: 3px 5px;
}
.picker-mode-preview .pv-avatar {
width: 6px;
}
.picker-mode-preview .pv-phone-body {
gap: 3px;
padding: 4px 4px 5px;
}
.picker-mode-preview .pv-mini-card {
gap: 2px;
padding: 2px 3px;
}
.picker-mode-preview .pv-mini-card i,
.picker-mode-preview .pv-mini-card span {
height: 2px;
}
.picker-mode-preview .pv-mini-card span {
height: 3px;
}
.picker-mode-preview .pv-mini-chart {
gap: 2px;
padding: 2px 3px;
}
.picker-mode-preview .pv-tabbar {
padding: 3px 0;
}
.picker-mode-preview .pv-tabbar i {
width: 3px;
}
.picker-mode-copy {
display: grid;
gap: 6px;
align-content: start;
}
.picker-mode-goal {
color: var(--ks-text-muted);
font-size: 14px;
line-height: 1.5;
}
/* The examples name the tile now that the mode word is gone, so they lead the
copy block and carry its strongest weight. Sized to keep a four-chip row
within two lines at the narrowest tile the grid produces. */
.picker-mode-pills {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.picker-mode-pills i {
padding: 3px 7px;
color: var(--ks-champagne);
font-size: 12px;
font-style: normal;
letter-spacing: 0.015em;
border: 1px solid color-mix(in oklab, var(--ks-champagne) 24%, var(--ks-rule));
border-radius: 2px;
transition:
color 180ms var(--ks-ease),
border-color 180ms var(--ks-ease);
}
.picker-mode-tile:hover .picker-mode-pills i {
border-color: color-mix(in oklab, var(--ks-patina) 30%, var(--ks-rule));
}
.picker-mode-tile:has(input:checked) .picker-mode-pills i {
color: color-mix(in oklab, var(--ks-patina) 85%, var(--ks-champagne));
border-color: color-mix(in oklab, var(--ks-patina) 55%, var(--ks-rule));
}
/* The wording here is doing careful work: every preview in the interview is
an isolating instrument, not a sketch of the deliverable, and this is the
one place the visitor is told so before meeting the rest of them. */
.picker-modes-note {
max-width: 52ch;
color: var(--ks-text-muted);
font-size: 1.08rem;
line-height: 1.5;
text-align: center;
text-wrap: balance;
}
.picker-modes-note strong,
.picker-mode-goal strong {
color: var(--ks-text);
font-weight: 600;
}
/* Screen 03 mirrors screen 02's rhythm: the same centered headline class,
the same vertical gaps, options left and preview right. */
.picker-strategy {
+2
View File
@@ -436,6 +436,8 @@ Done when: `fonts.json` is parseable, contains exactly six ranked pairs, every f
## Step 7: Launch the picker
Before launching, add a top-level `modes` array to `cues.json` naming the surface kinds this product already implies, judged from PRODUCT.md and the codebase: any of `persuade`, `operate`, `read`, `experience`. The picker's first question pre-checks those tiles as its starting point; the user corrects the set by hand, and the final selection returns in the answers as `surface-modes`. Omit the field when the product gives no clear signal; the picker then starts from `persuade` alone.
Tell the user in one line that the visual cues are ready at `.impeccable/visual-cues/` (name the count), then run `node {{scripts_path}}/picker-server.mjs` from the project root as a foreground command and parse its `PICKER_URL` line.
- **The harness has a browser tool**: open the URL with it and let the user drive. The tool is a viewport only; never drive the questionnaire yourself, because the answers are the user's.