This commit is contained in:
Abdul Wahab
2026-09-01 10:02:39 +05:00
parent ba9b1cd8c3
commit 1d192d1f39
11 changed files with 1525 additions and 225 deletions
+171
View File
@@ -0,0 +1,171 @@
---
/*
The wireframe landing page every option screen previews its answer on. One
copy so a change to the page's anatomy lands on every screen at once.
mode: 'bars' fills the hero with bar stacks, which is what a screen judging
color, layout, or depth needs. 'type' swaps them for real text elements and
adds the data-type-* hooks the font screen writes specimen copy into; that
screen is judging glyphs, so bars would tell it nothing.
phone: false drops the handset and leaves the desktop artboard alone in the
frame, for a screen whose answer only reads at desktop width.
*/
interface Props {
mode?: 'bars' | 'type';
phone?: boolean;
class?: string;
strategyHook?: boolean;
typeHook?: boolean;
cursor?: boolean;
/*
Marks an artboard as one that accumulates the structural answers. The
committed layout, boundary, corner, and depth selectors key on it, which is
how each screen shows the page as decided so far without those answers
reaching back to the screens that came before them.
*/
carry?: boolean;
}
const {
mode = 'bars',
phone = true,
class: className = '',
strategyHook = false,
typeHook = false,
cursor = false,
carry = false,
} = Astro.props;
const type = mode === 'type';
const classes = ['picker-artboard', className].filter(Boolean).join(' ');
// Empty string renders the bare attribute the font screen's selectors expect;
// undefined drops it entirely on the other screens.
const hook = type ? '' : undefined;
---
<div
class={classes}
data-artboard=""
data-carry={carry ? '' : undefined}
data-strategy-preview={strategyHook ? '' : undefined}
data-type-preview={typeHook ? '' : undefined}
aria-hidden="true"
>
<div class="ps-desktop">
<div class="ps-nav">
<div class="ps-brand-block" data-type-brand={hook}></div>
{type ? (
<div class="ps-nav-bars"><i data-type-nav></i><i data-type-nav></i><i data-type-nav></i><i data-type-nav></i></div>
) : (
<div class="ps-nav-bars"><i></i><i></i><i></i><i></i></div>
)}
<div class="ps-nav-action" data-type-nav-action={hook}></div>
</div>
<div class="ps-hero">
<div class="ps-hero-copy">
{type ? (
<Fragment>
<h2 class="pt-headline" data-type-headline></h2>
<p class="pt-body" data-type-body></p>
<div class="ps-actions"><i data-type-cta-primary></i><i data-type-cta-secondary></i></div>
</Fragment>
) : (
<Fragment>
<div class="ps-eyebrow"></div>
<div class="ps-headline"><i></i><i></i></div>
<div class="ps-copy"><i></i></div>
<div class="ps-actions"><i></i><i></i></div>
</Fragment>
)}
</div>
<div class="ps-image"></div>
</div>
<div class="ps-proof">
<div class="ps-proof-item"><i></i><span data-type-proof={hook}></span></div>
<i class="ps-proof-divider" aria-hidden="true"></i>
<div class="ps-proof-item"><i></i><span data-type-proof={hook}></span></div>
<i class="ps-proof-divider" aria-hidden="true"></i>
<div class="ps-proof-item"><i></i><span data-type-proof={hook}></span></div>
<i class="ps-proof-divider" aria-hidden="true"></i>
<div class="ps-proof-item"><i></i><span data-type-proof={hook}></span></div>
</div>
<div class="ps-editorial">
<div class="ps-editorial-copy">
<strong data-type-section-title={hook}></strong>
{type
? <p class="pt-section-body" data-type-section-body></p>
: <span><i></i><i></i></span>}
<em data-type-section-link={hook}></em>
</div>
<div class="ps-gallery">
{[0, 1, 2, 3].map(() => (
<div class="ps-gallery-item">
<i></i>
<span><b data-type-gallery-title={hook}></b><b data-type-gallery-meta={hook}></b></span>
</div>
))}
</div>
</div>
<div class="ps-footer">
{type ? (
<div class="ps-footer-links"><i data-type-footer-link></i><i data-type-footer-link></i><i data-type-footer-link></i><i data-type-footer-link></i></div>
) : (
<div class="ps-footer-links"><i></i><i></i><i></i><i></i></div>
)}
<div class="ps-footer-mark" data-type-footer-mark={hook}></div>
</div>
{cursor && <i class="ps-cursor" data-cursor aria-hidden="true"></i>}
</div>
{phone && (
<div class="ps-phone">
<div class="ps-phone-top">
<div class="ps-brand-block" data-type-brand={hook}></div>
<div class="ps-nav-action" data-type-menu-action={hook}></div>
</div>
<div class="ps-phone-body">
<div class="ps-image"></div>
{type ? (
<Fragment>
<h2 class="pt-headline" data-type-headline></h2>
<p class="pt-body" data-type-body></p>
</Fragment>
) : (
<Fragment>
<div class="ps-headline"><i></i><i></i></div>
<div class="ps-copy"><i></i><i></i></div>
</Fragment>
)}
<div class="ps-actions">
<i data-type-cta-primary={hook}></i><i data-type-cta-secondary={hook}></i>
</div>
<div class="ps-proof">
<div class="ps-proof-item"><i></i><span data-type-proof={hook}></span></div>
<i class="ps-proof-divider" aria-hidden="true"></i>
<div class="ps-proof-item"><i></i><span data-type-proof={hook}></span></div>
</div>
<div class="ps-editorial-copy">
<strong data-type-section-title={hook}></strong>
{type
? <p class="pt-section-body" data-type-section-body></p>
: <span><i></i><i></i></span>}
</div>
<div class="ps-gallery">
{[0, 1].map(() => (
<div class="ps-gallery-item">
<i></i>
<span><b data-type-gallery-title={hook}></b><b data-type-gallery-meta={hook}></b></span>
</div>
))}
</div>
</div>
<div class="ps-phone-footer">
{type ? (
<Fragment><i data-type-footer-link></i><i data-type-footer-link></i><i data-type-footer-link></i></Fragment>
) : (
<Fragment><i></i><i></i><i></i></Fragment>
)}
</div>
</div>
)}
</div>
+79
View File
@@ -0,0 +1,79 @@
---
/*
One question, one artboard: the shape screens 03 onward all share. A screen
that only remaps artboard slots needs nothing of its own here, so it arrives
as data and its design lives in its own stylesheet.
*/
import Artboard from './Artboard.astro';
interface Option {
value: string;
title: string;
desc: string;
}
interface Props {
id: string;
step: string;
slug: string;
name: string;
title: string;
legend: string;
cta: string;
options: Option[];
previewClass: string;
phone?: boolean;
}
const { id, step, slug, name, title, legend, cta, options, previewClass, phone = true } = Astro.props;
const titleId = `picker-${slug}-title`;
const artboardClass = phone ? previewClass : `${previewClass} picker-artboard--solo`;
---
<section class="picker-screen" data-screen={id} data-step={step} aria-hidden="true" aria-labelledby={titleId}>
<div class="picker-container">
<div class="picker-strategy" data-question={slug}>
<h2 id={titleId} class="picker-question-title">{title}</h2>
<div class="picker-strategy-grid">
<div class="picker-type-rail">
<fieldset
class="picker-strategy-options picker-strategy-choices picker-type-options"
style={`--pk-choices:${options.length}`}
>
<legend>{legend}</legend>
{options.map((option, index) => (
<label class="picker-strategy-option">
<input type="radio" name={name} value={option.value} checked={index === 0} />
<span class="picker-strategy-copy">
<span class="picker-strategy-title">{option.title}</span>
<span class="picker-strategy-desc">{option.desc}</span>
</span>
</label>
))}
</fieldset>
</div>
<Artboard
class={artboardClass}
phone={phone}
cursor={slug === 'motion'}
carry={slug !== 'motion'}
/>
</div>
<div class="picker-actions-stack">
<button class="ks-button ks-button-primary" type="button" data-advance="next">
{cta}
<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>
+8
View File
@@ -1,5 +1,13 @@
---
import '../styles/picker.css';
// Loaded after the shared sheet so a screen's own answers win on source order
// as well as specificity. One file per question, each owning only the slots
// its options rewrite.
import '../styles/screens/motion.css';
import '../styles/screens/layout.css';
import '../styles/screens/boundaries.css';
import '../styles/screens/corners.css';
import '../styles/screens/depth.css';
interface Props {
title?: string;
+172 -142
View File
@@ -1,5 +1,7 @@
---
import Picker from '../layouts/Picker.astro';
import Artboard from '../components/Artboard.astro';
import QuestionScreen from '../components/QuestionScreen.astro';
const roles = [
['primary', 'Primary'],
@@ -24,6 +26,169 @@ const scales = [
// 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]];
// The five questions that are answered by looking at the same wireframe page.
// Each one only rewrites the artboard slots it owns, so by the last screen the
// page is showing every decision made before it. Motion is the one question
// whose answer cannot be read on a handset, so it takes the frame alone.
const questions = [
{
id: '06',
step: 'Motion',
slug: 'motion',
name: 'motion-energy',
title: 'How much movement should this design have?',
legend: 'Motion energy',
cta: 'Select motion energy',
previewClass: 'picker-preview-motion',
phone: false,
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-preview-layout',
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 in it.',
},
{
value: 'editorial',
title: 'Editorial',
desc: 'Uneven columns, with one of them clearly leading.',
},
{
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',
options: [
{
value: 'open-space',
title: 'Open space',
desc: 'Nothing between sections but room. Spacing does the dividing.',
},
{
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.',
},
],
},
{
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',
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.',
},
],
},
{
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',
options: [
{
value: 'flat',
title: 'Flat',
desc: 'One plane. Nothing casts a shadow.',
},
{
value: 'layered',
title: 'Layered',
desc: 'Tone stacks the page. Still no shadows anywhere.',
},
{
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.',
},
],
},
];
---
<Picker>
@@ -265,76 +430,7 @@ const scaleRows = [['H1', 6], ['H2', 5], ['H3', 4], ['H4', 3], ['H5', 2], ['H6',
</fieldset>
</div>
<div class="picker-strategy-preview" data-strategy-preview aria-hidden="true">
<div class="ps-desktop">
<div class="ps-nav">
<div class="ps-brand-block"></div>
<div class="ps-nav-bars"><i></i><i></i><i></i><i></i></div>
<div class="ps-nav-action"></div>
</div>
<div class="ps-hero">
<div class="ps-hero-copy">
<div class="ps-eyebrow"></div>
<div class="ps-headline"><i></i><i></i></div>
<div class="ps-copy"><i></i></div>
<div class="ps-actions"><i></i><i></i></div>
</div>
<div class="ps-image"></div>
</div>
<div class="ps-proof">
<div class="ps-proof-item"><i></i><span></span></div>
<i class="ps-proof-divider" aria-hidden="true"></i>
<div class="ps-proof-item"><i></i><span></span></div>
<i class="ps-proof-divider" aria-hidden="true"></i>
<div class="ps-proof-item"><i></i><span></span></div>
<i class="ps-proof-divider" aria-hidden="true"></i>
<div class="ps-proof-item"><i></i><span></span></div>
</div>
<div class="ps-editorial">
<div class="ps-editorial-copy">
<strong></strong>
<span><i></i><i></i></span>
<em></em>
</div>
<div class="ps-gallery">
<div class="ps-gallery-item"><i></i><span><b></b><b></b></span></div>
<div class="ps-gallery-item"><i></i><span><b></b><b></b></span></div>
<div class="ps-gallery-item"><i></i><span><b></b><b></b></span></div>
<div class="ps-gallery-item"><i></i><span><b></b><b></b></span></div>
</div>
</div>
<div class="ps-footer">
<div class="ps-footer-links"><i></i><i></i><i></i><i></i></div>
<div class="ps-footer-mark"></div>
</div>
</div>
<div class="ps-phone">
<div class="ps-phone-top">
<div class="ps-brand-block"></div>
<div class="ps-nav-action"></div>
</div>
<div class="ps-phone-body">
<div class="ps-image"></div>
<div class="ps-headline"><i></i><i></i></div>
<div class="ps-copy"><i></i><i></i></div>
<div class="ps-actions"><i></i><i></i></div>
<div class="ps-proof">
<div class="ps-proof-item"><i></i><span></span></div>
<i class="ps-proof-divider" aria-hidden="true"></i>
<div class="ps-proof-item"><i></i><span></span></div>
</div>
<div class="ps-editorial-copy">
<strong></strong>
<span><i></i><i></i></span>
</div>
<div class="ps-gallery">
<div class="ps-gallery-item"><i></i><span><b></b><b></b></span></div>
<div class="ps-gallery-item"><i></i><span><b></b><b></b></span></div>
</div>
</div>
<div class="ps-phone-footer"><i></i><i></i><i></i></div>
</div>
</div>
<Artboard class="picker-strategy-preview" strategyHook />
</div>
<div class="picker-actions-stack">
@@ -380,75 +476,7 @@ const scaleRows = [['H1', 6], ['H2', 5], ['H3', 4], ['H4', 3], ['H5', 2], ['H6',
</div>
</div>
<div class="picker-strategy-preview picker-preview-type" data-type-preview aria-hidden="true">
<div class="ps-desktop">
<div class="ps-nav">
<div class="ps-brand-block" data-type-brand></div>
<div class="ps-nav-bars"><i data-type-nav></i><i data-type-nav></i><i data-type-nav></i><i data-type-nav></i></div>
<div class="ps-nav-action" data-type-nav-action></div>
</div>
<div class="ps-hero">
<div class="ps-hero-copy">
<h2 class="pt-headline" data-type-headline></h2>
<p class="pt-body" data-type-body></p>
<div class="ps-actions"><i data-type-cta-primary></i><i data-type-cta-secondary></i></div>
</div>
<div class="ps-image"></div>
</div>
<div class="ps-proof">
<div class="ps-proof-item"><i></i><span data-type-proof></span></div>
<i class="ps-proof-divider" aria-hidden="true"></i>
<div class="ps-proof-item"><i></i><span data-type-proof></span></div>
<i class="ps-proof-divider" aria-hidden="true"></i>
<div class="ps-proof-item"><i></i><span data-type-proof></span></div>
<i class="ps-proof-divider" aria-hidden="true"></i>
<div class="ps-proof-item"><i></i><span data-type-proof></span></div>
</div>
<div class="ps-editorial">
<div class="ps-editorial-copy">
<strong data-type-section-title></strong>
<p class="pt-section-body" data-type-section-body></p>
<em data-type-section-link></em>
</div>
<div class="ps-gallery">
<div class="ps-gallery-item"><i></i><span><b data-type-gallery-title></b><b data-type-gallery-meta></b></span></div>
<div class="ps-gallery-item"><i></i><span><b data-type-gallery-title></b><b data-type-gallery-meta></b></span></div>
<div class="ps-gallery-item"><i></i><span><b data-type-gallery-title></b><b data-type-gallery-meta></b></span></div>
<div class="ps-gallery-item"><i></i><span><b data-type-gallery-title></b><b data-type-gallery-meta></b></span></div>
</div>
</div>
<div class="ps-footer">
<div class="ps-footer-links"><i data-type-footer-link></i><i data-type-footer-link></i><i data-type-footer-link></i><i data-type-footer-link></i></div>
<div class="ps-footer-mark" data-type-footer-mark></div>
</div>
</div>
<div class="ps-phone">
<div class="ps-phone-top">
<div class="ps-brand-block" data-type-brand></div>
<div class="ps-nav-action" data-type-menu-action></div>
</div>
<div class="ps-phone-body">
<div class="ps-image"></div>
<h2 class="pt-headline" data-type-headline></h2>
<p class="pt-body" data-type-body></p>
<div class="ps-actions"><i data-type-cta-primary></i><i data-type-cta-secondary></i></div>
<div class="ps-proof">
<div class="ps-proof-item"><i></i><span data-type-proof></span></div>
<i class="ps-proof-divider" aria-hidden="true"></i>
<div class="ps-proof-item"><i></i><span data-type-proof></span></div>
</div>
<div class="ps-editorial-copy">
<strong data-type-section-title></strong>
<p class="pt-section-body" data-type-section-body></p>
</div>
<div class="ps-gallery">
<div class="ps-gallery-item"><i></i><span><b data-type-gallery-title></b><b data-type-gallery-meta></b></span></div>
<div class="ps-gallery-item"><i></i><span><b data-type-gallery-title></b><b data-type-gallery-meta></b></span></div>
</div>
</div>
<div class="ps-phone-footer"><i data-type-footer-link></i><i data-type-footer-link></i><i data-type-footer-link></i></div>
</div>
</div>
<Artboard mode="type" class="picker-strategy-preview picker-preview-type" typeHook />
</div>
<template data-pair-card>
@@ -563,7 +591,9 @@ const scaleRows = [['H1', 6], ['H2', 5], ['H3', 4], ['H4', 3], ['H5', 2], ['H6',
</div>
</section>
<section class="picker-screen" data-screen="06" data-step="Review" aria-hidden="true" aria-labelledby="picker-finish-copy">
{questions.map((question) => <QuestionScreen {...question} />)}
<section class="picker-screen" data-screen="11" data-step="Review" aria-hidden="true" aria-labelledby="picker-finish-copy">
<div class="picker-container">
<div class="picker-placeholder">
<p id="picker-finish-copy">Your visual direction is ready.</p>
@@ -584,10 +614,10 @@ const scaleRows = [['H1', 6], ['H2', 5], ['H3', 4], ['H4', 3], ['H5', 2], ['H6',
<div class="picker-progress" aria-hidden="true">
<div class="picker-progress-track">
<i></i><i></i><i></i><i></i><i></i>
<i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i><i></i>
</div>
<p class="picker-progress-label">
<span class="picker-progress-index"><b data-progress-index>1</b> / 5</span>
<span class="picker-progress-index"><b data-progress-index>1</b> / 10</span>
<span class="picker-progress-name" data-progress-name></span>
</p>
</div>
+7 -5
View File
@@ -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') {
+182 -78
View File
@@ -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,
+115
View File
@@ -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);
}
+61
View File
@@ -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;
}
+120
View File
@@ -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;
}
+130
View File
@@ -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);
}
+480
View File
@@ -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));
}
}