Files
pbakaus_impeccable/picker/pages/index.astro
T
Abdul WahabandCursor 60130bcea9 Add strategy-band contrast alert and expand design context fields.
Mirror the screen 02 neutral contrast badge on the strategy strip, share alert
painting between both strips, and render richer PRODUCT.md context in the
design document with matching visual-cues schema guidance.

AI-assisted commit.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-01 10:02:40 +05:00

1709 lines
90 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
import Picker from '../layouts/Picker.astro';
import Artboard from '../components/Artboard.astro';
import QuestionScreen from '../components/QuestionScreen.astro';
import iconPacks from '../data/icon-packs.json';
import { SURFACE_MODES, surfaceAttrs, surfaceTabsAttrs } from '../data/surfaces.js';
const roles = [
['primary', 'Primary'],
['secondary', 'Secondary'],
['tertiary', 'Tertiary'],
['neutral', 'Neutral'],
];
// Musical intervals, smallest step first. The ratio is the multiplier between
// one size and the next; the description says what that much contrast is for,
// since the number alone tells nobody which to pick.
const scales = [
['minor-second', 1.067, 'Minor Second', 'Almost no contrast. For dense tools where weight and color carry hierarchy instead of size.'],
['major-second', 1.125, 'Major Second', 'A quiet step. Long documents and data screens that need many levels close together.'],
['minor-third', 1.2, 'Minor Third', 'The workhorse. Clear order without shouting, and it survives on small screens.'],
['major-third', 1.25, 'Major Third', 'Confident separation. A safe default when the page has both marketing and interface on it.'],
['perfect-fourth', 1.333, 'Perfect Fourth', 'Real jumps between levels. Landing pages where the headline has to lead.'],
['augmented-fourth', 1.414, 'Augmented Fourth', 'Sharp contrast. Editorial layouts built from few sizes used decisively.'],
['perfect-fifth', 1.5, 'Perfect Fifth', 'Display type against small text, with little in between. Poster thinking.'],
['golden-ratio', 1.618, 'Golden Ratio', 'The widest range here. Beautiful with one big statement, awkward with six levels.'],
];
// Exponent above the 16px base: P sits at the base, H1 six steps up.
const scaleRows = [['H1', 6], ['H2', 5], ['H3', 4], ['H4', 3], ['H5', 2], ['H6', 1], ['P', 0]];
// Only the row copy is rendered here. The glyphs are 109KB of markup for sets
// the run will use one of, so they are fetched when the screen first opens.
const iconRows = iconPacks.packs.map((pack) => ({
id: pack.id,
name: pack.name,
license: pack.license,
url: pack.url,
note: pack.note,
meta: `${pack.grid} grid / ${pack.weight} / ${pack.license}`,
}));
// The surface tiles and the per-surface answer matrix both live in
// data/surfaces.js, so the tiles carry what each surface allows without the
// matrix being restated on every screen that reads it.
// The five questions that are answered by looking at a 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 whose answer
// cannot be read on a handset, so it takes the frame alone.
//
// All five draw a board per surface they are put to, which is four on
// boundaries, corners, and depth and a pair each on the two scoped questions,
// motion and layout. A structural answer is a claim about a kind of page, and the
// kinds are not the same shape: an app shell divides into a rail, a working
// column, and a panel, a document is a rail and a measure, an index is a run of
// entries. Repainting one landing page under four labels showed the same page
// four times and asked the visitor to imagine the rest.
//
// Which board is showing is read off `data-surface`, the attribute the component
// already publishes and the one the tab strip toggles, rather than a class per
// screen per surface. Screen 06 takes a class because its scenes are written per
// board; the rest ask the same question of every board and only need to know
// which anatomy is under them.
const boards = [
{ surface: 'persuade' },
{ surface: 'operate' },
{ surface: 'read' },
{ surface: 'experience' },
];
const questions = [
{
id: '06',
step: 'Motion',
slug: 'motion',
name: 'motion-energy',
title: 'How much movement should it have?',
legend: 'Motion energy',
cta: 'Select motion energy',
previewClass: 'picker-preview-motion',
phone: false,
// The two surfaces this question is put to, and the only screen where a
// surface answers on a page of its own kind: an energy is demonstrated by
// watching a page use it, and a landing page and a portfolio use movement
// for different ends. Repainting one wireframe would show the same scene
// twice under two labels.
boards: [
{ surface: 'persuade' },
{ surface: 'experience', class: 'picker-preview-motion--index' },
],
options: [
{
value: 'restrained',
title: 'Restrained',
desc: 'State changes only. Things are already changed by the time you see them.',
},
{
value: 'responsive',
title: 'Responsive',
desc: 'Feedback and transitions on whatever you touch. Nothing is orchestrated.',
},
{
value: 'choreographed',
title: 'Choreographed',
desc: 'Entrances are staged and scrolling drives sequences.',
},
],
},
{
id: '07',
step: 'Layout',
slug: 'layout',
name: 'layout-structure',
title: 'How strict should the layout feel?',
legend: 'Layout structure',
cta: 'Select layout structure',
previewClass: 'picker-strategy-preview picker-preview-layout',
// The two surfaces this question is put to, for the reason in
// data/surfaces.js: a landing page answers in the spans its hero and its
// card row take and a portfolio in how each entry splits between the work
// and its caption, where a shell and a document are composed by what they
// hold. The answer is one value all the same, and the boards on the screens
// after this one show it on all four pages.
boards: [
{ surface: 'persuade' },
{ surface: 'experience' },
],
options: [
{
value: 'simple-grid',
title: 'Simple grid',
desc: 'Everything lines up. Equal columns and one rhythm down the page.',
},
{
value: 'balanced',
title: 'Balanced',
desc: 'An aligned grid with a few deliberate breaks that carry the emphasis.',
},
{
value: 'freeform',
title: 'Freeform',
desc: 'Blocks step out of the grid and overlap where it pays off.',
},
],
},
{
id: '08',
step: 'Boundaries',
slug: 'boundaries',
name: 'boundary-style',
title: 'How should sections be separated?',
legend: 'Boundary style',
cta: 'Select boundary style',
previewClass: 'picker-preview-boundaries',
boards,
options: [
{
value: 'open-space',
title: 'Open space',
desc: 'Nothing between sections but room. Spacing does the dividing.',
blocked: 'Sidebars and toolbars need a ground of their own, which is the one thing air cannot give them.',
},
{
value: 'thin-dividers',
title: 'Thin dividers',
desc: 'A hairline marks where one section ends and the next starts.',
},
{
value: 'surface-changes',
title: 'Surface changes',
desc: 'Bands of tone. Sections are told apart by the ground they sit on.',
},
{
value: 'cards-and-panels',
title: 'Cards and panels',
desc: 'Content sits inside containers with edges you can see.',
blocked: 'An edge around every block comes between people and the thing they came for.',
},
],
},
{
id: '09',
step: 'Corners',
slug: 'corners',
name: 'corner-style',
title: 'How round should shapes be?',
legend: 'Corner style',
cta: 'Select corner style',
previewClass: 'picker-preview-corners',
boards,
options: [
{
value: 'sharp',
title: 'Sharp',
desc: 'Square corners throughout. Precise, drafted, architectural.',
},
{
value: 'slightly-soft',
title: 'Slightly soft',
desc: 'Just enough radius to take the edge off without being noticed.',
},
{
value: 'friendly',
title: 'Friendly',
desc: 'Cards, buttons, and images are clearly rounded.',
},
{
value: 'pill',
title: 'Pill-like',
desc: 'Buttons and tags go fully round.',
blocked: 'Fully round controls are the loudest shape on a page where the work should lead.',
},
],
},
{
id: '10',
step: 'Depth',
slug: 'depth',
name: 'depth-style',
title: 'How much depth should the surface have?',
legend: 'Depth style',
cta: 'Select depth style',
previewClass: 'picker-preview-depth',
boards,
options: [
{
value: 'flat',
title: 'Flat',
desc: 'One plane. Nothing casts a shadow.',
},
{
value: 'soft-lift',
title: 'Soft lift',
desc: 'Cards and controls sit a little off the page.',
},
{
value: 'floating',
title: 'Floating',
desc: 'Panels are well above the surface, with the lift to prove it.',
blocked: 'Lift this deep belongs to menus and dialogs, not to a page people work in or read at length.',
},
],
},
];
---
<Picker>
<main class="picker-shell">
<div class="picker-hero-art" style="background-image: url('assets/hero-dark.jpg')" aria-hidden="true"></div>
<form id="picker-form">
<section class="picker-screen" data-screen="01" data-active aria-labelledby="picker-start-title">
<div class="picker-container">
<div class="picker-start">
<div class="picker-brand">
<span class="picker-brand-logo" aria-hidden="true">
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M5 2.5 L13.5 2.5 L5.5 21.5 L5 21.5 Q2.5 21.5 2.5 19 L2.5 5 Q2.5 2.5 5 2.5 Z"></path>
<path d="M16.5 2.5 L19 2.5 Q21.5 2.5 21.5 5 L21.5 19 Q21.5 21.5 19 21.5 L8.5 21.5 Z"></path>
</svg>
</span>
<span class="picker-brand-name">Impeccable</span>
</div>
<h1 id="picker-start-title" class="picker-title">Let's build your<br />design system.</h1>
<div class="picker-actions">
<button class="ks-button ks-button-primary" type="button" data-advance="next">
Start
<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 class="picker-key-hint">
<div class="picker-key-cluster" aria-hidden="true">
<div class="picker-key-row picker-key-row-top">
<kbd class="picker-keycap">↑</kbd>
</div>
<div class="picker-key-row">
<kbd class="picker-keycap">←</kbd>
<kbd class="picker-keycap">↓</kbd>
<kbd class="picker-keycap">→</kbd>
</div>
</div>
<p class="picker-key-caption">Arrow keys to move</p>
</div>
</div>
</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" data-surface-label="Landing page" {...surfaceAttrs('persuade')} checked />
<span class="picker-mode-preview picker-mode-preview--exact" aria-hidden="true">
<span class="picker-preview">
<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-image"></span>
<span class="pv-mini-card"><i></i><span></span></span>
<span class="pv-mini-card"><i></i><span></span></span>
</span>
<span class="pv-tabbar"><i></i><i></i><i></i><i></i></span>
</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" data-surface-label="App UI" {...surfaceAttrs('operate')} />
<span class="picker-mode-preview picker-mode-preview--exact" aria-hidden="true">
<span class="picker-preview picker-preview--ops">
<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="po-body">
<span class="po-side">
<span class="po-item"><i class="po-dot"></i><i class="po-line"></i></span>
<span class="po-item po-item--on"><i class="po-dot"></i><i class="po-line"></i></span>
<span class="po-item"><i class="po-dot"></i><i class="po-line"></i></span>
</span>
<span class="po-main">
<span class="po-chart">
<i style="--h: 19.23cqh"></i>
<i style="--h: 26.17cqh"></i>
<i style="--h: 16cqh"></i>
<i class="po-peak" style="--h: 30.7cqh"></i>
<i style="--h: 22.3cqh"></i>
</span>
<span class="po-divider"></span>
<span class="po-rows">
<span class="po-row">
<i class="po-dot"></i>
<i class="po-cell" style="--w: 15.24"></i>
<i class="po-cell" style="--w: 12.76"></i>
<i class="po-cell" style="--w: 12.76"></i>
</span>
<span class="po-row po-row--on">
<i class="po-dot"></i>
<i class="po-cell" style="--w: 15.24"></i>
<i class="po-cell" style="--w: 12.76"></i>
<i class="po-cell" style="--w: 12.61"></i>
</span>
<span class="po-row">
<i class="po-dot"></i>
<i class="po-cell" style="--w: 15.24"></i>
<i class="po-cell" style="--w: 12.76"></i>
<i class="po-cell" style="--w: 12.61"></i>
</span>
</span>
</span>
<span class="po-panel">
<span class="po-heading"></span>
<span class="po-field"><i class="po-chev"></i></span>
<span class="po-switch"><i class="po-label"></i><i class="po-toggle po-toggle--on"></i></span>
<span class="po-switch"><i class="po-label"></i><i class="po-toggle"></i></span>
</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="po-phone-body">
<span class="po-chart">
<i style="--h: 17.34cqh"></i>
<i style="--h: 25.42cqh"></i>
<i style="--h: 15.33cqh"></i>
<i class="po-peak" style="--h: 33.57cqh"></i>
<i style="--h: 23.93cqh"></i>
</span>
<span class="po-field"><i class="po-chev"></i></span>
<span class="po-prow">
<i class="po-dot"></i>
<i class="po-label"></i>
<i class="po-toggle po-toggle--on"></i>
</span>
<span class="po-prow">
<i class="po-dot"></i>
<i class="po-label"></i>
<i class="po-chev po-chev--next"></i>
</span>
</span>
<span class="pv-tabbar"><i></i><i></i><i></i><i></i></span>
</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" data-surface-label="Docs" {...surfaceAttrs('read')} />
<span class="picker-mode-preview picker-mode-preview--exact" aria-hidden="true">
<span class="picker-preview picker-preview--docs">
<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="pd-body">
<span class="pd-side">
<span class="pd-item"><i></i><span></span></span>
<span class="pd-item pd-item--on"><i></i><span></span></span>
<span class="pd-item"><i></i><span></span></span>
<span class="pd-item"><i></i><span></span></span>
</span>
<span class="pd-main">
<span class="pd-heading" style="--w:95%"></span>
<span class="pd-para pd-para--lede"><i style="--w:71%"></i><i style="--w:41%"></i></span>
<span class="pd-para"><i></i><i></i><i style="--w:72%"></i></span>
<span class="pd-para"><i></i><i></i><i style="--w:43%"></i></span>
<span class="pd-note">
<i class="pd-note-dot"></i>
<span class="pd-note-lines"><i style="--w:85%"></i><i style="--w:62%"></i></span>
</span>
</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="pd-phone-body">
<span class="pd-crumb"></span>
<span class="pd-heading" style="--w:98.5%"></span>
<span class="pd-para pd-para--lede"><i style="--w:71%"></i><i style="--w:55%"></i></span>
<span class="pd-para"><i></i><i></i><i></i><i style="--w:69%"></i></span>
<span class="pd-para"><i></i><i></i><i style="--w:57%"></i></span>
<span class="pd-note">
<i class="pd-note-dot"></i>
<span class="pd-note-lines"><i style="--w:92%"></i><i style="--w:63%"></i></span>
</span>
</span>
<span class="pv-tabbar"><i></i><i></i><i></i><i></i></span>
</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" data-surface-label="Portfolio" {...surfaceAttrs('experience')} />
<span class="picker-mode-preview picker-mode-preview--exact" aria-hidden="true">
<span class="picker-preview picker-preview--gallery">
<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="pg-body">
<span class="pg-row">
<span class="pv-image"></span>
<span class="pg-cap">
<i class="pg-cap-title" style="--w:33.61%"></i>
<i></i>
<i style="--w:65.3%"></i>
</span>
</span>
<span class="pg-row pg-row--flip">
<span class="pg-cap">
<i class="pg-cap-title" style="--w:36.64%"></i>
<i></i>
<i style="--w:66.07%"></i>
</span>
<span class="pv-image"></span>
</span>
<span class="pg-rail">
<i class="pg-arrow"></i>
<span class="pg-track">
<i class="pg-track-on" style="--w: 20.31"></i>
<i style="--w: 9.14"></i>
<i style="--w: 8.98"></i>
<i style="--w: 12.44"></i>
</span>
<i class="pg-arrow pg-arrow--next"></i>
</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="pg-phone-body">
<span class="pv-image"></span>
<span class="pg-cap">
<i class="pg-cap-title" style="--w:30.18%"></i>
<i style="--w:79.68%"></i>
<i style="--w:55.6%"></i>
</span>
<span class="pv-image"></span>
<span class="pg-cap">
<i class="pg-cap-title" style="--w:30.18%"></i>
<i style="--w:79.68%"></i>
<i style="--w:55.6%"></i>
</span>
</span>
<span class="pv-tabbar"><i></i><i></i><i></i></span>
</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">
<h2 id="picker-palette-title" class="picker-question-title">Choose a color palette</h2>
<div class="picker-palette-grid">
<div class="picker-deck-column" aria-label="Palette sources">
<div class="picker-deck-stage">
<div class="picker-deck-scroller" data-deck-scroll aria-label="Browse palette cards">
<div data-deck-points></div>
</div>
<div class="picker-card-layer" data-deck-cards></div>
<div class="picker-loupe" data-loupe aria-hidden="true">
<canvas width="80" height="80"></canvas>
</div>
</div>
<template data-cue-card>
<article class="picker-card">
<div class="picker-card-face picker-cue-face">
<img alt="" />
{roles.map(([role, label]) => (
<button class="picker-ring" type="button" data-role={role} role="slider" aria-label={`${label} color picker`}></button>
))}
</div>
</article>
</template>
<template data-seed-card>
<article class="picker-card">
<div class="picker-card-face picker-seed-face">
{roles.map(() => <span></span>)}
</div>
</article>
</template>
<div class="picker-deck-controls">
<button class="picker-icon-button" type="button" data-deck-prev aria-label="Previous card">
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="m12.5 4.5-5 5.5 5 5.5"></path></svg>
</button>
<p class="picker-deck-count" data-deck-count aria-live="polite">Loading palettes</p>
<button class="picker-icon-button" type="button" data-deck-next aria-label="Next card">
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="m7.5 4.5 5 5.5-5 5.5"></path></svg>
</button>
</div>
<p class="picker-ring-guide" data-ring-guide>
<span>Drag the color rings to sample from the image.</span>
<span>Use arrow keys for precise adjustments.</span>
</p>
</div>
<div class="picker-palette-subgrid">
<fieldset class="picker-palette-panel">
<legend>Chosen colors</legend>
<button class="picker-icon-button picker-tip picker-reset" type="button" data-reset data-tip="Reset to this card's colors" aria-label="Reset to this card's colors">
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="M4.6 7.2A6.2 6.2 0 1 1 4 11.4M4.6 7.2V3.8m0 3.4H8"></path></svg>
</button>
<p
class="picker-palette-hint"
data-palette-hint
data-idle="Drag the rings on the image to pull colors from it, or edit any swatch directly."
data-primary="Your main brand color: buttons, links, the color people remember. Pick something with presence."
data-secondary="Supports the primary: section accents, hovers, secondary buttons. Adjacent, not identical."
data-tertiary="The rare accent: badges, highlights, one detail per screen. The most saturated of the four."
data-neutral="Backgrounds and large surfaces: most of every page. Quiet, near-white or near-black."
aria-live="polite"
>Drag the rings on the image to pull colors from it, or edit any swatch directly.</p>
<div class="picker-bands">
{roles.map(([role, label]) => (
<div class="picker-band-item" data-band-item={role}>
<div class="picker-band" data-band={role} tabindex="0">
<button class="picker-band-grip picker-tip" type="button" data-grip={role} data-tip="Drag to reorder" aria-label={`Reorder the ${label} color`}>
<svg viewBox="0 0 20 20" aria-hidden="true">
<circle cx="7" cy="5" r="1.5"></circle><circle cx="13" cy="5" r="1.5"></circle>
<circle cx="7" cy="10" r="1.5"></circle><circle cx="13" cy="10" r="1.5"></circle>
<circle cx="7" cy="15" r="1.5"></circle><circle cx="13" cy="15" r="1.5"></circle>
</svg>
</button>
<output data-band-hex={role}>#000000</output>
<div class="picker-band-tools" role="group" aria-label={`${label} color actions`}>
<button class="picker-band-tool picker-tip" type="button" data-copy-color={role} data-tip="Copy hex + OKLCH" aria-label={`Copy ${label} hex and OKLCH`}>
<svg viewBox="0 0 20 20" aria-hidden="true"><rect x="6.5" y="6.5" width="9" height="9"></rect><path d="M4.5 13.5h-1v-10h10v1"></path></svg>
</button>
<button class="picker-band-tool picker-tip" type="button" data-edit-tints={role} data-tip="Edit tints" aria-expanded="false" aria-label={`Edit ${label} tints`}>
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="m4 14.5-.4 2 2-.4L15 6.7 13.3 5 4 14.5Z"></path><path d="m11.9 6.4 1.7 1.7"></path></svg>
</button>
<button class="picker-band-tool picker-tip" type="button" data-custom-color={role} data-tip="Custom color" aria-label={`Choose custom ${label} color`}>
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="M10 4v12M4 10h12"></path></svg>
</button>
</div>
<input class="picker-native-color" type="color" value="#000000" data-color-input={role} tabindex="-1" aria-label={`Custom ${label} color`} />
{role === 'neutral' && (
<>
<span class="picker-band-alert" data-contrast-alert role="img" tabindex="0" aria-label="Contrast warning" aria-describedby="picker-contrast-tip" hidden>
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path fill="currentColor" fill-rule="evenodd" d="M12 2.5 22.5 20.5H1.5L12 2.5Z M11 9h2l-.3 5.4h-1.4L11 9Z M12 16.2a1.2 1.2 0 1 0 0 2.4 1.2 1.2 0 0 0 0-2.4Z"></path></svg>
</span>
<span class="picker-band-alert-tip" role="tooltip" id="picker-contrast-tip" data-contrast-tip></span>
</>
)}
</div>
<div class="picker-band-foot">
<h2>{label}</h2>
<div class="picker-tint-strip" data-tints={role} hidden>
{Array.from({ length: 7 }, () => <button class="picker-tint" type="button" data-tint></button>)}
<button class="picker-tint-close" type="button" data-close-tints aria-label="Close tints">×</button>
</div>
</div>
</div>
))}
</div>
<p class="picker-band-status" data-reorder-status role="status" aria-live="polite"></p>
<div class="picker-preview" aria-hidden="true">
<div class="pv-desktop">
<div class="pv-nav">
<div class="pv-logo"></div>
<div class="pv-nav-bars"><i></i><i></i><i></i></div>
<div class="pv-pill"></div>
</div>
<div class="pv-hero">
<div class="pv-hero-copy">
<div class="pv-eyebrow"></div>
<div class="pv-headline"><i></i><i></i></div>
<div class="pv-copy-bar"></div>
<div class="pv-actions"><i></i><i></i></div>
</div>
<div class="pv-image"></div>
</div>
</div>
<div class="pv-phone">
<div class="pv-phone-top">
<div class="pv-logo"></div>
<div class="pv-avatar"></div>
</div>
<div class="pv-phone-body">
<div class="pv-image"></div>
<div class="pv-mini-card"><i></i><span></span></div>
<div class="pv-mini-card"><i></i><span></span></div>
</div>
<div class="pv-tabbar"><i></i><i></i><i></i><i></i></div>
</div>
</div>
</fieldset>
<div class="picker-actions-stack">
<button class="ks-button ks-button-primary picker-select" type="button" data-select-palette data-advance="next" disabled>
Select this palette
<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>
<button class="picker-back" type="button" data-advance="prev">Back</button>
</div>
</div>
</div>
<input type="hidden" name="palette-source" />
{roles.map(([role]) => <input type="hidden" name={`palette-${role}`} />)}
</div>
</div>
</section>
<section class="picker-screen" data-screen="03" data-step="Color strategy" aria-hidden="true" aria-labelledby="picker-strategy-title">
<div class="picker-container">
<div class="picker-strategy">
<h2 id="picker-strategy-title" class="picker-question-title">How much color should this design use?</h2>
<div class="picker-strategy-grid">
<div class="picker-type-rail">
<fieldset class="picker-strategy-options picker-strategy-choices picker-type-options">
<legend>Color strategy</legend>
<label class="picker-strategy-option">
<input type="radio" name="color-strategy" value="restrained" />
<span class="picker-strategy-copy">
<span class="picker-strategy-title">Restrained</span>
<span class="picker-strategy-desc">Mostly neutral. The primary appears in a few deliberate places.</span>
</span>
</label>
<label class="picker-strategy-option">
<input type="radio" name="color-strategy" value="committed" />
<span class="picker-strategy-copy">
<span class="picker-strategy-title">Committed</span>
<span class="picker-strategy-desc">One strong color does real work across the page.</span>
</span>
</label>
{/* The reason a surface cannot take an option replaces the
description on the row, so it is written here beside the
copy it stands in for rather than in the script. */}
<label class="picker-strategy-option" data-blocked-reason="Four colors on duty compete with the work on show.">
<input type="radio" name="color-strategy" value="full-palette" checked />
<span class="picker-strategy-copy">
<span class="picker-strategy-title">Full palette</span>
<span class="picker-strategy-desc">All four colors on duty, each with a clear job.</span>
</span>
</label>
<label class="picker-strategy-option" data-blocked-reason="Too loud for a page people work in or read at length.">
<input type="radio" name="color-strategy" value="drenched" />
<span class="picker-strategy-copy">
<span class="picker-strategy-title">Drenched</span>
<span class="picker-strategy-desc">The page itself is the color. Everything else sits on top.</span>
</span>
</label>
</fieldset>
{/* Every chosen surface is colored separately and leaves its own
answer, so each has a field to leave it in. They stay disabled
until their tile is chosen, which is what keeps a surface
nobody asked for out of the answers. */}
{SURFACE_MODES.map((mode) => (
<input type="hidden" data-surface-field={`color-strategy-${mode}`} name={`color-strategy-${mode}`} disabled />
))}
<div class="picker-bands picker-bands--compact" data-band-scope="strategy">
{roles.map(([role, label]) => (
<div class="picker-band-item" data-band-item={role}>
<div class="picker-band" data-band={role}>
<button class="picker-band-grip picker-tip" type="button" data-grip={role} data-tip="Drag to reorder" aria-label={`Reorder the ${label} color`}>
<svg viewBox="0 0 20 20" aria-hidden="true">
<circle cx="7" cy="5" r="1.5"></circle><circle cx="13" cy="5" r="1.5"></circle>
<circle cx="7" cy="10" r="1.5"></circle><circle cx="13" cy="10" r="1.5"></circle>
<circle cx="7" cy="15" r="1.5"></circle><circle cx="13" cy="15" r="1.5"></circle>
</svg>
</button>
<output data-band-hex={role}>#000000</output>
{role === 'neutral' && (
<>
<span class="picker-band-alert" data-contrast-alert role="img" tabindex="0" aria-label="Contrast warning" aria-describedby="picker-strategy-contrast-tip" hidden>
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path fill="currentColor" fill-rule="evenodd" d="M12 2.5 22.5 20.5H1.5L12 2.5Z M11 9h2l-.3 5.4h-1.4L11 9Z M12 16.2a1.2 1.2 0 1 0 0 2.4 1.2 1.2 0 0 0 0-2.4Z"></path></svg>
</span>
<span class="picker-band-alert-tip" role="tooltip" id="picker-strategy-contrast-tip" data-contrast-tip></span>
</>
)}
</div>
<div class="picker-band-foot">
<h2>{label}</h2>
</div>
</div>
))}
</div>
<p class="picker-band-status" data-reorder-status role="status" aria-live="polite"></p>
</div>
<div class="picker-strategy-stage" data-surface-stage>
<div class="picker-surface-tabs" {...surfaceTabsAttrs('color-strategy')} hidden></div>
</div>
</div>
<div class="picker-actions-stack">
<button class="ks-button ks-button-primary" type="button" data-advance="next">
Select this strategy
<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>
<button class="picker-back" type="button" data-advance="prev">Back</button>
</div>
</div>
</div>
</section>
<section class="picker-screen" data-screen="04" data-step="Typeface" aria-hidden="true" aria-labelledby="picker-type-title">
<div class="picker-container">
<div class="picker-type">
<h2 id="picker-type-title" class="picker-question-title">Choose a font pair</h2>
<div class="picker-type-grid">
<div class="picker-type-rail">
<fieldset class="picker-strategy-options picker-type-options" data-font-options>
<legend>Font pairs</legend>
</fieldset>
<div class="picker-type-controls">
<div class="picker-type-scroll">
<button class="picker-icon-button" type="button" data-list-scroll="-1" aria-label="Scroll font pairs up">
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="m4.5 12.5 5.5-5 5.5 5"></path></svg>
</button>
<button class="picker-icon-button" type="button" data-list-scroll="1" aria-label="Scroll font pairs down">
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="m4.5 7.5 5.5 5 5.5-5"></path></svg>
</button>
</div>
<button class="picker-type-custom" type="button" data-font-custom-open>
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="M10 4.5v11M4.5 10h11"></path></svg>
Custom font
</button>
</div>
</div>
{/* One board per surface, all mounted and one shown. The stage is
the box the strip docks on and the node the chosen pair's
faces are written to, so every board is set in them at once. */}
<div class="picker-type-stage" data-type-stage>
<div class="picker-surface-tabs" {...surfaceTabsAttrs('font-pair')} hidden></div>
<Artboard mode="type" surface="persuade" class="picker-strategy-preview picker-preview-type" typeHook />
<Artboard mode="type" surface="operate" class="picker-strategy-preview picker-preview-type picker-preview-type--ops" typeHook />
<Artboard mode="type" surface="read" class="picker-strategy-preview picker-preview-type picker-preview-type--read" typeHook />
<Artboard mode="type" surface="experience" class="picker-strategy-preview picker-preview-type picker-preview-type--experience" typeHook />
</div>
</div>
<template data-pair-card>
<label class="picker-strategy-option picker-type-option">
<input type="radio" name="font-pair" />
<span class="picker-type-copy">
<span class="picker-type-sample-heading"><span class="picker-strategy-title" data-pair-heading></span><span class="picker-type-role">headings</span></span>
<span class="picker-type-sample-body"><span data-pair-body></span><span class="picker-type-role">body text</span></span>
<span class="picker-type-desc picker-strategy-desc" data-pair-why></span>
</span>
</label>
</template>
{/* The pair is one answer for the whole run, so these fields carry
no name and nothing but the strip reads them. They are what
stops a tab from putting its own surface's reading of the
question into the answers. */}
{SURFACE_MODES.map((mode) => (
<input type="hidden" data-surface-field={`font-pair-${mode}`} disabled />
))}
<input type="hidden" name="font-heading" />
<input type="hidden" name="font-body" />
<input type="hidden" name="font-heading-source" />
<input type="hidden" name="font-body-source" />
<div class="picker-actions-stack">
<button class="ks-button ks-button-primary" type="button" data-advance="next">
Select this pair
<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>
<button class="picker-back" type="button" data-advance="prev">Back</button>
</div>
</div>
</div>
</section>
<section class="picker-screen" data-screen="05" data-step="Type scale" aria-hidden="true" aria-labelledby="picker-scale-title">
<div class="picker-container">
<div class="picker-scale">
<div class="picker-question-head">
<h2 id="picker-scale-title" class="picker-question-title">Choose a type scale</h2>
<p class="picker-question-lede">
Create a polished, professional experience with text that stays consistent and easy to read everywhere.
</p>
</div>
<div class="picker-scale-grid">
<div class="picker-type-rail">
<fieldset class="picker-strategy-options picker-type-options picker-scale-options" data-scale-options>
<legend>Type scale</legend>
{scales.map(([slug, ratio, name, description]) => (
<label class="picker-strategy-option">
<input
type="radio"
name="type-scale"
value={slug}
data-ratio={ratio}
data-scale-name={name}
checked={slug === 'major-third'}
/>
<span class="picker-strategy-copy">
<span class="picker-strategy-title">
<b class="picker-scale-ratio">{ratio.toFixed(3)}</b>
{name}
</span>
<span class="picker-strategy-desc">{description}</span>
</span>
</label>
))}
</fieldset>
<div class="picker-type-controls">
<div class="picker-type-scroll">
<button class="picker-icon-button" type="button" data-list-scroll="-1" aria-label="Scroll type scales up">
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="m4.5 12.5 5.5-5 5.5 5"></path></svg>
</button>
<button class="picker-icon-button" type="button" data-list-scroll="1" aria-label="Scroll type scales down">
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="m4.5 7.5 5.5 5 5.5-5"></path></svg>
</button>
</div>
</div>
</div>
<div class="picker-scale-column">
<div class="picker-scale-sheet" data-scale-sheet aria-hidden="true">
{scaleRows.map(([tag, step]) => (
<div class="ts-row" data-scale-row={step}>
<p class="ts-sample" data-scale-sample></p>
<span class="ts-tag">{tag}</span>
<span class="ts-values">
<b data-scale-px></b>
<i data-scale-rem></i>
</span>
</div>
))}
</div>
<div class="picker-type-controls">
<p class="picker-scale-note">Values at a 16px base, specimen scaled to fit</p>
</div>
</div>
{/* The same scale on the components a page is actually built from.
Headings are placeholders and running text is lorem, for the
reason the wireframe copy is: a face and a ratio are judged on
texture, and real prose pulls the eye into reading it instead.
Every step the sheet lists has a component here, which is why
the lede and the quote exist at all: they carry the two steps
no editorial page would spend a heading level on. */}
<div class="picker-scale-column">
<div class="picker-scale-specimen" data-scale-specimen aria-hidden="true">
<p class="sp-h1" data-sp-label="H1">A generic display heading</p>
<p class="sp-lede" data-sp-label="Lede">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt.</p>
<p class="sp-p" data-sp-label="P">
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat. Duis aute <em>irure dolor in reprehenderit</em> in voluptate velit esse cillum dolore eu
fugiat nulla pariatur, <u>excepteur sint occaecat</u> cupidatat non proident.
</p>
<p class="sp-h2" data-sp-label="H2">A section heading</p>
<p class="sp-p" data-sp-label="P">
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium,
totam rem aperiam <strong>eaque ipsa quae</strong> ab illo inventore veritatis.
</p>
<ul class="sp-list" data-sp-label="List">
<li>Lorem ipsum dolor sit amet, consectetur</li>
<li>Adipiscing elit, sed do eiusmod tempor incididunt ut labore</li>
<li>Magna aliqua ut enim ad minim veniam</li>
</ul>
<blockquote class="sp-quote" data-sp-label="Quote">
<p>Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae.</p>
<cite class="sp-cite">Attribution line</cite>
</blockquote>
<p class="sp-h3" data-sp-label="H3">A subsection heading</p>
<p class="sp-p" data-sp-label="P">
At vero eos et accusamus et iusto odio dignissimos ducimus, qui blanditiis praesentium voluptatum
deleniti atque, held together by <code class="sp-code">a run of inline code</code> mid sentence.
</p>
<ol class="sp-list sp-list-ordered" data-sp-label="List">
<li>Nam libero tempore, cum soluta nobis</li>
<li>Eligendi optio cumque nihil impedit</li>
<li>Quo minus id quod maxime placeat</li>
</ol>
<pre class="sp-pre" data-sp-label="Code"><code>function step(base, ratio, n) &#123;
return base * ratio ** n;
&#125;</code></pre>
<p class="sp-h4" data-sp-label="H4">A minor heading</p>
<p class="sp-p" data-sp-label="P">
Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet, ut et
voluptates repudiandae sint et molestiae non recusandae.
</p>
</div>
<div class="picker-type-scroll">
<button class="picker-icon-button" type="button" data-list-scroll="-1" aria-label="Scroll the specimen up">
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="m4.5 12.5 5.5-5 5.5 5"></path></svg>
</button>
<button class="picker-icon-button" type="button" data-list-scroll="1" aria-label="Scroll the specimen down">
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="m4.5 7.5 5.5 5 5.5-5"></path></svg>
</button>
</div>
</div>
</div>
<input type="hidden" name="type-scale-ratio" />
<div class="picker-actions-stack">
<button class="ks-button ks-button-primary" type="button" data-advance="next">
Select this scale
<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>
<button class="picker-back" type="button" data-advance="prev">Back</button>
</div>
</div>
</div>
</section>
{questions.filter((question) => question.id === '07').map((question) => <QuestionScreen {...question} />)}
{/* Screen 04b: the configure hub. Every remaining question already
holds an answer (defaults are pre-filled the moment a surface tile
is checked), so the run can end here. The cards are skeletons;
summaries and edited marks are written by renderHub() in
scripts/palette-picker.js on every arrival. Placement is the
approved masonry: tall per-surface cards anchor the outer columns. */}
<section class="picker-screen" data-screen="04b" data-step="Configure" aria-hidden="true" aria-labelledby="picker-hub-title">
<div class="picker-container">
<div class="picker-hub">
<div class="picker-question-head">
<h2 id="picker-hub-title" class="picker-question-title">What else would you like to configure?</h2>
<p class="picker-question-lede">
Every choice below already holds a sensible default for your surfaces. Open any card to adjust it, or create your design context right away.
</p>
</div>
<div class="picker-hub-grid">
<div class="picker-hub-col">
<button class="picker-hub-card" type="button" data-hub-target="06" data-hub-category="material">
<span class="picker-hub-eyebrow">Material</span>
<span class="picker-hub-title">Motion
<svg class="picker-hub-arrow" viewBox="0 0 16 8" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="square" aria-hidden="true"><path d="M0 4h14M10 0l4 4-4 4"></path></svg>
<span class="picker-hub-mark" data-hub-mark hidden>Edited</span>
</span>
<span class="picker-hub-summary" data-hub-summary></span>
</button>
<button class="picker-hub-card" type="button" data-hub-target="08" data-hub-category="material">
<span class="picker-hub-eyebrow">Material</span>
<span class="picker-hub-title">Boundaries
<svg class="picker-hub-arrow" viewBox="0 0 16 8" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="square" aria-hidden="true"><path d="M0 4h14M10 0l4 4-4 4"></path></svg>
<span class="picker-hub-mark" data-hub-mark hidden>Edited</span>
</span>
<span class="picker-hub-summary" data-hub-summary></span>
</button>
</div>
<div class="picker-hub-col">
<button class="picker-hub-card" type="button" data-hub-target="11" data-hub-category="iconography">
<span class="picker-hub-eyebrow">Iconography</span>
<span class="picker-hub-title">Icon set
<svg class="picker-hub-arrow" viewBox="0 0 16 8" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="square" aria-hidden="true"><path d="M0 4h14M10 0l4 4-4 4"></path></svg>
<span class="picker-hub-mark" data-hub-mark hidden>Edited</span>
</span>
<span class="picker-hub-summary" data-hub-summary></span>
</button>
<button class="picker-hub-card" type="button" data-hub-target="09" data-hub-category="material">
<span class="picker-hub-eyebrow">Material</span>
<span class="picker-hub-title">Corners
<svg class="picker-hub-arrow" viewBox="0 0 16 8" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="square" aria-hidden="true"><path d="M0 4h14M10 0l4 4-4 4"></path></svg>
<span class="picker-hub-mark" data-hub-mark hidden>Edited</span>
</span>
<span class="picker-hub-summary" data-hub-summary></span>
</button>
</div>
<div class="picker-hub-col">
<button class="picker-hub-card" type="button" data-hub-target="10" data-hub-category="material">
<span class="picker-hub-eyebrow">Material</span>
<span class="picker-hub-title">Depth
<svg class="picker-hub-arrow" viewBox="0 0 16 8" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="square" aria-hidden="true"><path d="M0 4h14M10 0l4 4-4 4"></path></svg>
<span class="picker-hub-mark" data-hub-mark hidden>Edited</span>
</span>
<span class="picker-hub-summary" data-hub-summary></span>
</button>
</div>
</div>
<div class="picker-actions-stack">
<button class="ks-button ks-button-primary" type="button" data-hub-finish data-hub-target="12">
Create my design context
<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>
<button class="picker-back" type="button" data-advance="prev">Back</button>
</div>
</div>
</div>
</section>
{questions.filter((question) => question.id !== '07').map((question) => <QuestionScreen {...question} />)}
<section class="picker-screen" data-screen="11" data-step="Icons" aria-hidden="true" aria-labelledby="picker-icons-title">
<div class="picker-container">
<div class="picker-icons">
<h2 id="picker-icons-title" class="picker-question-title">Choose an icon set</h2>
<div class="picker-icons-grid">
<div class="picker-type-rail">
<fieldset class="picker-strategy-options picker-type-options picker-icon-options" data-icon-options>
<legend>Icon set</legend>
{iconRows.map((pack, index) => (
<div class="picker-icon-row">
<label class="picker-strategy-option">
<input
type="radio"
name="icon-pack"
value={pack.id}
data-pack-name={pack.name}
data-pack-license={pack.license}
data-pack-url={pack.url}
checked={index === 0}
/>
<span class="picker-strategy-copy">
<span class="picker-strategy-title">{pack.name}</span>
<span class="picker-icon-meta">{pack.meta}</span>
<span class="picker-strategy-desc">{pack.note}</span>
</span>
</label>
<a
class="picker-icon-out"
href={pack.url}
target="_blank"
rel="noreferrer"
aria-label={`Open the ${pack.name} site in a new tab`}
>
<svg viewBox="0 0 20 20" aria-hidden="true">
<path d="M11 4.5h4.5V9M15.5 4.5 9.5 10.5M13 11.5v3.5a.5.5 0 0 1-.5.5h-7a.5.5 0 0 1-.5-.5v-7a.5.5 0 0 1 .5-.5H9"></path>
</svg>
</a>
</div>
))}
</fieldset>
<div class="picker-type-scroll">
<button class="picker-icon-button" type="button" data-list-scroll="-1" aria-label="Scroll icon sets up">
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="m4.5 12.5 5.5-5 5.5 5"></path></svg>
</button>
<button class="picker-icon-button" type="button" data-list-scroll="1" aria-label="Scroll icon sets down">
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="m4.5 7.5 5.5 5 5.5-5"></path></svg>
</button>
</div>
</div>
<div class="picker-icon-sheet" data-icon-sheet aria-hidden="true">
<div class="picker-icon-field" data-icon-field></div>
<div class="picker-icon-strip">
<span class="picker-icon-strip-label">At 16px</span>
<div class="picker-icon-strip-row" data-icon-strip></div>
</div>
</div>
</div>
<input type="hidden" name="icon-pack-name" />
<input type="hidden" name="icon-pack-license" />
<input type="hidden" name="icon-pack-url" />
<div class="picker-actions-stack">
<button class="ks-button ks-button-primary" type="button" data-advance="next">
Select this set
<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>
<button class="picker-back" type="button" data-advance="prev">Back</button>
</div>
</div>
</div>
</section>
<section class="picker-screen" data-screen="12" data-step="Review" aria-hidden="true" aria-labelledby="picker-finish-copy">
<div class="picker-container">
<div class="picker-placeholder picker-finish">
<h2 id="picker-finish-copy" class="picker-title">Building your design context</h2>
<div class="picker-finish-loader" data-doc-loader aria-hidden="true"><i></i></div>
<div class="picker-actions-stack" data-doc-error hidden>
<p class="picker-finish-error">Your answers could not be saved &mdash; the picker server may have closed.</p>
<button class="ks-button ks-button-primary" type="button" data-doc-retry>
Try again
<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>
<button class="picker-back" type="button" data-advance="prev">Back</button>
</div>
</div>
</div>
</section>
<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>
</div>
<p class="picker-progress-label">
<span class="picker-progress-index"><b data-progress-index>1</b> / <span data-progress-total>12</span></span>
<span class="picker-progress-name" data-progress-name></span>
</p>
</div>
</form>
</main>
{/* Design context document — hidden until the review screen saves the
answers. Mosaic, morph, and article vocabulary ported unchanged from
docs/design-context-categorization/design-context.html; the detail
templates start empty and are filled from the interview by
scripts/design-context.js. */}
<section class="dcx-shell" data-dcx-shell hidden aria-label="Design context categories">
<div class="dcx-grid">
<div class="dcx-badge" data-reveal style="--reveal-delay: 0.45s" aria-hidden="true">
<svg viewBox="0 0 24 24" focusable="false">
<path d="M5 2.5 L13.5 2.5 L5.5 21.5 L5 21.5 Q2.5 21.5 2.5 19 L2.5 5 Q2.5 2.5 5 2.5 Z"></path>
<path d="M16.5 2.5 L19 2.5 Q21.5 2.5 21.5 5 L21.5 19 Q21.5 21.5 19 21.5 L8.5 21.5 Z"></path>
</svg>
</div>
<button class="dcx-tile dcx-tile--audience" type="button" data-category="audience" data-name="Audience" data-reveal style="--reveal-delay: 0s" aria-label="Open Audience">
<span class="dcx-tile-viz" aria-hidden="true">
<svg viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg" class="dcx-viz-svg">
<circle cx="15" cy="14" r="4" stroke="var(--viz-ink)" class="vz-stroke" vector-effect="non-scaling-stroke"></circle>
<path d="M7 30c0-5 3.6-8 8-8s8 3 8 8" stroke="var(--viz-ink)" stroke-linecap="round" class="vz-stroke" vector-effect="non-scaling-stroke"></path>
<g class="anim-join">
<circle cx="28" cy="16" r="3.4" stroke="var(--viz-accent)" class="vz-stroke" vector-effect="non-scaling-stroke"></circle>
<path d="M21.5 30c0-4.4 3-7 6.5-7s6.5 2.6 6.5 7" stroke="var(--viz-accent)" stroke-linecap="round" class="anim-draw vz-stroke" vector-effect="non-scaling-stroke"></path>
</g>
</svg>
</span>
<h2 class="dcx-tile-title">Audience</h2>
</button>
<button class="dcx-tile dcx-tile--product" type="button" data-category="product" data-name="Product" data-reveal style="--reveal-delay: 0.05s" aria-label="Open Product">
<span class="dcx-tile-viz" aria-hidden="true">
<svg viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg" class="dcx-viz-svg">
<line x1="8" y1="13" x2="26" y2="13" stroke="var(--viz-ink)" stroke-linecap="round" class="vz-stroke" vector-effect="non-scaling-stroke"></line>
<line x1="8" y1="19" x2="30" y2="19" stroke="var(--viz-mist)" stroke-linecap="round" class="vz-stroke" vector-effect="non-scaling-stroke"></line>
<line x1="8" y1="25" x2="22" y2="25" stroke="var(--viz-accent)" stroke-linecap="round" class="anim-join vz-stroke" vector-effect="non-scaling-stroke"></line>
<line x1="26" y1="23" x2="26" y2="27" stroke="var(--viz-accent)" class="anim-blink anim-join vz-stroke" vector-effect="non-scaling-stroke"></line>
</svg>
</span>
<h2 class="dcx-tile-title">Product</h2>
</button>
<button class="dcx-tile dcx-tile--brand" type="button" data-category="brand" data-name="Brand" data-reveal style="--reveal-delay: 0.1s" aria-label="Open Brand">
<span class="dcx-tile-viz" aria-hidden="true">
<svg viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg" class="dcx-viz-svg">
<path d="M6 17 L34 17" stroke="var(--viz-mist)" class="vz-ghost" vector-effect="non-scaling-stroke"></path>
<path d="M12 10 L16 17 L20 33 M28 10 L24 17 L20 33" stroke="var(--viz-ink)" stroke-linejoin="round" class="vz-stroke" vector-effect="non-scaling-stroke"></path>
<path d="M12 10 L28 10 L34 17 L20 33 L6 17 Z" stroke="var(--viz-accent)" stroke-linejoin="round" class="anim-draw anim-join vz-stroke" vector-effect="non-scaling-stroke"></path>
<circle cx="20" cy="13.5" r="1.4" fill="var(--viz-accent)" class="anim-join"></circle>
</svg>
</span>
<h2 class="dcx-tile-title">Brand</h2>
</button>
<button class="dcx-tile dcx-tile--color" type="button" data-category="color" data-name="Color" data-reveal style="--reveal-delay: 0.15s" aria-label="Open Color">
<span class="dcx-tile-viz" aria-hidden="true">
<svg viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg" class="dcx-viz-svg">
<circle cx="16" cy="20" r="8" stroke="var(--viz-ink)" class="anim-move-x vz-stroke" vector-effect="non-scaling-stroke"></circle>
<circle cx="24" cy="20" r="8" stroke="var(--viz-accent)" class="anim-move-x-opp anim-join vz-stroke" vector-effect="non-scaling-stroke"></circle>
<path d="M20 14.5C21.5 16 22.5 18 22.5 20C22.5 22 21.5 24 20 25.5C18.5 24 17.5 22 17.5 20C17.5 18 18.5 16 20 14.5Z" fill="var(--viz-accent)" class="anim-fade-in"></path>
</svg>
</span>
<h2 class="dcx-tile-title">Color</h2>
</button>
<button class="dcx-tile dcx-tile--typography" type="button" data-category="typography" data-name="Typography" data-reveal style="--reveal-delay: 0.2s" aria-label="Open Typography">
<span class="dcx-tile-viz" aria-hidden="true">
<svg viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg" class="dcx-viz-svg">
<path d="M10 30L20 10L30 30" stroke="var(--viz-ink)" stroke-linecap="round" stroke-linejoin="round" class="vz-stroke" vector-effect="non-scaling-stroke"></path>
<path d="M10 30L20 10L30 30" stroke="var(--viz-accent)" stroke-linecap="round" stroke-linejoin="round" class="anim-draw anim-join vz-stroke" vector-effect="non-scaling-stroke"></path>
<path d="M15 22H25" stroke="var(--viz-accent)" stroke-linecap="round" class="anim-draw-delay anim-join vz-stroke" vector-effect="non-scaling-stroke"></path>
</svg>
</span>
<h2 class="dcx-tile-title">Typography</h2>
</button>
<button class="dcx-tile dcx-tile--iconography" type="button" data-category="iconography" data-name="Iconography" data-reveal style="--reveal-delay: 0.25s" aria-label="Open Iconography">
<span class="dcx-tile-viz" aria-hidden="true">
<svg viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg" class="dcx-viz-svg">
<circle cx="14" cy="14" r="4.5" stroke="var(--viz-ink)" class="anim-pop anim-pop-1 vz-stroke" vector-effect="non-scaling-stroke"></circle>
<rect x="22" y="9.5" width="9" height="9" rx="1.5" stroke="var(--viz-ink)" class="anim-pop anim-pop-2 vz-stroke" vector-effect="non-scaling-stroke"></rect>
<path d="M14 22.5L18.5 30.5H9.5L14 22.5Z" stroke="var(--viz-ink)" stroke-linejoin="round" class="anim-pop anim-pop-3 vz-stroke" vector-effect="non-scaling-stroke"></path>
<path d="M26.5 22.5V30.5M22.5 26.5H30.5" stroke="var(--viz-accent)" stroke-linecap="round" class="anim-pop anim-pop-4 vz-stroke" vector-effect="non-scaling-stroke"></path>
</svg>
</span>
<h2 class="dcx-tile-title">Iconography</h2>
</button>
<button class="dcx-tile dcx-tile--material" type="button" data-category="material" data-name="Material" data-reveal style="--reveal-delay: 0.3s" aria-label="Open Material">
<span class="dcx-tile-viz" aria-hidden="true">
<svg viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg" class="dcx-viz-svg">
<rect x="5" y="10" width="30" height="18.5" stroke="var(--viz-ink)" class="vz-stroke" vector-effect="non-scaling-stroke"></rect>
<line x1="23.5" y1="10" x2="23.5" y2="28.5" stroke="var(--viz-ink)" class="vz-stroke" vector-effect="non-scaling-stroke"></line>
<line x1="23.5" y1="21.5" x2="35" y2="21.5" stroke="var(--viz-ink)" class="vz-stroke" vector-effect="non-scaling-stroke"></line>
<path d="M5 28.5C5 18.28 13.28 10 23.5 10C29.85 10 35 15.15 35 21.5C35 25.42 31.82 28.5 27.9 28.5" fill="none" stroke="var(--viz-accent)" stroke-linecap="round" class="anim-draw anim-join vz-stroke" vector-effect="non-scaling-stroke"></path>
</svg>
</span>
<h2 class="dcx-tile-title">Material</h2>
</button>
<button class="dcx-tile dcx-tile--interface" type="button" data-category="interface" data-name="Interface" data-reveal style="--reveal-delay: 0.35s" aria-label="Open Interface">
<span class="dcx-tile-viz" aria-hidden="true">
<svg viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg" class="dcx-viz-svg">
<rect x="10" y="14" width="20" height="12" rx="6" stroke="var(--viz-ink)" class="vz-stroke" vector-effect="non-scaling-stroke"></rect>
<circle cx="16" cy="20" r="4" fill="var(--viz-mist)" class="anim-toggle-move"></circle>
</svg>
</span>
<h2 class="dcx-tile-title">Interface</h2>
</button>
</div>
</section>
{/* Detail articles: empty shells filled per run by design-context.js. */}
<template id="dcx-detail-audience"></template>
<template id="dcx-detail-product"></template>
<template id="dcx-detail-brand"></template>
<template id="dcx-detail-color"></template>
<template id="dcx-detail-typography"></template>
<template id="dcx-detail-iconography"></template>
<template id="dcx-detail-material"></template>
<template id="dcx-detail-interface"></template>
{/* Expander shell: sidebar + topbar + main, cloned per open. */}
<template id="dcx-shell-template">
<div class="dcx-expander-inner">
<aside class="dcx-sidebar">
<a class="dcx-sidebar-brand" href="https://impeccable.style/" target="_blank" rel="noopener" aria-label="Impeccable home (opens the live site)">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M5 2.5 L13.5 2.5 L5.5 21.5 L5 21.5 Q2.5 21.5 2.5 19 L2.5 5 Q2.5 2.5 5 2.5 Z"></path>
<path d="M16.5 2.5 L19 2.5 Q21.5 2.5 21.5 5 L21.5 19 Q21.5 21.5 19 21.5 L8.5 21.5 Z"></path>
</svg>
<span class="dcx-sidebar-brand-name">Impeccable</span>
</a>
<nav class="dcx-nav" aria-label="Design context categories">
<span class="dcx-nav-label">Design context</span>
<ul class="dcx-nav-list">
<li data-category="audience"><a class="dcx-nav-link" href="#audience" data-dcx-nav="audience">Audience</a><div class="dcx-subnav"></div></li>
<li data-category="product"><a class="dcx-nav-link" href="#product" data-dcx-nav="product">Product</a><div class="dcx-subnav"></div></li>
<li data-category="brand"><a class="dcx-nav-link" href="#brand" data-dcx-nav="brand">Brand</a><div class="dcx-subnav"></div></li>
<li data-category="color"><a class="dcx-nav-link" href="#color" data-dcx-nav="color">Color</a><div class="dcx-subnav"></div></li>
<li data-category="typography"><a class="dcx-nav-link" href="#typography" data-dcx-nav="typography">Typography</a><div class="dcx-subnav"></div></li>
<li data-category="iconography"><a class="dcx-nav-link" href="#iconography" data-dcx-nav="iconography">Iconography</a><div class="dcx-subnav"></div></li>
<li data-category="material"><a class="dcx-nav-link" href="#material" data-dcx-nav="material">Material</a><div class="dcx-subnav"></div></li>
<li data-category="interface"><a class="dcx-nav-link" href="#interface" data-dcx-nav="interface">Interface</a><div class="dcx-subnav"></div></li>
</ul>
</nav>
</aside>
<div class="dcx-panel">
<header class="dcx-topbar">
<div class="dcx-current"></div>
<button class="dcx-request" type="button" data-dcx-request>Request a change</button>
<button class="dcx-close" type="button" aria-label="Close"></button>
</header>
<div class="dcx-main" tabindex="-1"></div>
</div>
</div>
</template>
{/* Edit-request modal: complex changes the document cannot apply itself
are written up here and queued for the agent over the doc session. */}
<dialog class="picker-modal dcx-request-modal" data-dcx-request-modal aria-labelledby="dcx-request-title">
<div class="picker-modal-inner">
<header class="picker-modal-head">
<h2 id="dcx-request-title">Request a change</h2>
<p data-dcx-request-lede>Describe the change; the agent applies it to the design document and the page updates here.</p>
</header>
<fieldset class="picker-modal-field">
<legend data-dcx-request-scope>Change</legend>
<label class="picker-modal-label" for="dcx-request-prompt">What should change?</label>
<textarea class="picker-modal-input dcx-request-prompt" id="dcx-request-prompt" rows="4" maxlength="4000"
placeholder="e.g. Swap the heading font for something with more contrast at display sizes."></textarea>
<div class="picker-modal-file-block" data-dcx-request-fonts hidden>
<label class="picker-modal-label" for="dcx-request-font-files">Font files (optional)</label>
<input class="picker-modal-file-input" id="dcx-request-font-files" type="file" multiple
accept=".woff2,.woff,.ttf,.otf" data-dcx-request-files />
<p class="picker-modal-or" data-dcx-request-upload-note></p>
</div>
</fieldset>
<footer class="picker-modal-actions">
<button class="picker-back" type="button" data-dcx-request-cancel>Cancel</button>
<button class="ks-button ks-button-primary" type="button" data-dcx-request-send>Send to the agent</button>
</footer>
</div>
</dialog>
{/* Live edit tray: every queued request with its status, fed by polling. */}
<div class="dcx-tray" data-dcx-tray hidden aria-live="polite"></div>
<dialog class="picker-modal" data-font-modal aria-labelledby="picker-font-modal-title">
<div class="picker-modal-inner">
<header class="picker-modal-head">
<h2 id="picker-font-modal-title">Add a custom font</h2>
<p>Link a hosted stylesheet or upload a face. Nothing is checked until you finish.</p>
</header>
{[['heading', 'Heading'], ['body', 'Body']].map(([role, label]) => (
<fieldset class="picker-modal-field">
<legend>{label}</legend>
<label class="picker-modal-label" for={`custom-${role}-url`}>Stylesheet or font URL</label>
<input
class="picker-modal-input"
id={`custom-${role}-url`}
type="url"
inputmode="url"
placeholder="https://example.com/typeface.css"
data-custom-url={role}
/>
<p class="picker-modal-or">or</p>
<div class="picker-modal-file-block">
<div class="picker-modal-file">
<input
class="picker-modal-file-input"
id={`custom-${role}-file`}
type="file"
accept=".woff2,.woff,.ttf,.otf,font/woff2,font/woff,font/ttf,font/otf,application/font-woff,application/font-woff2,application/x-font-ttf,application/x-font-otf"
multiple
data-custom-file={role}
/>
<label class="picker-modal-file-button" for={`custom-${role}-file`}>Choose font files</label>
</div>
<ul class="picker-modal-file-list" data-custom-file-list={role} hidden></ul>
</div>
</fieldset>
))}
<p class="picker-modal-status" data-custom-status aria-live="polite"></p>
<p class="picker-modal-hint" id="picker-font-modal-hint" data-custom-hint aria-live="polite"></p>
<div class="picker-modal-actions">
<button class="picker-back" type="button" data-font-custom-close>Cancel</button>
<button
class="ks-button ks-button-secondary"
type="button"
data-font-custom-save
aria-describedby="picker-font-modal-hint"
disabled
>Use these fonts</button>
</div>
</div>
</dialog>
<aside class="picker-width-gate" aria-labelledby="picker-width-title">
<div>
<h2 id="picker-width-title">This window is too narrow.</h2>
<p>Open this link in your browser at least 1200px wide.</p>
<div class="picker-copy-link">
<code data-copy-url-value aria-label="Picker URL"></code>
<button class="picker-copy-link-button" type="button" aria-label="Copy link" data-copy-url>
<svg class="picker-copy-link-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" aria-hidden="true">
<rect x="9" y="9" width="13" height="13" rx="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
<svg class="picker-copy-link-check" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M20 6 9 17l-5-5"></path>
</svg>
<span>Copy link</span>
</button>
</div>
<p class="picker-copy-status" data-copy-status aria-live="polite">Paste it into your browser to continue.</p>
</div>
</aside>
<script>
import '../scripts/palette-picker.js';
import '../scripts/design-context.js';
const $ = (selector, root = document) => root.querySelector(selector);
const form = $('#picker-form');
const copyButton = $('[data-copy-url]');
const status = $('[data-copy-status]');
const screens = [...form.querySelectorAll('.picker-screen')];
let current = screens.findIndex((screen) => screen.hasAttribute('data-active'));
const url = new URL(location.href);
url.hostname = 'localhost';
const link = url.href.replace(/\/$/, '');
$('[data-copy-url-value]').textContent = link;
const controls = () => [...screens[current].querySelectorAll(
'button:not([disabled]), input:not([disabled]):not([type="hidden"]):not([tabindex="-1"]), select:not([disabled]), textarea:not([disabled]), a[href]',
)];
// Neither the start screen nor the review is a question, so the stepper
// counts from the surfaces screen and fills on the last one, icons.
// Review holds it full rather than counting past the end.
//
// A question no chosen surface takes is not a step either, so the count and
// the row of ticks come from the screens this run actually asks rather than
// from the markup: a docs-only run skips motion and layout, so it is nine
// questions long and says so.
const progress = $('.picker-progress');
const track = $('.picker-progress-track');
const steps = () => screens.filter((screen, index) => (
index > 0 && index < screens.length - 1 && !screen.hasAttribute('data-skip')
));
const setProgress = (target) => {
const list = steps();
const total = list.length;
const at = list.indexOf(target);
const step = at >= 0 ? at + 1 : (target === screens[0] ? 1 : total);
progress.dataset.step = String(step);
progress.style.setProperty('--pk-steps', String(total));
[...track.children].forEach((tick, index) => tick.toggleAttribute('data-off', index >= total));
$('[data-progress-index]').textContent = String(step);
$('[data-progress-total]').textContent = String(total);
$('[data-progress-name]').textContent = target.dataset.step || '';
};
// The surfaces, palette, and strategy screens are the one stretch of the run
// that shows the same objects more than once, so they are where a change is
// worth animating: the test page travels from the leading surface tile into
// the palette panel and on to the strategy stage, and the four colors travel
// with it over that second step, instead of each being redrawn. Everywhere
// else there is nothing to carry, and capturing a screen that is
// mid-animation reads worse than a plain swap does. Only adjacent screens
// are ever swapped, so listing the three ids names exactly the two pairs.
const MORPH = new Set(['01b', '02', '03']);
const morphs = (from, to) => (
typeof document.startViewTransition === 'function'
&& !matchMedia('(prefers-reduced-motion: reduce)').matches
&& MORPH.has(from?.dataset.screen)
&& MORPH.has(to?.dataset.screen)
);
/* A question the run does not ask is not a screen the run has, so the step
carries on in the direction it was already travelling. That is what makes
Back from the screen after a skipped one land on the screen before it
rather than on the one nobody was shown. */
const goTo = (index, step = 1) => {
let at = index;
while (screens[at]?.hasAttribute('data-skip')) at += step;
const target = screens[at];
if (!target) return;
if (morphs(screens[current], target)) {
document.startViewTransition(() => swap(at));
return;
}
swap(at);
};
const swap = (index) => {
const target = screens[index];
screens.forEach((screen, screenIndex) => {
const active = screenIndex === index;
screen.toggleAttribute('data-active', active);
if (active) screen.removeAttribute('aria-hidden');
else screen.setAttribute('aria-hidden', 'true');
});
current = index;
const id = target.dataset.screen;
form.dataset.current = id;
setProgress(target);
document.dispatchEvent(new CustomEvent('picker:screenchange', {
detail: { screen: id },
}));
controls()[0]?.focus();
};
/* Motion, boundaries, corners, depth, and icons are reachable only
from the configure hub, so both their CTA and their Back return
there rather than to the DOM neighbor. Type scale (05) and layout
(07) sit on the linear path ahead of the hub and follow DOM order.
The hub itself and the review screen keep the ordinary rule: Back
on the hub is the layout screen (or type scale when layout is
skipped), Back on the review's error box is the icons screen. */
const HUB_SCREEN = '04b';
const HUB_RETURNS = new Set(['06', '08', '09', '10', '11']);
const screenIndex = (id) => screens.findIndex((screen) => screen.dataset.screen === id);
form.onclick = (e) => {
const control = e.target.closest('[data-advance]');
if (!control) return;
if (HUB_RETURNS.has(screens[current].dataset.screen)) {
goTo(screenIndex(HUB_SCREEN));
return;
}
const step = control.dataset.advance === 'prev' ? -1 : 1;
goTo(current + step, step);
};
/* The hub's cards and its finish CTA jump by screen id. goTo() keeps
its skip rule, so a jump aimed at a skipped screen walks off it. */
document.addEventListener('picker:goto', (e) => {
const index = screenIndex(e.detail.screen);
if (index !== -1) goTo(index);
});
document.addEventListener('keydown', (e) => {
if (e.defaultPrevented) return;
if (e.target instanceof Element && e.target.closest('input, textarea, select, [role="slider"]')) return;
if (e.key === 'Enter') {
if (current === 0 && matchMedia('(max-width: 1199px)').matches) return;
const control = current === 0 ? $('[data-advance="next"]', screens[0]) : document.activeElement;
if (controls().includes(control)) {
e.preventDefault();
control.click();
}
return;
}
const d = { ArrowUp: -1, ArrowLeft: -1, ArrowDown: 1, ArrowRight: 1 };
if (!(e.key in d)) return;
e.preventDefault();
const items = controls();
const focused = items.indexOf(document.activeElement);
const index = focused < 0 ? 0 : (focused + d[e.key] + items.length) % items.length;
items[index]?.focus();
});
// Submission belongs to the review screen now: arriving on it triggers
// design-context.js's finish sequence (save, then reveal the design
// context document), so a stray native submit has nothing left to do.
form.onsubmit = (e) => e.preventDefault();
copyButton.onclick = async () => {
try {
await navigator.clipboard.writeText(link);
copyButton.classList.add('is-copied');
copyButton.setAttribute('aria-label', 'Link copied');
status.textContent = 'Copied. Paste it into your browser to continue.';
setTimeout(() => {
copyButton.classList.remove('is-copied');
copyButton.setAttribute('aria-label', 'Copy link');
}, 1200);
} catch {
status.textContent = 'Copy failed. Select the link above and copy it instead.';
}
};
form.dataset.current = screens[current].dataset.screen;
setProgress(screens[current]);
</script>
</Picker>