Your visual direction is ready.
@@ -584,10 +614,10 @@ const scaleRows = [['H1', 6], ['H2', 5], ['H3', 4], ['H4', 3], ['H5', 2], ['H6',
diff --git a/picker/scripts/palette-picker.js b/picker/scripts/palette-picker.js
index 7057c1688..719899324 100644
--- a/picker/scripts/palette-picker.js
+++ b/picker/scripts/palette-picker.js
@@ -13,7 +13,6 @@ const hint = $('[data-palette-hint]');
const ringGuide = $('[data-ring-guide]');
const loupe = $('[data-loupe]');
const preview = $('.picker-preview');
-const strategyPreview = document.querySelector('[data-strategy-preview]');
const typePreview = document.querySelector('[data-type-preview]');
const fontOptions = document.querySelector('[data-font-options]');
const pairTemplate = document.querySelector('[data-pair-card]');
@@ -1032,10 +1031,13 @@ scroller.addEventListener('scroll', () => {
}, { passive: true });
document.addEventListener('picker:screenchange', (event) => {
activate(event.detail.screen === '02');
- // The scale sheet is deliberately absent: it is picker chrome in the
- // picker's own theme, not a third artboard painted in the user's palette.
- const target = { '03': strategyPreview, '04': typePreview }[event.detail.screen];
- if (target) syncCommittedPalette(target);
+ // Every artboard on the screen being shown, whatever question it belongs to,
+ // gets the committed palette. The scale sheet is deliberately not one: it is
+ // picker chrome in the picker's own theme, not a page in the user's palette.
+ const screenNode = document.querySelector(`.picker-screen[data-screen="${event.detail.screen}"]`);
+ for (const artboard of screenNode?.querySelectorAll('[data-artboard]') ?? []) {
+ syncCommittedPalette(artboard);
+ }
// Arriving is the quietest moment there is, so the rail settles here even
// if it is already in order: the chosen pair is the row you land on.
if (event.detail.screen === '04') {
diff --git a/picker/styles/picker.css b/picker/styles/picker.css
index bd9c695d8..689682122 100644
--- a/picker/styles/picker.css
+++ b/picker/styles/picker.css
@@ -272,10 +272,18 @@ body.picker-page {
line-height: 1.5;
}
+/* 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="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,
-.picker-shell:has(#picker-form[data-current="05"]) .picker-hero-art {
+.picker-shell:has(#picker-form[data-current="05"]) .picker-hero-art,
+.picker-shell:has(#picker-form[data-current="06"]) .picker-hero-art,
+.picker-shell:has(#picker-form[data-current="07"]) .picker-hero-art,
+.picker-shell:has(#picker-form[data-current="08"]) .picker-hero-art,
+.picker-shell:has(#picker-form[data-current="09"]) .picker-hero-art,
+.picker-shell:has(#picker-form[data-current="10"]) .picker-hero-art {
opacity: 0;
}
@@ -291,10 +299,14 @@ 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. */
.picker-progress-track {
+ width: 232px;
display: grid;
- grid-template-columns: repeat(5, 34px);
- gap: 6px;
+ grid-template-columns: repeat(10, minmax(0, 1fr));
+ gap: 5px;
}
.picker-progress-track i {
@@ -307,7 +319,12 @@ body.picker-page {
.picker-progress[data-step="2"] .picker-progress-track i:nth-child(-n + 2),
.picker-progress[data-step="3"] .picker-progress-track i:nth-child(-n + 3),
.picker-progress[data-step="4"] .picker-progress-track i:nth-child(-n + 4),
-.picker-progress[data-step="5"] .picker-progress-track i {
+.picker-progress[data-step="5"] .picker-progress-track i:nth-child(-n + 5),
+.picker-progress[data-step="6"] .picker-progress-track i:nth-child(-n + 6),
+.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 {
background: var(--ks-kinpaku);
}
@@ -1354,7 +1371,7 @@ body.picker-page {
.picker-strategy-choices.picker-type-options {
height: 100%;
align-content: stretch;
- grid-template-rows: repeat(4, minmax(0, 1fr));
+ grid-template-rows: repeat(var(--pk-choices, 4), minmax(0, 1fr));
}
.picker-strategy-choices.picker-type-options .picker-strategy-option {
@@ -1381,16 +1398,18 @@ body.picker-page {
max-width: 34ch;
}
-/* Screen 03 shows one editorial landing page at desktop and phone sizes.
- Geometry stays fixed while the strategy radios remap its color slots.
- Screen 04's artboards read the same slots, so the strategy travels there
- with the palette instead of being re-decided.
+/* The wireframe landing page every option screen previews its answer on, at
+ desktop and phone sizes. Each screen remaps the slots its own question owns:
+ color on 03, glyphs on 04, then motion, grid, dividers, radius, and shadow.
+ Answers travel forward, so by the last screen the artboard is showing every
+ decision made so far rather than one at a time.
- Two of the slots name an ink rather than a fill: a filled button's label
- has to come from the fill it sits on, and which fill that is changes with
- the strategy. --pv-t-ink is set alongside the palette by the picker script;
- the fallback here matches the gold that stands in before a palette lands. */
-.picker-strategy-preview {
+ Two of the color slots name an ink rather than a fill: a filled button's
+ label has to come from the fill it sits on, and which fill that is changes
+ with the strategy. --pv-t-ink is set alongside the palette by the picker
+ script; the fallback here matches the gold that stands in before a palette
+ lands. */
+.picker-artboard {
--pv-primary: var(--ks-champagne);
--pv-secondary: var(--ks-patina);
--pv-tertiary: var(--ks-kinpaku);
@@ -1447,6 +1466,58 @@ body.picker-page {
--pvs-accent-c: var(--pv-primary);
--pvs-accent-d: var(--pv-bone);
--ps-proof-gap: 25px;
+
+ /* Geometry slots. Every default below is the value the artboard already had,
+ so screens 03 and 04 are unchanged by their existence. A later screen
+ answers its question by rewriting a handful of them.
+
+ Radius is four slots rather than one because a wireframe bar standing in
+ for a line of text must never round like a button: at pill radius it stops
+ reading as text and starts reading as a tag. So controls, surfaces, and
+ text bars move independently, and the dot stays a dot unless a screen
+ says otherwise. */
+ --pvs-radius-frame: 4px;
+ --pvs-radius-frame-phone: 5px;
+ --pvs-radius-surface: 2px;
+ --pvs-radius-control: 2px;
+ --pvs-radius-bar: 2px;
+ --pvs-radius-dot: 50%;
+
+ /* Section separation. The frame is the browser window and stays put; the
+ dividers are the page's own, and are what a boundary answer removes. */
+ --pvs-frame-w: 1px;
+ --pvs-frame-c: var(--pvs-rule);
+ --pvs-divider-w: 1px;
+ --pvs-divider-c: var(--pvs-rule);
+
+ /* Section grounds and element containers. Transparent by default, which is
+ why they cost nothing until a screen fills them. */
+ --pvs-band: transparent;
+ --pvs-band-alt: transparent;
+ --pvs-panel: transparent;
+ --pvs-panel-edge: transparent;
+ --pvs-panel-edge-w: 0px;
+ --pvs-panel-pad: 0px;
+
+ --pvs-shadow-card: none;
+ --pvs-shadow-control: none;
+ --pvs-shadow-surface: none;
+ --pvs-shadow-chrome: none;
+
+ /* Page grid. Gutters are percentages of the artboard, so they hold at both
+ sizes and at any zoom. */
+ --pvs-gutter: 5.5%;
+ --pvs-gutter-end: 3.2%;
+ --pvs-rows: 9.4% 50.6% 9% 22.7% 8.3%;
+ --pvs-nav-cols: auto 1fr auto;
+ --pvs-hero-cols: 35.8% minmax(0, 1fr);
+ --pvs-hero-gap: 11.3%;
+ --pvs-editorial-cols: 27.9% minmax(0, 1fr);
+ --pvs-editorial-gap: 6%;
+ --pvs-gallery-cols: 1.12fr 0.9fr 0.9fr 0.9fr;
+ --pvs-gallery-gap: 3%;
+ --pvs-section-align: center;
+
width: 100%;
aspect-ratio: 1.865 / 1;
display: grid;
@@ -1454,19 +1525,33 @@ body.picker-page {
gap: 5.25%;
}
+/* One artboard filling the frame, for a screen whose answer only reads at
+ desktop width. It keeps the paired frame's ratio rather than taking a
+ taller one of its own: the preview then occupies the same block on every
+ screen, and the CTA underneath does not move as you page through. */
+.picker-artboard--solo {
+ aspect-ratio: 1.865 / 1;
+ grid-template-columns: minmax(0, 1fr);
+ gap: 0;
+}
+
/* Hovering an option previews its strategy before the click commits it.
While any strategy option is hovered, the hovered branch drives the
preview and the checked branch stands down; hovering Restrained needs
no block of its own because standing down lands on the defaults.
- The third selector in each block carries the committed answer onto screen
- 04's type artboards. Only the committed answer travels: a hover preview
- belongs to the screen whose options are under the cursor. Its :where()
- holds that selector at one class of specificity on purpose, so the
+ The last two selectors in each block carry the committed answer onto every
+ later screen's artboard, so a screen deciding motion or radius shows it on
+ the page the visitor actually chose. Only the committed answer travels: a
+ hover preview belongs to the screen whose options are under the cursor,
+ which is what the no-hover guard on the [data-artboard] selector is for.
+ Both hold themselves at one class of specificity on purpose, so the
+ artboard's own defaults are still beaten on source order while the
reading-contrast slots the type preview declares further down still win. */
#picker-form:has(.picker-strategy-option:hover input[name="color-strategy"][value="committed"]) .picker-strategy-preview:not(.picker-preview-type),
#picker-form:not(:has(.picker-strategy-option:hover input[name="color-strategy"])):has(input[name="color-strategy"][value="committed"]:checked) .picker-strategy-preview:not(.picker-preview-type),
-:where(#picker-form:has(input[name="color-strategy"][value="committed"]:checked)) .picker-preview-type {
+:where(#picker-form:has(input[name="color-strategy"][value="committed"]:checked)) .picker-preview-type,
+:where(#picker-form:not(:has(.picker-strategy-option:hover input[name="color-strategy"])):has(input[name="color-strategy"][value="committed"]:checked)) [data-artboard] {
--pvs-headline: color-mix(in oklab, var(--pv-primary) 80%, var(--pv-strong));
--pvs-headline-text: var(--pv-p-on-n);
--pvs-eyebrow: var(--pv-primary);
@@ -1487,7 +1572,8 @@ body.picker-page {
signal, and accents; neutral stays the ground. */
#picker-form:has(.picker-strategy-option:hover input[name="color-strategy"][value="full-palette"]) .picker-strategy-preview:not(.picker-preview-type),
#picker-form:not(:has(.picker-strategy-option:hover input[name="color-strategy"])):has(input[name="color-strategy"][value="full-palette"]:checked) .picker-strategy-preview:not(.picker-preview-type),
-:where(#picker-form:has(input[name="color-strategy"][value="full-palette"]:checked)) .picker-preview-type {
+:where(#picker-form:has(input[name="color-strategy"][value="full-palette"]:checked)) .picker-preview-type,
+:where(#picker-form:not(:has(.picker-strategy-option:hover input[name="color-strategy"])):has(input[name="color-strategy"][value="full-palette"]:checked)) [data-artboard] {
--pvs-headline: color-mix(in oklab, var(--pv-primary) 80%, var(--pv-strong));
--pvs-headline-text: var(--pv-p-on-n);
--pvs-accent-text: var(--pv-t-on-n);
@@ -1512,7 +1598,8 @@ body.picker-page {
work; nothing is left bare white and the hairlines tint with the ground. */
#picker-form:has(.picker-strategy-option:hover input[name="color-strategy"][value="drenched"]) .picker-strategy-preview:not(.picker-preview-type),
#picker-form:not(:has(.picker-strategy-option:hover input[name="color-strategy"])):has(input[name="color-strategy"][value="drenched"]:checked) .picker-strategy-preview:not(.picker-preview-type),
-:where(#picker-form:has(input[name="color-strategy"][value="drenched"]:checked)) .picker-preview-type {
+:where(#picker-form:has(input[name="color-strategy"][value="drenched"]:checked)) .picker-preview-type,
+:where(#picker-form:not(:has(.picker-strategy-option:hover input[name="color-strategy"])):has(input[name="color-strategy"][value="drenched"]:checked)) [data-artboard] {
--pvs-ground: var(--pv-primary);
--pvs-ink: var(--pv-p-ink);
--pvs-surface: color-mix(in oklab, var(--pv-p-ink) 10%, var(--pv-primary));
@@ -1553,13 +1640,13 @@ body.picker-page {
min-height: 0;
overflow: hidden;
background-color: var(--pvs-ground);
- border: 1px solid var(--pvs-rule);
+ border: var(--pvs-frame-w) solid var(--pvs-frame-c);
}
.ps-desktop {
display: grid;
- grid-template-rows: 9.4% 50.6% 9% 22.7% 8.3%;
- border-radius: 4px;
+ grid-template-rows: var(--pvs-rows);
+ border-radius: var(--pvs-radius-frame);
}
.ps-phone {
@@ -1568,16 +1655,18 @@ body.picker-page {
align-self: stretch;
display: grid;
grid-template-rows: 7.2% 1fr 7.5%;
- border-radius: 5px;
+ border-radius: var(--pvs-radius-frame-phone);
}
.ps-nav {
display: grid;
- grid-template-columns: auto 1fr auto;
+ grid-template-columns: var(--pvs-nav-cols);
gap: 24px;
align-items: center;
- padding: 0 5.5%;
- border-bottom: 1px solid var(--pvs-rule);
+ padding: 0 var(--pvs-gutter);
+ background-color: var(--pvs-band);
+ border-bottom: var(--pvs-divider-w) solid var(--pvs-divider-c);
+ box-shadow: var(--pvs-shadow-chrome);
}
.ps-brand-block {
@@ -1585,7 +1674,7 @@ body.picker-page {
height: auto;
aspect-ratio: 1;
background-color: var(--pvs-cta);
- border-radius: 50%;
+ border-radius: var(--pvs-radius-dot);
}
.ps-nav-bars {
@@ -1599,23 +1688,24 @@ body.picker-page {
width: clamp(18px, 7%, 34px);
height: 4px;
background-color: var(--pvs-bars);
- border-radius: 2px;
+ border-radius: var(--pvs-radius-bar);
}
.ps-nav-action {
width: clamp(34px, 3.4vw, 48px);
height: clamp(7px, 26%, 13px);
background-color: var(--pvs-signal);
- border-radius: 2px;
+ border-radius: var(--pvs-radius-control);
+ box-shadow: var(--pvs-shadow-control);
}
.ps-hero {
min-height: 0;
display: grid;
- grid-template-columns: 35.8% minmax(0, 1fr);
- gap: 11.3%;
- align-items: center;
- padding: 2% 3.2% 2% 5.5%;
+ grid-template-columns: var(--pvs-hero-cols);
+ gap: var(--pvs-hero-gap);
+ align-items: var(--pvs-section-align);
+ padding: 2% var(--pvs-gutter-end) 2% var(--pvs-gutter);
}
.ps-hero-copy {
@@ -1628,7 +1718,7 @@ body.picker-page {
height: 10px;
margin-bottom: 14px;
background-color: var(--pvs-eyebrow);
- border-radius: 2px;
+ border-radius: var(--pvs-radius-bar);
}
.ps-headline {
@@ -1642,7 +1732,7 @@ body.picker-page {
width: 100%;
height: 15px;
background-color: var(--pvs-headline);
- border-radius: 2px;
+ border-radius: var(--pvs-radius-bar);
}
.ps-headline i:last-child {
@@ -1660,7 +1750,7 @@ body.picker-page {
width: 64%;
height: 8px;
background-color: var(--pvs-copy);
- border-radius: 2px;
+ border-radius: var(--pvs-radius-bar);
}
.ps-copy i:last-child {
@@ -1677,7 +1767,8 @@ body.picker-page {
width: clamp(42px, 42%, 86px);
height: clamp(12px, 2.35vw, 29px);
background-color: var(--pvs-cta);
- border-radius: 2px;
+ border-radius: var(--pvs-radius-control);
+ box-shadow: var(--pvs-shadow-control);
}
.ps-actions i:last-child {
@@ -1693,7 +1784,8 @@ body.picker-page {
background-color: var(--pvs-image);
background-image: linear-gradient(135deg, var(--pvs-image-a), var(--pvs-image-b));
border: 1px solid var(--pvs-image-edge);
- border-radius: 2px;
+ border-radius: var(--pvs-radius-surface);
+ box-shadow: var(--pvs-shadow-surface);
}
.ps-image::after {
@@ -1704,7 +1796,7 @@ body.picker-page {
width: clamp(6px, 1.1vw, 10px);
aspect-ratio: 1;
background-color: var(--pvs-accent-d);
- border-radius: 50%;
+ border-radius: var(--pvs-radius-dot);
}
.ps-proof {
@@ -1713,7 +1805,8 @@ body.picker-page {
grid-template-columns: 1.05fr 1fr 1fr 1.15fr;
align-items: stretch;
column-gap: var(--ps-proof-gap);
- padding: 2% 3.2% 2% 5.5%;
+ padding: 2% var(--pvs-gutter-end) 2% var(--pvs-gutter);
+ background-color: var(--pvs-band-alt);
position: relative;
}
@@ -1721,10 +1814,10 @@ body.picker-page {
.ps-proof::after {
content: "";
position: absolute;
- left: 5.5%;
- right: 3.2%;
- height: 1px;
- background-color: var(--pvs-rule);
+ left: var(--pvs-gutter);
+ right: var(--pvs-gutter-end);
+ height: var(--pvs-divider-w);
+ background-color: var(--pvs-divider-c);
}
.ps-proof::before {
@@ -1745,7 +1838,7 @@ body.picker-page {
}
.ps-proof-item + .ps-proof-item {
- border-left: 1px solid var(--pvs-rule);
+ border-left: var(--pvs-divider-w) solid var(--pvs-divider-c);
}
.ps-proof-item i {
@@ -1753,10 +1846,10 @@ body.picker-page {
width: clamp(7px, 1.5vw, 13px);
aspect-ratio: 1;
background-color: var(--pvs-accent-a);
- border-radius: 50%;
+ border-radius: var(--pvs-radius-dot);
}
-.picker-strategy-preview:not(.picker-preview-type) .ps-proof {
+.picker-artboard:not(.picker-preview-type) .ps-proof {
grid-template-columns:
minmax(0, 1fr) auto
minmax(0, 1fr) auto
@@ -1764,29 +1857,29 @@ body.picker-page {
minmax(0, 1fr);
}
-.picker-strategy-preview:not(.picker-preview-type) .ps-proof-item + .ps-proof-item {
+.picker-artboard:not(.picker-preview-type) .ps-proof-item + .ps-proof-item {
border-left: 0;
}
-.picker-strategy-preview:not(.picker-preview-type) .ps-proof-divider {
- width: 1px;
+.picker-artboard:not(.picker-preview-type) .ps-proof-divider {
+ width: var(--pvs-divider-w);
align-self: stretch;
- background-color: var(--pvs-rule);
+ background-color: var(--pvs-divider-c);
}
-.picker-strategy-preview:not(.picker-preview-type) .ps-phone-body .ps-proof {
+.picker-artboard:not(.picker-preview-type) .ps-phone-body .ps-proof {
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
}
-.picker-strategy-preview:not(.picker-preview-type) .ps-proof-item:nth-of-type(2) i {
+.picker-artboard:not(.picker-preview-type) .ps-proof-item:nth-of-type(2) i {
background-color: var(--pvs-accent-b);
}
-.picker-strategy-preview:not(.picker-preview-type) .ps-proof-item:nth-of-type(3) i {
+.picker-artboard:not(.picker-preview-type) .ps-proof-item:nth-of-type(3) i {
background-color: var(--pvs-accent-c);
}
-.picker-strategy-preview:not(.picker-preview-type) .ps-proof-item:nth-of-type(4) i {
+.picker-artboard:not(.picker-preview-type) .ps-proof-item:nth-of-type(4) i {
background-color: var(--pvs-accent-d);
}
@@ -1796,16 +1889,16 @@ body.picker-page {
flex: 1;
height: 6px;
background-color: var(--pvs-bars);
- border-radius: 2px;
+ border-radius: var(--pvs-radius-bar);
}
.ps-editorial {
min-height: 0;
display: grid;
- grid-template-columns: 27.9% minmax(0, 1fr);
- gap: 6%;
- align-items: center;
- padding: 2.2% 5.5%;
+ grid-template-columns: var(--pvs-editorial-cols);
+ gap: var(--pvs-editorial-gap);
+ align-items: var(--pvs-section-align);
+ padding: 2.2% var(--pvs-gutter);
}
.ps-editorial-copy {
@@ -1817,7 +1910,7 @@ body.picker-page {
width: 78%;
height: 11px;
background-color: var(--pvs-title);
- border-radius: 2px;
+ border-radius: var(--pvs-radius-bar);
}
.ps-editorial-copy span {
@@ -1830,7 +1923,7 @@ body.picker-page {
width: 90%;
height: 6px;
background-color: var(--pvs-bars);
- border-radius: 2px;
+ border-radius: var(--pvs-radius-bar);
}
.ps-editorial-copy span i:last-child {
@@ -1842,15 +1935,15 @@ body.picker-page {
height: 6px;
margin-top: 3px;
background-color: var(--pvs-signal);
- border-radius: 2px;
+ border-radius: var(--pvs-radius-bar);
}
.ps-gallery {
min-width: 0;
height: 100%;
display: grid;
- grid-template-columns: 1.12fr 0.9fr 0.9fr 0.9fr;
- gap: 3%;
+ grid-template-columns: var(--pvs-gallery-cols);
+ gap: var(--pvs-gallery-gap);
}
.ps-gallery-item {
@@ -1858,6 +1951,11 @@ body.picker-page {
display: grid;
grid-template-rows: minmax(0, 1fr) auto;
gap: 12px;
+ padding: var(--pvs-panel-pad);
+ background-color: var(--pvs-panel);
+ border: var(--pvs-panel-edge-w) solid var(--pvs-panel-edge);
+ border-radius: var(--pvs-radius-surface);
+ box-shadow: var(--pvs-shadow-card);
}
.ps-desktop .ps-editorial-copy {
@@ -1877,7 +1975,7 @@ body.picker-page {
display: block;
min-height: 0;
background-color: var(--pvs-accent-a);
- border-radius: 2px;
+ border-radius: var(--pvs-radius-surface);
}
.ps-gallery-item:nth-child(2) > i {
@@ -1902,7 +2000,7 @@ body.picker-page {
width: 76%;
height: 6px;
background-color: var(--pvs-bars);
- border-radius: 2px;
+ border-radius: var(--pvs-radius-bar);
}
.ps-gallery-item b:last-child {
@@ -1913,8 +2011,9 @@ body.picker-page {
display: flex;
justify-content: space-between;
align-items: center;
- padding-inline: 5.5%;
- border-top: 1px solid var(--pvs-rule);
+ padding-inline: var(--pvs-gutter);
+ background-color: var(--pvs-band);
+ border-top: var(--pvs-divider-w) solid var(--pvs-divider-c);
}
.ps-footer-links {
@@ -1927,14 +2026,14 @@ body.picker-page {
width: clamp(12px, 3vw, 28px);
height: 4px;
background-color: var(--pvs-bars);
- border-radius: 2px;
+ border-radius: var(--pvs-radius-bar);
}
.ps-footer-mark {
width: 7%;
height: 5px;
background-color: var(--pvs-bars);
- border-radius: 2px;
+ border-radius: var(--pvs-radius-bar);
}
/* Phone artboard: the same site on mobile. */
@@ -1945,7 +2044,8 @@ body.picker-page {
min-height: 0;
padding: 0 6%;
background-color: var(--pvs-surface);
- border-bottom: 1px solid var(--pvs-rule);
+ border-bottom: var(--pvs-divider-w) solid var(--pvs-divider-c);
+ box-shadow: var(--pvs-shadow-chrome);
}
.ps-phone-top .ps-brand-block {
@@ -2082,10 +2182,10 @@ body.picker-page {
min-height: 0;
padding-inline: 6%;
background-color: var(--pvs-surface);
- border-top: 1px solid var(--pvs-rule);
+ border-top: var(--pvs-divider-w) solid var(--pvs-divider-c);
}
-.picker-strategy-preview:not(.picker-preview-type) :is(
+.picker-artboard:not(.picker-preview-type) :is(
.ps-desktop,
.ps-phone,
.ps-nav,
@@ -2110,6 +2210,8 @@ body.picker-page {
.ps-editorial-copy strong,
.ps-editorial-copy span i,
.ps-editorial-copy > em,
+ .ps-gallery,
+ .ps-gallery-item,
.ps-gallery-item > i,
.ps-gallery-item b,
.ps-footer-links i,
@@ -2119,7 +2221,9 @@ body.picker-page {
transition:
background-color 280ms var(--ks-ease),
background-image 280ms var(--ks-ease),
- border-color 280ms var(--ks-ease);
+ border-color 280ms var(--ks-ease),
+ border-radius 280ms var(--ks-ease),
+ box-shadow 280ms var(--ks-ease);
}
.picker-preview-type :is(
@@ -3366,7 +3470,7 @@ body.picker-page {
.picker-tip::after,
.picker-palette-hint,
.picker-strategy-option,
- .picker-strategy-preview:not(.picker-preview-type) :is(
+ .picker-artboard:not(.picker-preview-type) :is(
.ps-desktop,
.ps-phone,
.ps-nav,
diff --git a/picker/styles/screens/boundaries.css b/picker/styles/screens/boundaries.css
new file mode 100644
index 000000000..f7eecca87
--- /dev/null
+++ b/picker/styles/screens/boundaries.css
@@ -0,0 +1,115 @@
+/* ============================================================
+ Screen 08: boundary style.
+
+ What tells the reader one section has ended and the next has begun. Four
+ answers, in order of how much material they spend: nothing, a line, a
+ change of ground, an edge you can see.
+
+ Bands are mixed from the artboard's own ink and ground rather than named
+ colors, so a drenched page separates with a lighter wash of its primary
+ instead of turning grey.
+
+ The tokens moved here are section-scale, and they are the grounds and edges
+ themselves. Depth works at element scale and only ever adds to what is set
+ here, as a shadow or as a tonal overlay, so the two answers compose rather
+ than overwrite each other.
+ ============================================================ */
+
+/* ── Open space ──────────────────────────────────────────────
+ Every rule off and every ground the same, so the only thing left holding
+ the sections apart is the air between them. The band the proof sits in
+ gets more of it, because with nothing drawn the page has to spend room to
+ say the same thing. */
+#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="open-space"]) .picker-preview-boundaries,
+:where(#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="open-space"]:checked)) [data-carry] {
+ --pvs-divider-w: 0px;
+ --pvs-band: transparent;
+ --pvs-band-alt: transparent;
+ --pvs-band-top: transparent;
+ --pvs-panel: transparent;
+ --pvs-panel-edge-w: 0px;
+ --pvs-panel-pad: 0px;
+ --pvs-proof-pad-y: 4.2%;
+ --pvs-proof-inset: 0px;
+}
+
+/* ── Thin dividers ───────────────────────────────────────────
+ One hairline per boundary and nothing else: at the nav, above and below
+ the proof band, between its columns, and over the footer. */
+#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="thin-dividers"]) .picker-preview-boundaries,
+:where(#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="thin-dividers"]:checked)) [data-carry] {
+ --pvs-divider-w: 1px;
+ --pvs-divider-c: color-mix(in oklab, var(--pvs-ink) 24%, var(--pvs-ground));
+ --pvs-band: transparent;
+ --pvs-band-alt: transparent;
+ --pvs-band-top: transparent;
+ --pvs-panel: transparent;
+ --pvs-panel-edge-w: 0px;
+ --pvs-panel-pad: 0px;
+ --pvs-proof-pad-y: 2%;
+ --pvs-proof-inset: 0px;
+}
+
+/* ── Surface changes ─────────────────────────────────────────
+ No lines at all. Chrome sits on one tone, the proof band on another, and
+ the boundary is wherever the ground changes under your eye. */
+#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="surface-changes"]) .picker-preview-boundaries,
+:where(#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="surface-changes"]:checked)) [data-carry] {
+ --pvs-divider-w: 0px;
+ --pvs-band: color-mix(in oklab, var(--pvs-ink) 7%, var(--pvs-ground));
+ --pvs-band-alt: color-mix(in oklab, var(--pvs-ink) 13%, var(--pvs-ground));
+ --pvs-band-top: color-mix(in oklab, var(--pvs-ink) 7%, var(--pvs-ground));
+ --pvs-panel: transparent;
+ --pvs-panel-edge-w: 0px;
+ --pvs-panel-pad: 0px;
+ --pvs-proof-pad-y: 2.8%;
+ --pvs-proof-inset: 0px;
+}
+
+/* ── Cards and panels ────────────────────────────────────────
+ Content moves inside containers. The proof band pulls in off the page
+ margins and grows an edge, the cards do the same, and the page reads as a
+ set of objects on a ground rather than a run of sections. */
+#picker-form:has(.picker-strategy-option:hover input[name="boundary-style"][value="cards-and-panels"]) .picker-preview-boundaries,
+:where(#picker-form:not(:has(.picker-strategy-option:hover input[name="boundary-style"])):has(input[name="boundary-style"][value="cards-and-panels"]:checked)) [data-carry] {
+ --pvs-divider-w: 0px;
+ --pvs-band: transparent;
+ --pvs-band-alt: color-mix(in oklab, var(--pvs-ink) 5%, var(--pvs-ground));
+ --pvs-band-top: transparent;
+ --pvs-panel: color-mix(in oklab, var(--pvs-ink) 5%, var(--pvs-ground));
+ --pvs-panel-edge: color-mix(in oklab, var(--pvs-ink) 20%, var(--pvs-ground));
+ --pvs-panel-edge-w: 1px;
+ --pvs-panel-pad: 5%;
+ --pvs-proof-pad-y: 2.4%;
+ --pvs-proof-inset: var(--pvs-gutter);
+}
+
+/* The proof band is the one section that can become a container without the
+ page being restructured, so it carries the difference between a tinted
+ band and a panel: the inset pulls it off the page margins and the edge
+ only exists where an inset was asked for. */
+[data-carry] .ps-desktop > .ps-proof {
+ margin-inline: var(--pvs-proof-inset, 0px);
+ padding-block: var(--pvs-proof-pad-y, 2%);
+ border: var(--pvs-panel-edge-w) solid var(--pvs-panel-edge);
+ border-radius: var(--pvs-radius-surface);
+}
+
+/* The handset's chrome carries its own tint in the base artboard, which is a
+ boundary the open answers did not ask for. Both ends take the band. */
+[data-carry] .ps-phone-top,
+[data-carry] .ps-phone-footer {
+ background-color: var(--pvs-band-top, var(--pvs-surface));
+}
+
+/* Percentage padding resolves against the card's own width, so one value
+ holds at both artboard sizes instead of swamping the handset. */
+[data-carry] .ps-gallery-item {
+ padding: var(--pvs-panel-pad, 0px);
+}
+
+/* The handset's card row is already the block closest to the footer, so it
+ takes a shorter version of the same padding rather than growing into it. */
+[data-carry] .ps-phone-body .ps-gallery-item {
+ padding: calc(var(--pvs-panel-pad, 0px) * 0.5);
+}
diff --git a/picker/styles/screens/corners.css b/picker/styles/screens/corners.css
new file mode 100644
index 000000000..46262671e
--- /dev/null
+++ b/picker/styles/screens/corners.css
@@ -0,0 +1,61 @@
+/* ============================================================
+ Screen 09: corner style.
+
+ One question, four radii, and a deliberate limit: the text bars barely move
+ and the dots do not move at all.
+
+ A bar in this wireframe stands for a line of type. Round it like a button
+ and it stops reading as text and starts reading as a tag, which would make
+ the pill answer look like a different page rather than the same page with
+ rounder corners. So bars top out a few pixels short of their own height.
+
+ The dots stay circular in every answer, Sharp included. A dot is a mark
+ rather than a shape with corners; squaring it changes what it is, not how
+ round it is, and the Sharp page reads as sharp without it.
+ ============================================================ */
+
+/* ── Sharp ───────────────────────────────────────────────────
+ Nothing rounded. Every corner is drafted. */
+#picker-form:has(.picker-strategy-option:hover input[name="corner-style"][value="sharp"]) .picker-preview-corners,
+:where(#picker-form:not(:has(.picker-strategy-option:hover input[name="corner-style"])):has(input[name="corner-style"][value="sharp"]:checked)) [data-carry] {
+ --pvs-radius-frame: 0px;
+ --pvs-radius-frame-phone: 0px;
+ --pvs-radius-surface: 0px;
+ --pvs-radius-control: 0px;
+ --pvs-radius-bar: 0px;
+}
+
+/* ── Slightly soft ───────────────────────────────────────────
+ Enough radius to take the edge off and not enough to be remarked on. */
+#picker-form:has(.picker-strategy-option:hover input[name="corner-style"][value="slightly-soft"]) .picker-preview-corners,
+:where(#picker-form:not(:has(.picker-strategy-option:hover input[name="corner-style"])):has(input[name="corner-style"][value="slightly-soft"]:checked)) [data-carry] {
+ --pvs-radius-frame: 4px;
+ --pvs-radius-frame-phone: 5px;
+ --pvs-radius-surface: 3px;
+ --pvs-radius-control: 3px;
+ --pvs-radius-bar: 2px;
+}
+
+/* ── Friendly ────────────────────────────────────────────────
+ Cards, pictures, and buttons are clearly rounded, and the handset rounds
+ with them. */
+#picker-form:has(.picker-strategy-option:hover input[name="corner-style"][value="friendly"]) .picker-preview-corners,
+:where(#picker-form:not(:has(.picker-strategy-option:hover input[name="corner-style"])):has(input[name="corner-style"][value="friendly"]:checked)) [data-carry] {
+ --pvs-radius-frame: 7px;
+ --pvs-radius-frame-phone: 11px;
+ --pvs-radius-surface: 8px;
+ --pvs-radius-control: 8px;
+ --pvs-radius-bar: 3px;
+}
+
+/* ── Pill-like ───────────────────────────────────────────────
+ Controls go fully round. Surfaces round hard but stay rectangles, because
+ a card at half its own height is no longer a card. */
+#picker-form:has(.picker-strategy-option:hover input[name="corner-style"][value="pill"]) .picker-preview-corners,
+:where(#picker-form:not(:has(.picker-strategy-option:hover input[name="corner-style"])):has(input[name="corner-style"][value="pill"]:checked)) [data-carry] {
+ --pvs-radius-frame: 9px;
+ --pvs-radius-frame-phone: 15px;
+ --pvs-radius-surface: 12px;
+ --pvs-radius-control: 999px;
+ --pvs-radius-bar: 3px;
+}
diff --git a/picker/styles/screens/depth.css b/picker/styles/screens/depth.css
new file mode 100644
index 000000000..90de2d4b5
--- /dev/null
+++ b/picker/styles/screens/depth.css
@@ -0,0 +1,120 @@
+/* ============================================================
+ Screen 10: depth style.
+
+ How far off the page an element sits. Boundaries fills section grounds;
+ this file lifts individual elements, and the two answers compose instead
+ of arguing.
+
+ Two kinds of value, because a shadow and a tonal step are not the same
+ material. Cast shadows go in the --pvs-shadow-* slots and need something
+ solid under them, which is why the block at the bottom moves the card's
+ lift onto the picture inside it wherever the boundary answer left the card
+ transparent: a shadow around an invisible rectangle reads as a bug. Tonal
+ steps go in --pvs-tint-* and are painted as a ground, so they land on any
+ element whether or not it already has one.
+
+ Shadows are sized for the artboard rather than for a real page. The frame
+ is about a third of browser width, so a blur that would read as gentle at
+ full size disappears here; the offsets and blurs below are already scaled.
+
+ They are also black rather than mixed from the artboard's ink. Ink follows
+ the ground, so on a drenched page an ink shadow turns pale and the lift
+ comes out as a glow. Light falls the same way whatever color the page is.
+ ============================================================ */
+
+/* ── Flat ────────────────────────────────────────────────────
+ One plane. Nothing casts, nothing steps. */
+#picker-form:has(.picker-strategy-option:hover input[name="depth-style"][value="flat"]) .picker-preview-depth,
+:where(#picker-form:not(:has(.picker-strategy-option:hover input[name="depth-style"])):has(input[name="depth-style"][value="flat"]:checked)) [data-carry] {
+ --pvs-shadow-chrome: none;
+ --pvs-shadow-card: none;
+ --pvs-shadow-control: none;
+ --pvs-shadow-surface: none;
+ --pvs-tint-chrome: none;
+ --pvs-tint-card: none;
+}
+
+/* ── Layered ─────────────────────────────────────────────────
+ Two tonal steps off the ground, cards further up than chrome, and not one
+ shadow on the page. */
+#picker-form:has(.picker-strategy-option:hover input[name="depth-style"][value="layered"]) .picker-preview-depth,
+:where(#picker-form:not(:has(.picker-strategy-option:hover input[name="depth-style"])):has(input[name="depth-style"][value="layered"]:checked)) [data-carry] {
+ --pvs-shadow-chrome: none;
+ --pvs-shadow-card: none;
+ --pvs-shadow-control: none;
+ --pvs-shadow-surface: none;
+ --pvs-tint-chrome: linear-gradient(
+ color-mix(in oklab, var(--pvs-ink) 6%, transparent),
+ color-mix(in oklab, var(--pvs-ink) 6%, transparent)
+ );
+ --pvs-tint-card: linear-gradient(
+ color-mix(in oklab, var(--pvs-ink) 12%, transparent),
+ color-mix(in oklab, var(--pvs-ink) 12%, transparent)
+ );
+}
+
+/* ── Soft lift ───────────────────────────────────────────────
+ Cards and controls come off the page by a millimetre. Two shadows on the
+ cards: a tight one for the contact edge, a wider one for the ambient. */
+#picker-form:has(.picker-strategy-option:hover input[name="depth-style"][value="soft-lift"]) .picker-preview-depth,
+:where(#picker-form:not(:has(.picker-strategy-option:hover input[name="depth-style"])):has(input[name="depth-style"][value="soft-lift"]:checked)) [data-carry] {
+ --pvs-shadow-chrome: 0 1px 2px rgb(0 0 0 / 0.1);
+ --pvs-shadow-card:
+ 0 1px 1px rgb(0 0 0 / 0.15),
+ 0 3px 7px rgb(0 0 0 / 0.14);
+ --pvs-shadow-control: 0 1px 2px rgb(0 0 0 / 0.24);
+ --pvs-shadow-surface: 0 2px 5px rgb(0 0 0 / 0.14);
+ --pvs-tint-chrome: none;
+ --pvs-tint-card: none;
+}
+
+/* ── Floating ────────────────────────────────────────────────
+ A clear gap between the panels and the page. Offsets grow faster than
+ blurs, so the lift reads as height rather than as fog. */
+#picker-form:has(.picker-strategy-option:hover input[name="depth-style"][value="floating"]) .picker-preview-depth,
+:where(#picker-form:not(:has(.picker-strategy-option:hover input[name="depth-style"])):has(input[name="depth-style"][value="floating"]:checked)) [data-carry] {
+ --pvs-shadow-chrome: 0 2px 6px rgb(0 0 0 / 0.16);
+ --pvs-shadow-card:
+ 0 2px 3px rgb(0 0 0 / 0.2),
+ 0 9px 18px rgb(0 0 0 / 0.24);
+ --pvs-shadow-control: 0 3px 6px rgb(0 0 0 / 0.32);
+ --pvs-shadow-surface: 0 6px 14px rgb(0 0 0 / 0.22);
+ --pvs-tint-chrome: none;
+ --pvs-tint-card: none;
+}
+
+/* Tonal steps paint as a ground, over whatever ground the boundary answer
+ already chose. */
+[data-carry] .ps-gallery-item {
+ background-image: var(--pvs-tint-card, none);
+}
+
+[data-carry] :is(.ps-nav, .ps-footer, .ps-phone-top, .ps-phone-footer) {
+ background-image: var(--pvs-tint-chrome, none);
+}
+
+/* ── Where a cast shadow is allowed to land ──────────────────
+ Only off something solid. Under the two boundary answers that give the
+ card and the proof band a ground, the section itself lifts. Under the two
+ that leave them transparent, the lift moves inward to the picture, which
+ is the only part of the card that is a surface in its own right. */
+#picker-form:has(input[name="boundary-style"][value="cards-and-panels"]:checked) [data-carry] :is(.ps-gallery-item, .ps-desktop > .ps-proof),
+#picker-form:has(input[name="boundary-style"][value="surface-changes"]:checked) [data-carry] .ps-desktop > .ps-proof {
+ box-shadow: var(--pvs-shadow-card);
+}
+
+#picker-form:not(:has(input[name="boundary-style"][value="cards-and-panels"]:checked)) [data-carry] .ps-gallery-item {
+ box-shadow: none;
+}
+
+#picker-form:not(:has(input[name="boundary-style"][value="cards-and-panels"]:checked)) [data-carry] .ps-gallery-item > i {
+ box-shadow: var(--pvs-shadow-card);
+}
+
+/* Chrome is a ground only under the surface answer, so that is the only one
+ where the bar at the top of the page casts. The footers are left out: a
+ footer's shadow would fall out of the frame and be clipped away, so the
+ only trace of it would be a hard line. */
+#picker-form:not(:has(input[name="boundary-style"][value="surface-changes"]:checked)) [data-carry] :is(.ps-nav, .ps-phone-top) {
+ box-shadow: none;
+}
diff --git a/picker/styles/screens/layout.css b/picker/styles/screens/layout.css
new file mode 100644
index 000000000..519b00220
--- /dev/null
+++ b/picker/styles/screens/layout.css
@@ -0,0 +1,130 @@
+/* ============================================================
+ Screen 07: layout structure.
+
+ Four grids on the same page. The answer is read off the columns, the page
+ margins, the vertical bands, and whether a block is allowed to leave its
+ cell, so each option rewrites all four rather than nudging one number.
+
+ Two selectors per option, the shape screen 03 established. The first is the
+ hover preview and belongs to this screen alone. The second is the committed
+ answer and reaches every artboard marked data-carry, which is this screen
+ plus boundaries, corners, and depth, so each later question is asked about
+ the page as it has been chosen so far.
+ ============================================================ */
+
+/* ── Simple grid ─────────────────────────────────────────────
+ Halves and quarters, both margins equal, bands of near-equal weight. The
+ thing to notice is that there is nothing to notice. */
+#picker-form:has(.picker-strategy-option:hover input[name="layout-structure"][value="simple-grid"]) .picker-preview-layout,
+:where(#picker-form:not(:has(.picker-strategy-option:hover input[name="layout-structure"])):has(input[name="layout-structure"][value="simple-grid"]:checked)) [data-carry] {
+ --pvs-gutter: 5.5%;
+ --pvs-gutter-end: 5.5%;
+ --pvs-hero-cols: minmax(0, 1fr) minmax(0, 1fr);
+ --pvs-hero-gap: 5.5%;
+ --pvs-editorial-cols: 22.4% minmax(0, 1fr);
+ --pvs-editorial-gap: 5.5%;
+ --pvs-gallery-cols: repeat(4, minmax(0, 1fr));
+ --pvs-gallery-gap: 5.5%;
+ --pvs-section-align: start;
+ --pvs-rows: 9.4% 45.5% 9% 27.8% 8.3%;
+ --pvs-phone-gutter: 6%;
+}
+
+/* ── Balanced ────────────────────────────────────────────────
+ The grid still runs the page, but the hero leans and the first card is
+ given more room than its neighbours. Two breaks, both on purpose. */
+#picker-form:has(.picker-strategy-option:hover input[name="layout-structure"][value="balanced"]) .picker-preview-layout,
+:where(#picker-form:not(:has(.picker-strategy-option:hover input[name="layout-structure"])):has(input[name="layout-structure"][value="balanced"]:checked)) [data-carry] {
+ --pvs-gutter: 5.5%;
+ --pvs-gutter-end: 4.5%;
+ --pvs-hero-cols: 42% minmax(0, 1fr);
+ --pvs-hero-gap: 9%;
+ --pvs-editorial-cols: 29% minmax(0, 1fr);
+ --pvs-editorial-gap: 6%;
+ --pvs-gallery-cols: 1.35fr 0.88fr 0.88fr 0.88fr;
+ --pvs-gallery-gap: 3.5%;
+ --pvs-section-align: center;
+ --pvs-rows: 9.4% 50.6% 9% 22.7% 8.3%;
+}
+
+/* ── Editorial ───────────────────────────────────────────────
+ A wide standing column against a narrow portrait, uneven margins, a hero
+ band that takes most of the page, and a lead card at twice its neighbours.
+ Copy hangs from the top of each band the way a column of text hangs from a
+ rule. */
+#picker-form:has(.picker-strategy-option:hover input[name="layout-structure"][value="editorial"]) .picker-preview-layout,
+:where(#picker-form:not(:has(.picker-strategy-option:hover input[name="layout-structure"])):has(input[name="layout-structure"][value="editorial"]:checked)) [data-carry] {
+ --pvs-gutter: 9.5%;
+ --pvs-gutter-end: 2.5%;
+ --pvs-hero-cols: 56% minmax(0, 1fr);
+ --pvs-hero-gap: 11%;
+ --pvs-editorial-cols: 14% minmax(0, 1fr);
+ --pvs-editorial-gap: 7%;
+ --pvs-gallery-cols: 2.2fr 0.8fr 0.8fr 0.8fr;
+ --pvs-gallery-gap: 3%;
+ --pvs-section-align: start;
+ --pvs-rows: 8% 57.5% 8.5% 18.5% 7.5%;
+ --pvs-phone-gutter: 11%;
+ --pvs-phone-bleed: -9%;
+ --pvs-phone-bleed-w: 118%;
+}
+
+/* ── Freeform ────────────────────────────────────────────────
+ Columns of no particular relation, and blocks that leave their cells. */
+#picker-form:has(.picker-strategy-option:hover input[name="layout-structure"][value="freeform"]) .picker-preview-layout,
+:where(#picker-form:not(:has(.picker-strategy-option:hover input[name="layout-structure"])):has(input[name="layout-structure"][value="freeform"]:checked)) [data-carry] {
+ --pvs-gutter: 4%;
+ --pvs-gutter-end: 7%;
+ --pvs-hero-cols: 46% minmax(0, 1fr);
+ --pvs-hero-gap: 2%;
+ --pvs-editorial-cols: 37% minmax(0, 1fr);
+ --pvs-editorial-gap: 2.5%;
+ --pvs-gallery-cols: 1.45fr 0.72fr 1.1fr 0.83fr;
+ --pvs-gallery-gap: 2.5%;
+ --pvs-section-align: end;
+ --pvs-rows: 9.4% 48% 8.4% 26.2% 8%;
+ --pvs-phone-gutter: 4%;
+ --pvs-drift: 1;
+}
+
+/* Drift is declared on every artboard and multiplied by a flag only freeform
+ raises, so the other three options pay nothing for the rule existing. The
+ offsets are transforms: a block that steps out of its cell does not push
+ the rest of the page around to make room, which is what makes the overlap
+ read as intent rather than as a broken grid. */
+[data-carry] .ps-hero-copy {
+ position: relative;
+ z-index: var(--pvs-drift, 0);
+ translate: calc(var(--pvs-drift, 0) * 4%) calc(var(--pvs-drift, 0) * 9%);
+}
+
+[data-carry] .ps-hero > .ps-image {
+ translate: calc(var(--pvs-drift, 0) * -5%) calc(var(--pvs-drift, 0) * -7%);
+}
+
+/* Drift is a desktop move. The handset has one column and a stack sized in
+ pixels, so a block stepping out of line there has nowhere to go but through
+ the bottom of the frame. */
+[data-carry] .ps-desktop .ps-gallery-item:nth-child(2n) {
+ translate: 0 calc(var(--pvs-drift, 0) * 14%);
+}
+
+[data-carry] .ps-desktop .ps-editorial-copy {
+ translate: 0 calc(var(--pvs-drift, 0) * -12%);
+}
+
+/* ── The handset ─────────────────────────────────────────────
+ One column still answers the question, in the margins and in whether
+ anything is allowed to break the column: the picture is the one block given
+ that permission, full bleed under Editorial. The rhythm between blocks is
+ left alone, because the handset's stack is sized in pixels and a couple of
+ points of extra gap pushes the card row through the bottom of the frame at
+ the narrowest window the picker supports. */
+[data-carry] .ps-phone-body {
+ padding-inline: var(--pvs-phone-gutter, 6%);
+}
+
+[data-carry] .ps-phone-body > .ps-image {
+ margin-inline: var(--pvs-phone-bleed, 0);
+ width: var(--pvs-phone-bleed-w, auto);
+}
diff --git a/picker/styles/screens/motion.css b/picker/styles/screens/motion.css
new file mode 100644
index 000000000..0612ed325
--- /dev/null
+++ b/picker/styles/screens/motion.css
@@ -0,0 +1,480 @@
+/* ============================================================
+ Screen 06: motion energy.
+
+ One scene, three energies. A synthetic pointer walks the same route through
+ the same wireframe on all three options and clicks the same four things: a
+ nav item, the second of two hero buttons, a choice in the proof row, and a
+ card. What differs is what the page does about it.
+
+ The whole scene runs on one 8s timeline with every beat written as a
+ percentage of it, and nothing is offset by animation-delay. That is what
+ keeps a four-element orchestrated response in step after an hour of looping,
+ and it is why a frozen frame of this screen is always coherent.
+
+ The three options are not three sets of keyframes. They set the amplitude of
+ the shared ones: how far a press travels, whether an entrance starts from
+ nothing, whether siblings dim, and above all whether a state change steps or
+ eases. Restrained's step-start puts every change on the frame the click
+ lands, which is what "state changes only" means in practice.
+ ============================================================ */
+.picker-preview-motion {
+ --mt: 8s;
+ --mt-ease: cubic-bezier(0.16, 1, 0.3, 1);
+
+ /* Restrained is the default state of every slot below: changes land whole,
+ nothing travels, nothing arrives. */
+ --mt-tf: steps(1, start);
+ --mt-press-scale: 1;
+ --mt-lift: 0px;
+ --mt-dim: 1;
+ --mt-in-o: 1;
+ --mt-in-y: 0px;
+ --mt-in-clip: 0%;
+}
+
+/* Hover previews the energy before the click commits it, same as the color
+ strategy rows. Motion is the one answer that does not travel to the screens
+ after it: a page still moving is a page you cannot judge a grid on. */
+/* State changes get a gentler curve than movement does. The deceleration curve
+ below is right for something arriving from somewhere, but it puts half a
+ color cross-fade in the first 25ms, which is another way of spelling
+ instant. */
+#picker-form:has(.picker-strategy-option:hover input[name="motion-energy"][value="responsive"]) .picker-preview-motion,
+#picker-form:not(:has(.picker-strategy-option:hover input[name="motion-energy"])):has(input[name="motion-energy"][value="responsive"]:checked) .picker-preview-motion {
+ --mt-tf: ease;
+ --mt-press-scale: 0.94;
+}
+
+#picker-form:has(.picker-strategy-option:hover input[name="motion-energy"][value="choreographed"]) .picker-preview-motion,
+#picker-form:not(:has(.picker-strategy-option:hover input[name="motion-energy"])):has(input[name="motion-energy"][value="choreographed"]:checked) .picker-preview-motion {
+ --mt-tf: ease;
+ --mt-press-scale: 0.94;
+ --mt-lift: -4px;
+ --mt-dim: 0.45;
+ --mt-in-o: 0;
+ --mt-in-y: 14px;
+ --mt-in-clip: 100%;
+}
+
+/* Size containment turns the frame into the coordinate system: the pointer's
+ route is written in cqw and cqh, so it lands on the same button at every
+ viewport width without a line of script. */
+.picker-preview-motion .ps-desktop {
+ position: relative;
+ container-type: size;
+}
+
+/* ── The pointer ─────────────────────────────────────────────── */
+
+.picker-preview-motion .ps-cursor {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 13px;
+ height: 13px;
+ background-color: var(--pvs-ink);
+ clip-path: polygon(0 0, 0 76%, 24% 58%, 41% 100%, 58% 92%, 41% 50%, 74% 42%);
+ filter: drop-shadow(0.5px 0.5px 0 var(--pvs-ground));
+ animation: mt-path var(--mt) ease-in-out infinite;
+}
+
+/* The click has to be visible or the state change looks unprovoked. This ring
+ belongs to the pointer rather than to the page, so it reads the same on all
+ three options: it is the visitor's hand, not the design's motion. */
+.picker-preview-motion .ps-cursor::after {
+ content: "";
+ position: absolute;
+ top: -4px;
+ left: -4px;
+ width: 18px;
+ height: 18px;
+ border: 1.5px solid var(--pvs-ink);
+ border-radius: 50%;
+ opacity: 0;
+ animation: mt-click var(--mt) linear infinite;
+}
+
+@keyframes mt-path {
+ 0%, 6% { translate: 34cqw 96cqh; }
+ 16%, 19% { translate: 76.9cqw 4.8cqh; }
+ 31%, 33% { translate: 21.8cqw 45.8cqh; }
+ 45%, 47% { translate: 54.9cqw 65.1cqh; }
+ 58%, 60.5% { translate: 73.8cqw 80.2cqh; }
+ 72%, 100% { translate: 34cqw 96cqh; }
+}
+
+@keyframes mt-click {
+ 0%, 17.6% { scale: 0.35; opacity: 0; }
+ 18% { scale: 0.35; opacity: 0.55; }
+ 20.4% { scale: 1.5; opacity: 0; }
+
+ 31.6% { scale: 0.35; opacity: 0; }
+ 32% { scale: 0.35; opacity: 0.55; }
+ 34.4% { scale: 1.5; opacity: 0; }
+
+ 45.6% { scale: 0.35; opacity: 0; }
+ 46% { scale: 0.35; opacity: 0.55; }
+ 48.4% { scale: 1.5; opacity: 0; }
+
+ 58.6% { scale: 0.35; opacity: 0; }
+ 59% { scale: 0.35; opacity: 0.55; }
+ 61.4%, 100% { scale: 1.5; opacity: 0; }
+}
+
+/* ── Beat one: the nav item at 18% ───────────────────────────── */
+
+.picker-preview-motion .ps-nav-bars i {
+ position: relative;
+}
+
+.picker-preview-motion .ps-nav-bars i::after {
+ content: "";
+ position: absolute;
+ inset: auto 0 -4px 0;
+ height: 2px;
+ background-color: var(--pvs-signal);
+ transform: scaleX(0);
+}
+
+.picker-preview-motion .ps-nav-bars i:first-child {
+ animation: mt-nav-off var(--mt) var(--mt-tf) infinite;
+}
+
+/* The outgoing marker collapses toward the item taking over and the incoming
+ one grows from the side it came from, so the pair reads as one marker
+ changing places rather than two independent bars. */
+.picker-preview-motion .ps-nav-bars i:first-child::after {
+ transform-origin: right;
+ animation: mt-rule-out var(--mt) var(--mt-tf) infinite;
+}
+
+.picker-preview-motion .ps-nav-bars i:nth-child(3) {
+ animation:
+ mt-nav-on var(--mt) var(--mt-tf) infinite,
+ mt-press-nav var(--mt) linear infinite;
+}
+
+.picker-preview-motion .ps-nav-bars i:nth-child(3)::after {
+ transform-origin: left;
+ animation: mt-rule-in var(--mt) var(--mt-tf) infinite;
+}
+
+@keyframes mt-nav-off {
+ 0%, 18% { background-color: var(--pvs-cta); }
+ 21%, 88% { background-color: var(--pvs-bars); }
+ 91%, 100% { background-color: var(--pvs-cta); }
+}
+
+@keyframes mt-nav-on {
+ 0%, 18% { background-color: var(--pvs-bars); }
+ 21%, 88% { background-color: var(--pvs-cta); }
+ 91%, 100% { background-color: var(--pvs-bars); }
+}
+
+@keyframes mt-rule-out {
+ 0%, 18% { transform: scaleX(1); }
+ 21%, 88% { transform: scaleX(0); }
+ 91%, 100% { transform: scaleX(1); }
+}
+
+@keyframes mt-rule-in {
+ 0%, 18% { transform: scaleX(0); }
+ 21%, 88% { transform: scaleX(1); }
+ 91%, 100% { transform: scaleX(0); }
+}
+
+@keyframes mt-press-nav {
+ 0%, 17.6% { scale: 1; }
+ 18.4% { scale: var(--mt-press-scale); }
+ 20% { scale: 1; }
+ 100% { scale: 1; }
+}
+
+/* ── Beat two: the hero buttons as one control, at 32% ───────── */
+
+.picker-preview-motion .ps-actions i:first-child {
+ animation: mt-seg-off var(--mt) var(--mt-tf) infinite;
+}
+
+.picker-preview-motion .ps-actions i:last-child {
+ animation:
+ mt-seg-on var(--mt) var(--mt-tf) infinite,
+ mt-press-seg var(--mt) linear infinite;
+}
+
+@keyframes mt-seg-off {
+ 0%, 32% { background-color: var(--pvs-cta); }
+ 35%, 88% { background-color: var(--pvs-ghost); }
+ 91%, 100% { background-color: var(--pvs-cta); }
+}
+
+@keyframes mt-seg-on {
+ 0%, 32% { background-color: var(--pvs-ghost); }
+ 35%, 88% { background-color: var(--pvs-cta); }
+ 91%, 100% { background-color: var(--pvs-ghost); }
+}
+
+@keyframes mt-press-seg {
+ 0%, 31.6% { scale: 1; }
+ 32.4% { scale: var(--mt-press-scale); }
+ 34% { scale: 1; }
+ 100% { scale: 1; }
+}
+
+/* ── Beat three: the proof row as a choice, at 46% ───────────── */
+
+/* The row's four claims become four options, which is what gives the pointer
+ something to choose. Selection is a halo on the marker and the claim beside
+ it darkening: the dots keep the accent colors the palette gave them, because
+ this screen is not the one relitigating color. */
+.picker-preview-motion .ps-proof-item:nth-of-type(1) i {
+ animation: mt-halo-off var(--mt) var(--mt-tf) infinite;
+}
+
+.picker-preview-motion .ps-proof-item:nth-of-type(1) span {
+ animation: mt-claim-off var(--mt) var(--mt-tf) infinite;
+}
+
+.picker-preview-motion .ps-proof-item:nth-of-type(3) i {
+ animation:
+ mt-halo-on var(--mt) var(--mt-tf) infinite,
+ mt-press-choice var(--mt) linear infinite;
+}
+
+.picker-preview-motion .ps-proof-item:nth-of-type(3) span {
+ animation: mt-claim-on var(--mt) var(--mt-tf) infinite;
+}
+
+@keyframes mt-halo-off {
+ 0%, 46% { box-shadow: 0 0 0 3px color-mix(in oklab, var(--pvs-cta) 30%, transparent); }
+ 49%, 88% { box-shadow: 0 0 0 0 color-mix(in oklab, var(--pvs-cta) 0%, transparent); }
+ 91%, 100% { box-shadow: 0 0 0 3px color-mix(in oklab, var(--pvs-cta) 30%, transparent); }
+}
+
+@keyframes mt-halo-on {
+ 0%, 46% { box-shadow: 0 0 0 0 color-mix(in oklab, var(--pvs-cta) 0%, transparent); }
+ 49%, 88% { box-shadow: 0 0 0 3px color-mix(in oklab, var(--pvs-cta) 30%, transparent); }
+ 91%, 100% { box-shadow: 0 0 0 0 color-mix(in oklab, var(--pvs-cta) 0%, transparent); }
+}
+
+@keyframes mt-claim-off {
+ 0%, 46% { background-color: var(--pvs-headline); }
+ 49%, 88% { background-color: var(--pvs-bars); }
+ 91%, 100% { background-color: var(--pvs-headline); }
+}
+
+@keyframes mt-claim-on {
+ 0%, 46% { background-color: var(--pvs-bars); }
+ 49%, 88% { background-color: var(--pvs-headline); }
+ 91%, 100% { background-color: var(--pvs-bars); }
+}
+
+@keyframes mt-press-choice {
+ 0%, 45.6% { scale: 1; }
+ 46.4% { scale: var(--mt-press-scale); }
+ 48% { scale: 1; }
+ 100% { scale: 1; }
+}
+
+/* ── Beat four: the card at 59% ──────────────────────────────── */
+
+/* Where the three energies separate most. All three mark the card as chosen.
+ Responsive stops there. Choreographed lifts it off the page and takes its
+ neighbours down 80ms later, which is the difference between feedback and
+ something staged. */
+.picker-preview-motion .ps-gallery-item:nth-child(3) {
+ animation:
+ mt-card-on var(--mt) var(--mt-tf) infinite,
+ mt-card-lift var(--mt) var(--mt-tf) infinite;
+}
+
+.picker-preview-motion .ps-gallery-item:not(:nth-child(3)) {
+ animation: mt-card-dim var(--mt) var(--mt-tf) infinite;
+}
+
+@keyframes mt-card-on {
+ 0%, 59% {
+ background-color: transparent;
+ box-shadow: inset 0 0 0 0 var(--pvs-cta);
+ }
+ 62%, 88% {
+ background-color: var(--pvs-surface);
+ box-shadow: inset 0 0 0 1.5px var(--pvs-cta);
+ }
+ 91%, 100% {
+ background-color: transparent;
+ box-shadow: inset 0 0 0 0 var(--pvs-cta);
+ }
+}
+
+@keyframes mt-card-lift {
+ 0%, 59% { transform: translateY(0); }
+ 62%, 88% { transform: translateY(var(--mt-lift)); }
+ 91%, 100% { transform: translateY(0); }
+}
+
+@keyframes mt-card-dim {
+ 0%, 60% { filter: opacity(1); }
+ 63%, 88% { filter: opacity(var(--mt-dim)); }
+ 91%, 100% { filter: opacity(1); }
+}
+
+/* ── The entrance, for the option that has one ───────────────── */
+
+/* Six bands down the page, 80ms apart, each arriving over 360ms: 760ms from
+ the first movement to the last, inside the band an authored entrance is
+ allowed to take. Restrained and Responsive run the same animations with the
+ travel set to zero, so the page is simply already there.
+
+ The bands are containers rather than leaves. Every element that changes
+ state during the loop is inside one, so an entrance never has to share a
+ property with an interaction. */
+.picker-preview-motion .ps-nav {
+ animation: mt-in-1 var(--mt) var(--mt-ease) infinite;
+}
+
+.picker-preview-motion .ps-hero-copy > * {
+ animation: mt-in-2 var(--mt) var(--mt-ease) infinite;
+}
+
+.picker-preview-motion .ps-hero > .ps-image {
+ animation: mt-in-image var(--mt) var(--mt-ease) infinite;
+}
+
+.picker-preview-motion .ps-proof-item {
+ animation: mt-in-4 var(--mt) var(--mt-ease) infinite;
+}
+
+.picker-preview-motion .ps-editorial-copy > * {
+ animation: mt-in-5 var(--mt) var(--mt-ease) infinite;
+}
+
+.picker-preview-motion .ps-gallery {
+ animation: mt-in-5 var(--mt) var(--mt-ease) infinite;
+}
+
+.picker-preview-motion .ps-footer {
+ animation: mt-in-6 var(--mt) var(--mt-ease) infinite;
+}
+
+@keyframes mt-in-1 {
+ 0% { opacity: var(--mt-in-o); translate: 0 var(--mt-in-y); }
+ 4.5%, 95% { opacity: 1; translate: 0 0; }
+ 100% { opacity: var(--mt-in-o); translate: 0 0; }
+}
+
+@keyframes mt-in-2 {
+ 0%, 1% { opacity: var(--mt-in-o); translate: 0 var(--mt-in-y); }
+ 5.5%, 95% { opacity: 1; translate: 0 0; }
+ 100% { opacity: var(--mt-in-o); translate: 0 0; }
+}
+
+@keyframes mt-in-4 {
+ 0%, 3% { opacity: var(--mt-in-o); translate: 0 var(--mt-in-y); }
+ 7.5%, 95% { opacity: 1; translate: 0 0; }
+ 100% { opacity: var(--mt-in-o); translate: 0 0; }
+}
+
+@keyframes mt-in-5 {
+ 0%, 4% { opacity: var(--mt-in-o); translate: 0 var(--mt-in-y); }
+ 8.5%, 95% { opacity: 1; translate: 0 0; }
+ 100% { opacity: var(--mt-in-o); translate: 0 0; }
+}
+
+@keyframes mt-in-6 {
+ 0%, 5% { opacity: var(--mt-in-o); translate: 0 var(--mt-in-y); }
+ 9.5%, 95% { opacity: 1; translate: 0 0; }
+ 100% { opacity: var(--mt-in-o); translate: 0 0; }
+}
+
+/* The picture is the one element that arrives on its own terms: it uncovers
+ rather than rises, because a photograph revealing itself is the moment a
+ staged entrance is built around. */
+@keyframes mt-in-image {
+ 0%, 2% {
+ opacity: var(--mt-in-o);
+ clip-path: inset(0 0 var(--mt-in-clip) 0);
+ }
+ 6.5%, 95% {
+ opacity: 1;
+ clip-path: inset(0 0 0% 0);
+ }
+ 100% {
+ opacity: var(--mt-in-o);
+ clip-path: inset(0 0 var(--mt-in-clip) 0);
+ }
+}
+
+/* A loop nobody asked for is the definition of a nonessential one. Reduced
+ motion gets the end of the scene instead: the pointer parked over the card
+ it chose, with every state already changed, which is the same information
+ without the movement. */
+@media (prefers-reduced-motion: reduce) {
+ /* Pseudo-elements are not valid inside :is() and get dropped from the list
+ rather than failing loudly, so the two that animate are cancelled on their
+ own lines. */
+ .picker-preview-motion .ps-cursor::after,
+ .picker-preview-motion .ps-nav-bars i::after,
+ .picker-preview-motion :is(
+ .ps-cursor,
+ .ps-nav,
+ .ps-nav-bars i,
+ .ps-hero-copy > *,
+ .ps-image,
+ .ps-actions i,
+ .ps-proof-item,
+ .ps-proof-item i,
+ .ps-proof-item span,
+ .ps-editorial-copy > *,
+ .ps-gallery,
+ .ps-gallery-item,
+ .ps-footer
+ ) {
+ /* The declarations being cancelled are per-element and more specific than
+ any list can be without repeating all fifteen selectors. A preference
+ the visitor set in their operating system outranks them. */
+ animation: none !important;
+ }
+
+ .picker-preview-motion .ps-cursor {
+ translate: 73.8cqw 80.2cqh;
+ }
+
+ .picker-preview-motion .ps-nav-bars i:first-child {
+ background-color: var(--pvs-bars);
+ }
+
+ .picker-preview-motion .ps-nav-bars i:nth-child(3) {
+ background-color: var(--pvs-cta);
+ }
+
+ .picker-preview-motion .ps-nav-bars i:nth-child(3)::after {
+ transform: scaleX(1);
+ }
+
+ .picker-preview-motion .ps-actions i:first-child {
+ background-color: var(--pvs-ghost);
+ }
+
+ .picker-preview-motion .ps-actions i:last-child {
+ background-color: var(--pvs-cta);
+ }
+
+ .picker-preview-motion .ps-proof-item:nth-of-type(3) i {
+ box-shadow: 0 0 0 3px color-mix(in oklab, var(--pvs-cta) 30%, transparent);
+ }
+
+ .picker-preview-motion .ps-proof-item:nth-of-type(3) span {
+ background-color: var(--pvs-headline);
+ }
+
+ .picker-preview-motion .ps-gallery-item:nth-child(3) {
+ background-color: var(--pvs-surface);
+ box-shadow: inset 0 0 0 1.5px var(--pvs-cta);
+ transform: translateY(var(--mt-lift));
+ }
+
+ .picker-preview-motion .ps-gallery-item:not(:nth-child(3)) {
+ filter: opacity(var(--mt-dim));
+ }
+}