diff --git a/picker/pages/index.astro b/picker/pages/index.astro index 08978402d..909549450 100644 --- a/picker/pages/index.astro +++ b/picker/pages/index.astro @@ -1099,73 +1099,90 @@ const questions = [
-
- - + + +
+ + + +
- -
- - + + +
+ + + +
- -
- + + +
+ + + +
+
+
+ + + +
+ + + +
+
+
+ + + +
+ + + +
diff --git a/picker/scripts/palette-picker.js b/picker/scripts/palette-picker.js index 841bf7392..378d69a9c 100644 --- a/picker/scripts/palette-picker.js +++ b/picker/scripts/palette-picker.js @@ -5257,15 +5257,6 @@ const HUB_GROUPS = { '10': 'depth-style', '11': 'icon-pack', }; -/* The icon set renders no per-surface fields, so it has no - dataset.chosen. A change event on its group is the one signal - that a person picked rather than the markup default: programmatic - checks never fire it. */ -const hubEdited = new Set(); -document.addEventListener('change', ({ target }) => { - if (target?.name === 'icon-pack') hubEdited.add(target.name); -}); - const hubSurfaceLabel = (mode) => modeInputs.find((input) => input.value === mode)?.dataset.surfaceLabel ?? mode; /* The display title lives on the option row of the question's own @@ -5291,10 +5282,8 @@ function hubSurfaceRows(group) { })); } -/* A summary row is a surface pill on the left and the value docked - right behind a patina dot, the same signal the surface tabs and the - option rows spend on a committed choice. The plain line survives - only for the skipped-card message. */ +/* The only line the renderer still builds by hand: the skipped-card + message, which stands in for the note. */ function hubPlainLine(text) { const line = document.createElement('span'); line.className = 'picker-hub-line'; @@ -5302,33 +5291,6 @@ function hubPlainLine(text) { return line; } -function hubRow(surfaceLabel, valueTitle, group, value) { - const entry = document.createElement('span'); - entry.className = 'picker-hub-entry'; - const pill = document.createElement('span'); - pill.className = 'picker-hub-pill'; - const label = document.createElement('span'); - label.className = 'picker-hub-pill-label'; - label.textContent = surfaceLabel; - const val = document.createElement('span'); - val.className = 'picker-hub-value'; - const dot = document.createElement('i'); - dot.className = 'picker-hub-dot'; - dot.setAttribute('aria-hidden', 'true'); - val.append(dot, valueTitle); - pill.append(label, val); - entry.append(pill); - if (group !== 'icon-pack') { - const note = document.createElement('span'); - note.className = 'picker-hub-note'; - const why = value ? hubOptionDesc(group, value) : ''; - note.textContent = why; - note.hidden = !why; - entry.append(note); - } - return entry; -} - /* The one-line reason the current answer is a sound default, read off the option row's own description so the hub never restates copy. dataset.copy is read first because applyAllows swaps textContent for @@ -5424,18 +5386,37 @@ const hubQuestionBoard = (screen, mode) => { return boards.find((board) => board.dataset.motionCell === `${mode}-${answer}`) ?? boards[0]; }; -/* The preview and its tab strip for one card. The active tab carries - the patina dot and the surface's answer, so the choice reads without - hovering; the tooltip carries the summary rows. */ -function renderHubPreview(cardNode, rows) { +/* CSS cannot divide one length by another to reach the unitless number + scale() needs, so that number is measured here: the clone's own desktop + pane, read unscaled through offsetWidth, against the frame it has to + sit in. The frame is one fixed shape for every card, so the pane is + fitted rather than stretched to it and centred in whichever direction + it falls short. The handset beside the pane lands outside the frame, + which is the crop. */ +function fitHubPreview(cardNode) { const previewSlot = cardNode.querySelector('[data-hub-preview]'); - const tabsSlot = cardNode.querySelector('[data-hub-tabs]'); - const screen = cardNode.dataset.hubTarget; - const group = HUB_GROUPS[screen]; + if (!previewSlot || previewSlot.classList.contains('picker-hub-preview--icons')) return; + const proof = previewSlot.firstElementChild; + const pane = proof?.querySelector('.ps-desktop, .pv-desktop') ?? proof?.firstElementChild; + const width = previewSlot.clientWidth; + const height = previewSlot.clientHeight; + if (!width || !height || !pane?.offsetWidth || !pane.offsetHeight) return; + const scale = Math.min(width / pane.offsetWidth, height / pane.offsetHeight); + cardNode.style.setProperty('--pk-hub-scale', scale); + cardNode.style.setProperty('--pk-hub-x', `${(width - pane.offsetWidth * scale) / 2}px`); + cardNode.style.setProperty('--pk-hub-y', `${(height - pane.offsetHeight * scale) / 2}px`); +} + +/* One card's miniature, for the surface the slider is parked on. */ +function renderHubPreview(cardNode, screen, group, mode) { + const previewSlot = cardNode.querySelector('[data-hub-preview]'); + previewSlot.classList.toggle('picker-hub-preview--icons', group === 'icon-pack'); if (group === 'icon-pack') { - tabsSlot.hidden = true; - tabsSlot.replaceChildren(); - previewSlot.replaceChildren(hubProof(iconSheet)); + /* The 16px row alone. A pack is chosen for interface work and that row + is the same glyphs at the size they will be read at, so it is the + specimen worth showing; the magnified field above it on screen 11 is + a reading this card has no room for and no need of. */ + previewSlot.replaceChildren(hubProof(iconStrip)); /* The glyphs are fetched on screen 11's first arrival, which a run that stops at the hub never makes; fetch them here and re-render once. The empty marker is the failed state, which must not refetch @@ -5445,14 +5426,9 @@ function renderHubPreview(cardNode, rows) { } return; } - const modes = rows.map((row) => row.mode); - const mode = modes.includes(cardNode.dataset.hubSurface) ? cardNode.dataset.hubSurface : modes[0]; - cardNode.dataset.hubSurface = mode ?? ''; const board = mode ? hubQuestionBoard(screen, mode) : null; if (!board) { previewSlot.replaceChildren(); - tabsSlot.hidden = true; - tabsSlot.replaceChildren(); return; } if (group === 'motion-energy') { @@ -5462,82 +5438,62 @@ function renderHubPreview(cardNode, rows) { previewSlot.replaceChildren(proof); /* The route stops are measured off the clone's own layout, exactly what screen 06 does for the originals on arrival. */ - requestAnimationFrame(() => plotMotionRoute(proof.firstChild)); + requestAnimationFrame(() => { + plotMotionRoute(proof.firstChild); + fitHubPreview(cardNode); + }); } else { previewSlot.replaceChildren(hubProof(board, hubPreviewMarks(mode))); } - tabsSlot.hidden = rows.length === 0; - tabsSlot.replaceChildren(...rows.map((row) => { - const tab = document.createElement('span'); - tab.className = 'picker-hub-tab'; - tab.dataset.hubTab = row.mode; - const dot = document.createElement('i'); - dot.className = 'picker-hub-tab-dot'; - dot.setAttribute('aria-hidden', 'true'); - tab.append(dot, hubSurfaceLabel(row.mode)); - if (row.mode === mode) { - tab.dataset.on = ''; - const value = document.createElement('span'); - value.className = 'picker-hub-tab-value'; - value.textContent = hubOptionTitle(group, row.value); - tab.append(value); - } - return tab; - })); + fitHubPreview(cardNode); } +/* A card reads top to bottom: the question's name, a miniature of its own + board, the line saying what the current answer does, and the control + that walks the surfaces. One surface is shown at a time, so the note and + the label both speak for whichever one the slider is parked on. */ function renderHubCard(cardNode) { - const group = HUB_GROUPS[cardNode.dataset.hubTarget]; + const opener = cardNode.querySelector('[data-hub-target]'); + const screen = opener?.dataset.hubTarget; + const group = HUB_GROUPS[screen]; if (!group) return; - const summary = cardNode.querySelector('[data-hub-summary]'); - const mark = cardNode.querySelector('[data-hub-mark]'); const previewSlot = cardNode.querySelector('[data-hub-preview]'); - const tabsSlot = cardNode.querySelector('[data-hub-tabs]'); - const target = document.querySelector(`.picker-screen[data-screen="${cardNode.dataset.hubTarget}"]`); + const note = cardNode.querySelector('[data-hub-note]'); + const slider = cardNode.querySelector('[data-hub-slider]'); + const label = cardNode.querySelector('[data-hub-slider-label]'); + const target = document.querySelector(`.picker-screen[data-screen="${screen}"]`); const skipped = Boolean(target?.hasAttribute('data-skip')); cardNode.classList.toggle('is-skipped', skipped); - cardNode.disabled = skipped; - cardNode.setAttribute('aria-disabled', skipped ? 'true' : 'false'); + opener.disabled = skipped; + opener.setAttribute('aria-disabled', skipped ? 'true' : 'false'); if (skipped) { - summary.replaceChildren(); - previewSlot.replaceChildren(hubPlainLine('Not asked of these surfaces')); - tabsSlot.hidden = true; - tabsSlot.replaceChildren(); - cardNode.classList.remove('is-edited'); - mark.hidden = true; + previewSlot.replaceChildren(); + note.replaceChildren(hubPlainLine('Not asked of these surfaces')); + note.hidden = false; + slider.hidden = true; return; } const rows = hubSurfaceRows(group); - let edited; - let lines; - if (rows.length === 0) { - const checked = document.querySelector(`input[name="${group}"]:checked`); - edited = hubEdited.has(group); - lines = [hubRow( - 'All surfaces', - checked ? hubOptionTitle(group, checked.value) : '', - group, - checked?.value, - )]; - } else { - edited = rows.some((row) => row.chosen); - if (rows.length > 1 && rows.every((row) => row.value === rows[0].value)) { - lines = [hubRow('All surfaces', hubOptionTitle(group, rows[0].value), group, rows[0].value)]; - } else if (rows.length === 1) { - lines = [hubRow(hubSurfaceLabel(rows[0].mode), hubOptionTitle(group, rows[0].value), group, rows[0].value)]; - } else { - lines = rows.map((row) => hubRow( - hubSurfaceLabel(row.mode), - hubOptionTitle(group, row.value), - group, - row.value, - )); - } - } - summary.replaceChildren(...lines); - cardNode.classList.toggle('is-edited', edited); - mark.hidden = !edited; - renderHubPreview(cardNode, rows); + const modes = rows.map((row) => row.mode); + const mode = modes.includes(cardNode.dataset.hubSurface) ? cardNode.dataset.hubSurface : modes[0]; + cardNode.dataset.hubSurface = mode ?? ''; + /* A flat question answers for the whole run, so its value comes off the + checked radio and it has no surfaces to walk. */ + const value = mode + ? rows.find((row) => row.mode === mode)?.value + : document.querySelector(`input[name="${group}"]:checked`)?.value; + const why = value ? hubOptionDesc(group, value) : ''; + note.textContent = why; + note.hidden = !why; + const answer = document.createElement('span'); + answer.className = 'picker-hub-slider-value'; + answer.textContent = value ? hubOptionTitle(group, value) : ''; + label.replaceChildren(...(mode ? [`${hubSurfaceLabel(mode)} ยท `, answer] : [answer])); + slider.hidden = false; + /* One surface is not a trip, so the chevrons stand down and the strip + stays as the answer's label. */ + for (const step of slider.querySelectorAll('[data-hub-step]')) step.hidden = modes.length < 2; + renderHubPreview(cardNode, screen, group, mode); } function renderHub() { @@ -5545,50 +5501,52 @@ function renderHub() { for (const cardNode of hubScreen.querySelectorAll('.picker-hub-card')) renderHubCard(cardNode); } -/* The tooltip floats above the card and flips below when the card sits - too near the viewport top to hold it; when neither side holds the - whole panel, the roomier side takes it. Measured when the pointer or - focus arrives, before the reveal transition starts; the tip is - opacity-hidden rather than display-hidden, so its height is real. */ -function placeHubTip(cardNode) { - const tip = cardNode.querySelector('[data-hub-tip]'); - if (!tip) return; - const rect = cardNode.getBoundingClientRect(); - const need = tip.offsetHeight + 20; - const above = rect.top; - const below = window.innerHeight - rect.bottom; - cardNode.dataset.tipAt = (above >= need || above >= below) ? 'above' : 'below'; +function stepHubSurface(cardNode, step) { + const opener = cardNode?.querySelector('[data-hub-target]'); + const group = HUB_GROUPS[opener?.dataset.hubTarget]; + if (!group) return; + const modes = hubSurfaceRows(group).map((row) => row.mode); + if (modes.length < 2) return; + const at = modes.indexOf(cardNode.dataset.hubSurface); + cardNode.dataset.hubSurface = modes[(at + step + modes.length) % modes.length]; + renderHubCard(cardNode); } -hubScreen?.addEventListener('pointerover', (event) => { - const cardNode = event.target.closest('.picker-hub-card'); - if (cardNode) placeHubTip(cardNode); -}); -hubScreen?.addEventListener('focusin', (event) => { - const cardNode = event.target.closest('.picker-hub-card'); - if (cardNode) placeHubTip(cardNode); -}); /* Cards and the finish CTA jump by screen id. The inline nav script owns goTo(); it listens for this event, so no swap logic is - duplicated here. A disabled card never reaches this handler. A mini - tab hit re-clones its own card instead and never travels: the card - click must stay the jump to the question. */ + duplicated here. A step button is a sibling of the opener rather than + a child of it, so walking the surfaces never travels to the question + and needs nothing stopped. */ hubScreen?.addEventListener('click', (event) => { - const tab = event.target.closest('[data-hub-tab]'); - if (tab) { - event.stopPropagation(); - const cardNode = tab.closest('.picker-hub-card'); - cardNode.dataset.hubSurface = tab.dataset.hubTab; - renderHubCard(cardNode); + const step = event.target.closest('[data-hub-step]'); + if (step) { + stepHubSurface(step.closest('.picker-hub-card'), Number(step.dataset.hubStep)); return; } - const cardNode = event.target.closest('[data-hub-target]'); - if (!cardNode || cardNode.disabled) return; + const opener = event.target.closest('[data-hub-target]'); + if (!opener || opener.disabled) return; document.dispatchEvent(new CustomEvent('picker:goto', { - detail: { screen: cardNode.dataset.hubTarget }, + detail: { screen: opener.dataset.hubTarget }, })); }); +/* Arrow keys walk the surfaces too: a control that reads as a slider owes + a keyboard the same trip its chevrons offer a pointer. */ +hubScreen?.addEventListener('keydown', (event) => { + const step = { ArrowLeft: -1, ArrowRight: 1 }[event.key]; + if (!step || !event.target.closest('[data-hub-slider]')) return; + event.preventDefault(); + stepHubSurface(event.target.closest('.picker-hub-card'), step); +}); + +/* The grid flips to one full-width column at the narrow breakpoint, and a + scale measured against the three-column width would stay behind. */ +if (hubScreen && window.ResizeObserver) { + new ResizeObserver(() => { + for (const cardNode of hubScreen.querySelectorAll('.picker-hub-card')) fitHubPreview(cardNode); + }).observe(hubScreen); +} + for (const input of modeInputs) { input.addEventListener('change', () => { syncModesNext(); diff --git a/picker/styles/picker.css b/picker/styles/picker.css index da34afa59..ed9e97484 100644 --- a/picker/styles/picker.css +++ b/picker/styles/picker.css @@ -8120,12 +8120,14 @@ body.picker-page { } /* ============================================================ - Screen 04b: the configure hub. Five near-uniform preview cards - flow a three-column grid, three over two; the column wrappers - are display: contents so the markup's masonry grouping costs - nothing. Sharp corners and hairline rules, the same vocabulary - as the option rows. Previews, tabs, and the tooltip's summary - rows are written by renderHub(). + Screen 04b: the configure hub. Five preview cards flow a + three-column grid, three over two; the column wrappers are + display: contents so the markup's grouping costs nothing. + Every card reads top to bottom and centred: the question's + name, a miniature of its own board, the one line saying what + the current answer does, and the control that walks the + surfaces. Sharp corners and hairline rules, the same + vocabulary as the option rows. ============================================================ */ .picker-hub { display: grid; @@ -8139,29 +8141,43 @@ body.picker-page { font-weight: 300; } +/* Six tracks so a card can take two of them: three across the top, and + the remaining pair centred underneath by starting one track in. */ .picker-hub-grid { display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); + grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 14px; align-items: stretch; width: min(100%, 1080px); } -.picker-hub-col { - display: contents; +.picker-hub-card { + grid-column: span 2; } +.picker-hub-card:nth-child(4) { + grid-column: 2 / span 2; +} + +/* Every card lays its four parts on the same four rows of the grid, so a + two-line note on one card and a one-line note on its neighbour still + leave both titles on one line, both previews on the next, and both + sliders on the last. Centring each card's own stack instead let the + taller stack start higher, which is what pushed Corners' title above + Depth's. */ .picker-hub-card { position: relative; + grid-row: span 4; display: grid; - gap: 16px; + grid-template-rows: subgrid; + align-items: center; + justify-items: center; width: 100%; - padding: 24px 28px; - text-align: left; + padding: 22px 20px; + text-align: center; background: var(--ks-lacquer-raised); border: 1px solid var(--ks-rule); border-radius: 0; - cursor: pointer; transition: background-color 180ms var(--ks-ease), border-color 180ms var(--ks-ease); @@ -8171,28 +8187,34 @@ body.picker-page { border-color: color-mix(in oklab, var(--ks-patina) 45%, var(--ks-rule)); } -.picker-hub-card:focus-visible { - outline: 2px solid var(--ks-patina-deep); - outline-offset: -2px; - z-index: 1; +/* The title is the control, and its stretched pseudo-element carries + the whole card's click without wrapping it. A card that wrapped its + contents in a button could not hold the slider's own buttons, which + is why the surfaces used to be spans no keyboard could reach. */ +.picker-hub-open { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 0; + border: 0; + background: none; + color: inherit; + font-family: var(--ks-font); + cursor: pointer; } -.picker-hub-eyebrow { +.picker-hub-open::after { + content: ""; position: absolute; - top: 24px; - right: 28px; - color: var(--ks-text-faint); - font-size: var(--ks-type-eyebrow-size); - font-weight: 500; - letter-spacing: var(--ks-type-eyebrow-track); - text-transform: uppercase; + inset: 0; +} + +.picker-hub-open:focus-visible { + outline: 2px solid var(--ks-patina-deep); + outline-offset: -2px; } .picker-hub-title { - display: flex; - align-items: center; - gap: 8px; - padding-right: 72px; color: var(--ks-champagne); font-size: var(--ks-type-title-size); font-weight: var(--ks-type-title-weight); @@ -8212,121 +8234,125 @@ body.picker-page { transform: translateX(2px); } -.picker-hub-mark { - margin-left: auto; - padding: 2px 8px; - border: 1px solid var(--ks-kinpaku-deep); - color: var(--ks-kinpaku); - font-size: var(--ks-type-eyebrow-size); - font-weight: 500; - letter-spacing: var(--ks-type-eyebrow-track); - text-transform: uppercase; -} - -.picker-hub-summary { - display: grid; - gap: 14px; - color: var(--ks-text-muted); - font-size: 0.86rem; +/* Why the current answer is a sound default, read off the option + row's own description by renderHubCard(). */ +/* Top of its row rather than centred in it: the row is as tall as the + longest note in the band, and a shorter one centred there would drop + its first line below its neighbour's. */ +.picker-hub-note { + align-self: start; + max-width: 34ch; + color: var(--ks-text-faint); + font-size: 0.84rem; line-height: 1.5; } -.picker-hub-entry { - display: grid; - gap: 8px; +.picker-hub-note[hidden] { + display: none; } .picker-hub-line { display: block; } -/* Surface pill wraps the label and the committed value; patina dot - plus the text mix mirror the surface tabs' answered state. */ -.picker-hub-pill { - display: inline-flex; - align-items: center; - justify-content: space-between; - gap: 12px; - width: fit-content; - max-width: 100%; - padding: 6px 9px; - color: var(--ks-text-faint); - font-size: 12.5px; - letter-spacing: 0.015em; - border: 1px solid var(--ks-rule); - border-radius: 2px; -} - -.picker-hub-pill-label { - flex: 0 0 auto; -} - -.picker-hub-value { - display: inline-flex; - align-items: center; - gap: 8px; - color: color-mix(in oklab, var(--ks-patina) 85%, var(--ks-champagne)); - text-align: right; -} - -.picker-hub-dot { - flex: 0 0 auto; - width: 7px; - height: 7px; - border-radius: 50%; - background: var(--ks-patina); -} - -/* Why this answer is a sound default, read off the option row's own - description by renderHub(). One note per answer row. */ -.picker-hub-note { - padding-inline: 2px; - color: var(--ks-text-faint); - font-size: 0.84rem; - line-height: 1.5; -} - -/* The card body: a clone of the question's own artboard, written by - renderHubPreview(). The frame borrows design-context.css's - .dcx-proof--board hooks for the committed bodies, so no rendition - rule is restated here. Pointer events stay off: everything inside - the card except a mini tab is the jump to the question. */ +/* The board is authored at 918px wide and mixes fixed pixels, + viewport clamps, and container queries keyed to absolute height, so + reflowing it into a card crushes it: the layout shrinks and the + drawn detail does not. Laid out at its natural width it resolves + exactly as it does on its own screen, and one transform shrinks the + finished picture. The frame crops to the desktop pane, so the + handset beside it falls outside. renderHubCard() measures the pane + and writes the scale and the ratio; the fallbacks below are the + four-surface geometry, so the frame is never wrong-shaped even for + the frame before the measurement lands. */ .picker-hub-preview { + position: relative; display: block; + width: 100%; + /* A miniature, never a magnification: past the pane's own width the + frame would blow the board up bigger than the screen it came from + ever draws it. The cap only bites in the single-column collapse. */ + max-width: 560px; + /* One shape for all five. The desktop panes differ by about a percent + between the artboard and the derived drawings, and measuring each + card's own pane made five frames of five heights. The pane is fitted + inside this box instead, so the cards line up. */ + aspect-ratio: 1.34; + overflow: hidden; + background: var(--ks-lacquer); + border: 1px solid var(--ks-rule); pointer-events: none; } +.picker-hub-preview > .picker-hub-proof { + position: absolute; + top: 0; + left: 0; + width: var(--pk-hub-natural, 918px); + transform: translate(var(--pk-hub-x, 0px), var(--pk-hub-y, 0px)) scale(var(--pk-hub-scale, 0.32)); + transform-origin: top left; +} + +/* The icon sheet takes the same frame as the boards beside it, filled by + a grid of its own glyphs. They stay a hard 16px, which is the size a + pack is chosen to be read at and the reason this one preview is drawn + rather than scaled; the room between them is the frame's, not theirs. */ +.picker-hub-preview--icons > .picker-hub-proof { + position: absolute; + inset: 0; + width: auto; + transform: none; +} + +.picker-hub-preview--icons .picker-icon-strip-row { + display: grid; + grid-template-columns: repeat(6, minmax(0, 1fr)); + grid-auto-rows: minmax(0, 1fr); + place-items: center; + gap: 0; + height: 100%; + padding: 10px 8px; + color: var(--ks-text-muted); +} + .picker-hub-proof { display: block; } -/* The mini tab strip: the surface-tab vocabulary at card scale. Spans - rather than buttons because the card itself is one, and a button - cannot hold another. */ -.picker-hub-tabs { - display: flex; - flex-wrap: wrap; - gap: 3px; - width: fit-content; +/* One surface at a time, the chevrons wrapped around the surface and + the answer it holds. The strip keeps the surface tabs' vocabulary, + and it sits above the stretched opener so its buttons take their + own clicks. */ +.picker-hub-slider { + position: relative; + z-index: 1; + display: inline-flex; + align-items: center; + gap: 4px; max-width: 100%; + /* A card with nothing to walk stands its chevrons down, and the strip + would shrink to the label and sit lower than the strips beside it. + The height is the one with chevrons in it. */ + min-height: 34px; padding: 3px; background: var(--ks-lacquer-raised); border: 1px solid var(--ks-rule); border-radius: 2px; } -.picker-hub-tabs[hidden] { +.picker-hub-slider[hidden] { display: none; } -.picker-hub-tab { - display: inline-flex; - align-items: center; - gap: 8px; - padding: 6px 10px; +.picker-hub-step { + display: grid; + place-items: center; + width: 26px; + height: 26px; + padding: 0; color: var(--ks-text-muted); - font-size: 12.5px; - letter-spacing: 0.015em; + background: none; + border: 0; border-radius: 2px; cursor: pointer; transition: @@ -8334,99 +8360,43 @@ body.picker-page { color 180ms var(--ks-ease); } -.picker-hub-tab:hover { +.picker-hub-step:hover { color: var(--ks-champagne); - background: color-mix(in oklab, var(--ks-patina) 9%, var(--ks-lacquer-raised)); + background: color-mix(in oklab, var(--ks-patina) 12%, var(--ks-lacquer-raised)); } -.picker-hub-tab[data-on] { - color: var(--ks-champagne); - background: color-mix(in oklab, var(--ks-patina) 14%, var(--ks-lacquer-raised)); +.picker-hub-step:focus-visible { + outline: 2px solid var(--ks-patina-deep); + outline-offset: -2px; } -/* The same report the full-size strip's dot gives: filled on the tab - being previewed, hollow on the rest. */ -.picker-hub-tab-dot { - flex: none; - width: 7px; - height: 7px; - border: 1px solid color-mix(in oklab, var(--ks-patina) 55%, transparent); - border-radius: 50%; +.picker-hub-step[hidden] { + display: none; } -.picker-hub-tab[data-on] .picker-hub-tab-dot { - background: var(--ks-patina); - border-color: var(--ks-patina); +.picker-hub-step svg { + width: 13px; + height: 13px; + fill: none; + stroke: currentColor; + stroke-width: 1.5; + stroke-linecap: square; } -.picker-hub-tab-value { +.picker-hub-slider-label { + padding-inline: 8px; + color: var(--ks-text-muted); + font-size: 12.5px; + letter-spacing: 0.015em; +} + +.picker-hub-slider-value { color: color-mix(in oklab, var(--ks-patina) 85%, var(--ks-champagne)); } -/* The summary rows' new home: a floating panel on the card's own - hover and focus, the contrast alert tip's pattern at card width. - Above by default; placeHubTip() flips it below when the card sits - too near the viewport top. The hovered card rises over its - neighbours so the panel is never clipped by the masonry. */ -.picker-hub-card:is(:hover, :focus-within) { - z-index: 3; -} - -.picker-hub-tip { - position: absolute; - left: -1px; - right: -1px; - bottom: calc(100% + 10px); - z-index: 12; - padding: 16px 18px; - display: grid; - gap: 14px; - opacity: 0; - background: var(--ks-lacquer-raised); - border: 1px solid var(--ks-rule); - border-radius: 2px; - pointer-events: none; - transform: translateY(3px); - transition: - opacity 180ms var(--ks-ease), - transform 180ms var(--ks-ease); -} - -.picker-hub-tip::after { - content: ""; - position: absolute; - left: 50%; - bottom: -5px; - width: 8px; - height: 8px; - background: var(--ks-lacquer-raised); - border-right: 1px solid var(--ks-rule); - border-bottom: 1px solid var(--ks-rule); - transform: translateX(-50%) rotate(45deg); -} - -.picker-hub-card[data-tip-at="below"] .picker-hub-tip { - top: calc(100% + 10px); - bottom: auto; - transform: translateY(-3px); -} - -.picker-hub-card[data-tip-at="below"] .picker-hub-tip::after { - top: -5px; - bottom: auto; - border: 0; - border-left: 1px solid var(--ks-rule); - border-top: 1px solid var(--ks-rule); -} - -.picker-hub-card:not(.is-skipped):is(:hover, :focus-within) .picker-hub-tip { - opacity: 1; - transform: translateY(0); -} - @media (prefers-reduced-motion: reduce) { - .picker-hub-tip, - .picker-hub-tab { + .picker-hub-step, + .picker-hub-arrow { transition: none; } } @@ -8442,8 +8412,7 @@ body.picker-page { } .picker-hub-card.is-skipped .picker-hub-title, -.picker-hub-card.is-skipped .picker-hub-eyebrow, -.picker-hub-card.is-skipped .picker-hub-summary, +.picker-hub-card.is-skipped .picker-hub-note, .picker-hub-card.is-skipped .picker-hub-arrow { color: var(--ks-text-mute-deep); } @@ -8455,4 +8424,10 @@ body.picker-page { .picker-hub-grid { grid-template-columns: minmax(0, 1fr); } + + /* One track, so the two-track spans and the centring offset both go. */ + .picker-hub-card, + .picker-hub-card:nth-child(4) { + grid-column: auto; + } }