mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-12 14:16:28 +03:00
5334 lines
144 KiB
CSS
5334 lines
144 KiB
CSS
/* Vendored from the site's design system (now in the private impeccable-site
|
|
repo) so the picker stays self-contained. Update by re-copying from there. */
|
|
@import "./vendor/kinpaku-tokens.css";
|
|
@import "./vendor/kinpaku-kit.css";
|
|
|
|
/* Ink for surfaces painted in a color the user chose, not in one of ours:
|
|
swatch labels and the wireframe artboards. Every --ks-* text token swaps
|
|
between the light and dark themes, which would invert these against
|
|
palettes that never changed. Values are the system's own ink and raised
|
|
paper, so neither is pure black or pure white. contrastInk() in
|
|
scripts/color.js mirrors their luminance; change both together. */
|
|
:root {
|
|
--pk-ink-dark: oklch(14% 0.018 95);
|
|
--pk-ink-light: oklch(99% 0.008 95);
|
|
}
|
|
|
|
/* Scoped subset of the reset imported by site/styles/main.css through
|
|
site/styles/tokens.css (lines 12-27). */
|
|
.picker-page,
|
|
.picker-page *,
|
|
.picker-page *::before,
|
|
.picker-page *::after {
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.picker-page * {
|
|
margin: 0;
|
|
}
|
|
|
|
.picker-page :is(button, input, textarea, select) {
|
|
font-family: inherit;
|
|
}
|
|
|
|
body.picker-page {
|
|
margin: 0;
|
|
min-height: 100vh;
|
|
min-height: 100dvh;
|
|
background: var(--ks-lacquer);
|
|
color: var(--ks-text);
|
|
font-family: var(--ks-font);
|
|
font-size: var(--ks-type-body-size);
|
|
line-height: var(--ks-type-body-line);
|
|
-webkit-font-smoothing: antialiased;
|
|
-moz-osx-font-smoothing: grayscale;
|
|
text-rendering: optimizeLegibility;
|
|
}
|
|
|
|
/* The gold-leaf face, as the site sets it: a flat kinpaku fill with the leaf
|
|
asset over it at 38%, not the leaf itself as the fill. The texture is a
|
|
pseudo-element because a background-image cannot carry its own opacity, and
|
|
the fixed 340px render keeps the crack structure readable at button size
|
|
where `cover` would average the asset into flat gold.
|
|
|
|
Kept here rather than in the vendored kit for one reason: the picker serves
|
|
its own compressed copy of the leaf from its own assets directory, so the
|
|
URL has to come from --pk-foil (set on <body> in layouts/Picker.astro). The
|
|
rest matches `.ks-button.ks-button-primary` in the site's kinpaku-kit.css,
|
|
including the hover that lifts to vivid gold rather than the pale beige the
|
|
vendored copy still carries. */
|
|
.picker-page .ks-button.ks-button-primary {
|
|
position: relative;
|
|
isolation: isolate;
|
|
}
|
|
|
|
/* The paint is gated on :not([disabled]) so it never competes with the disabled
|
|
state below. Without the gate this selector is (0,3,0) and the kit's
|
|
`.ks-button[disabled]` is (0,2,0), so a disabled button kept the full gold
|
|
fill and read as clickable. */
|
|
.picker-page .ks-button.ks-button-primary:not([disabled]) {
|
|
color: var(--ks-dark-ink);
|
|
background: var(--ks-kinpaku);
|
|
border-color: var(--ks-kinpaku);
|
|
}
|
|
|
|
.picker-page .ks-button.ks-button-primary::before {
|
|
content: "";
|
|
position: absolute;
|
|
inset: 0;
|
|
z-index: -1;
|
|
border-radius: inherit;
|
|
background: var(--pk-foil) 68% 44% / 340px auto no-repeat;
|
|
opacity: 0.38;
|
|
pointer-events: none;
|
|
transition: opacity 180ms var(--ks-ease);
|
|
}
|
|
|
|
.picker-page .ks-button.ks-button-primary:not([disabled]):hover {
|
|
color: var(--ks-dark-ink);
|
|
background: var(--ks-kinpaku-vivid);
|
|
border-color: var(--ks-kinpaku-vivid);
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
.picker-page .ks-button.ks-button-primary:not([disabled]):hover::before {
|
|
opacity: 0.26;
|
|
}
|
|
|
|
.picker-page .ks-button.ks-button-primary:not([disabled]):active {
|
|
background: var(--ks-kinpaku-rich);
|
|
border-color: var(--ks-kinpaku-rich);
|
|
transform: translateY(0);
|
|
}
|
|
|
|
/* Select this palette waits for a committed color, and the kit's disabled state
|
|
drops the fill to transparent. The leaf has to go with it, or the texture
|
|
floats on the page with no gold under it. */
|
|
.picker-page .ks-button.ks-button-primary[disabled]::before {
|
|
opacity: 0;
|
|
}
|
|
|
|
/* A disabled button answering the pointer is the other half of the same bug:
|
|
the kit's `.ks-button.ks-button-secondary:hover` is (0,3,0) and outranks its
|
|
own `.ks-button[disabled]` at (0,2,0), so the tint and the lift still fired
|
|
on a button that cannot be pressed. Restating the state here puts it at
|
|
(0,3,0) at rest and (0,4,0) under the pointer, which clears every variant
|
|
rule in the kit without an !important. */
|
|
.picker-page .ks-button[disabled],
|
|
.picker-page .ks-button[disabled]:hover,
|
|
.picker-page .ks-button[disabled]:active {
|
|
color: var(--ks-text-mute-deep);
|
|
background: transparent;
|
|
border-color: var(--ks-rule);
|
|
cursor: not-allowed;
|
|
transform: none;
|
|
}
|
|
|
|
.picker-shell {
|
|
position: relative;
|
|
min-height: 100vh;
|
|
min-height: 100svh;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.picker-hero-art {
|
|
position: absolute;
|
|
inset: 0;
|
|
z-index: 0;
|
|
background-position: left center;
|
|
background-size: cover;
|
|
background-repeat: no-repeat;
|
|
/* Same grade the site gives .hero-rebuild-art over this asset. */
|
|
filter: saturate(1.2) contrast(1.08);
|
|
pointer-events: none;
|
|
transition: opacity 180ms var(--ks-ease);
|
|
}
|
|
|
|
#picker-form {
|
|
position: relative;
|
|
z-index: 1;
|
|
}
|
|
|
|
.picker-screen {
|
|
min-height: 100vh;
|
|
min-height: 100svh;
|
|
display: grid;
|
|
align-items: center;
|
|
}
|
|
|
|
.picker-screen:not([data-active]) {
|
|
display: none;
|
|
}
|
|
|
|
/* The palette screen and the strategy screen show the same two things: the four
|
|
chosen colors, and the page they are being judged on. Between those two the
|
|
objects move and resize rather than being redrawn, so each carries a name on
|
|
both of its copies. An inactive screen is display:none and therefore not
|
|
rendered, which is what leaves each name on exactly one element at capture.
|
|
|
|
Named per role rather than once for the whole strip: each color then travels
|
|
to its own slot, so a strip that was reordered reads as four colors moving to
|
|
new places instead of one block sliding. */
|
|
:is(.picker-screen[data-screen="02"], .picker-screen[data-screen="03"]) .picker-band[data-band="primary"] {
|
|
view-transition-name: pk-band-primary;
|
|
}
|
|
|
|
:is(.picker-screen[data-screen="02"], .picker-screen[data-screen="03"]) .picker-band[data-band="secondary"] {
|
|
view-transition-name: pk-band-secondary;
|
|
}
|
|
|
|
:is(.picker-screen[data-screen="02"], .picker-screen[data-screen="03"]) .picker-band[data-band="tertiary"] {
|
|
view-transition-name: pk-band-tertiary;
|
|
}
|
|
|
|
:is(.picker-screen[data-screen="02"], .picker-screen[data-screen="03"]) .picker-band[data-band="neutral"] {
|
|
view-transition-name: pk-band-neutral;
|
|
}
|
|
|
|
/* Both screens now judge their answer on the same drawing, so the pair the
|
|
transition carries is one component in two sizes: the frame grows out of the
|
|
palette screen's rather than dissolving into a different page. The hidden
|
|
surfaces are display: none and so are not rendered, which is what keeps the
|
|
name unique while every chosen surface stays mounted. */
|
|
.picker-screen[data-screen="02"] .picker-preview,
|
|
.picker-screen[data-screen="03"] .picker-strategy-stage > .picker-preview {
|
|
view-transition-name: pk-test-page;
|
|
}
|
|
|
|
::view-transition-group(*) {
|
|
animation-duration: 340ms;
|
|
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
|
|
}
|
|
|
|
/* Mirrors site/styles/home-rebuild.css's .hero-rebuild-container width math. */
|
|
.picker-container {
|
|
position: relative;
|
|
width: 100%;
|
|
max-width: 1500px;
|
|
margin: 0 auto;
|
|
padding-inline: 56px;
|
|
}
|
|
|
|
.picker-screen[data-screen="01"] .picker-container {
|
|
padding-inline: 75px;
|
|
}
|
|
|
|
.picker-start {
|
|
max-width: 620px;
|
|
display: grid;
|
|
justify-items: start;
|
|
gap: 28px;
|
|
}
|
|
|
|
/* Brand lockup, matching the site header: carved-tile mark plus tracked
|
|
wordmark, both in kinpaku gold. */
|
|
.picker-brand {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
color: var(--ks-kinpaku);
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.picker-brand-logo {
|
|
display: grid;
|
|
place-items: center;
|
|
width: 34px;
|
|
height: 34px;
|
|
}
|
|
|
|
.picker-brand-logo svg {
|
|
width: 32px;
|
|
height: 32px;
|
|
}
|
|
|
|
.picker-brand-name {
|
|
font-family: var(--ks-font-wordmark);
|
|
font-size: 1.3rem;
|
|
font-weight: 400;
|
|
line-height: 1;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.picker-title {
|
|
color: var(--ks-champagne);
|
|
font-family: var(--ks-font-display);
|
|
font-size: var(--ks-type-display-size);
|
|
font-weight: var(--ks-type-display-weight);
|
|
line-height: var(--ks-type-display-line);
|
|
letter-spacing: var(--ks-type-display-track);
|
|
text-wrap: balance;
|
|
}
|
|
|
|
.picker-actions {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 14px;
|
|
margin-top: 4px;
|
|
}
|
|
|
|
.picker-key-hint {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
gap: 12px;
|
|
margin-top: 12px;
|
|
color: var(--ks-text-muted);
|
|
}
|
|
|
|
.picker-key-cluster {
|
|
display: grid;
|
|
gap: 4px;
|
|
}
|
|
|
|
.picker-key-row {
|
|
display: flex;
|
|
gap: 4px;
|
|
}
|
|
|
|
.picker-key-row-top {
|
|
justify-content: center;
|
|
}
|
|
|
|
.picker-keycap {
|
|
min-width: 26px;
|
|
height: 26px;
|
|
display: inline-grid;
|
|
place-items: center;
|
|
padding-inline: 6px;
|
|
background: var(--ks-lacquer-raised);
|
|
border: 1px solid var(--ks-rule);
|
|
border-radius: 2px;
|
|
color: var(--ks-champagne);
|
|
font-family: var(--ks-mono);
|
|
font-size: var(--ks-type-mono-size);
|
|
line-height: 1;
|
|
}
|
|
|
|
.picker-key-caption {
|
|
color: var(--ks-text-muted);
|
|
font-size: var(--ks-type-mono-size);
|
|
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="01b"]) .picker-hero-art,
|
|
.picker-shell:has(#picker-form[data-current="02"]) .picker-hero-art,
|
|
.picker-shell:has(#picker-form[data-current="03"]) .picker-hero-art,
|
|
.picker-shell:has(#picker-form[data-current="04"]) .picker-hero-art,
|
|
.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,
|
|
.picker-shell:has(#picker-form[data-current="11"]) .picker-hero-art {
|
|
opacity: 0;
|
|
}
|
|
|
|
/* One hairline per step, gold once passed, plus the count and the name of the
|
|
step in hand so the position is legible without counting ticks. */
|
|
.picker-progress {
|
|
position: fixed;
|
|
top: 32px;
|
|
left: max(56px, calc((100vw - 1500px) / 2 + 56px));
|
|
z-index: 5;
|
|
display: grid;
|
|
gap: 10px;
|
|
transition: opacity 180ms var(--ks-ease);
|
|
}
|
|
|
|
/* Eleven steps hold the same total width five did: the row is a measure of
|
|
how far along the interview is, and a wider one would start competing with
|
|
the question. The ticks divide whatever that width is. */
|
|
.picker-progress-track {
|
|
width: 232px;
|
|
display: grid;
|
|
grid-template-columns: repeat(11, minmax(0, 1fr));
|
|
gap: 5px;
|
|
}
|
|
|
|
.picker-progress-track i {
|
|
height: 2px;
|
|
background: var(--ks-rule);
|
|
transition: background-color 260ms var(--ks-ease);
|
|
}
|
|
|
|
.picker-progress[data-step="1"] .picker-progress-track i:nth-child(-n + 1),
|
|
.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: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:nth-child(-n + 10),
|
|
.picker-progress[data-step="11"] .picker-progress-track i {
|
|
background: var(--ks-kinpaku);
|
|
}
|
|
|
|
.picker-progress-label {
|
|
display: flex;
|
|
align-items: baseline;
|
|
gap: 10px;
|
|
font-family: var(--ks-font);
|
|
font-size: 0.68rem;
|
|
letter-spacing: 0.16em;
|
|
text-transform: uppercase;
|
|
color: var(--ks-text-muted);
|
|
}
|
|
|
|
.picker-progress-index b {
|
|
color: var(--ks-kinpaku);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.picker-progress-name {
|
|
color: var(--ks-champagne);
|
|
letter-spacing: 0.1em;
|
|
}
|
|
|
|
#picker-form[data-current="01"] .picker-progress {
|
|
opacity: 0;
|
|
visibility: hidden;
|
|
}
|
|
|
|
.picker-palette {
|
|
display: grid;
|
|
gap: 48px;
|
|
padding-block: clamp(32px, 5svh, 48px) clamp(24px, 3.5svh, 48px);
|
|
}
|
|
|
|
/* Every question wears the site's section-heading treatment: the same thin
|
|
display face `.slop-teaser-title` gets inside `.slop-pattern-head` on the
|
|
homepage. Centered rather than set in a 15ch column, because a question owns
|
|
the top of the screen instead of leading a left-hand text block. */
|
|
.picker-question-title {
|
|
margin: 0;
|
|
color: var(--ks-champagne);
|
|
font-family: var(--ks-font-display);
|
|
font-size: clamp(3rem, 5.6vw, 5.5rem);
|
|
font-weight: 100;
|
|
line-height: 1.04;
|
|
letter-spacing: -0.005em;
|
|
text-align: center;
|
|
text-wrap: balance;
|
|
}
|
|
|
|
.picker-palette-grid {
|
|
display: grid;
|
|
grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
|
|
gap: 50px;
|
|
align-items: center;
|
|
}
|
|
|
|
.picker-palette-subgrid {
|
|
display: grid;
|
|
grid-template-rows: auto auto;
|
|
gap: 30px;
|
|
min-width: 0;
|
|
align-self: start;
|
|
}
|
|
|
|
.picker-palette-subgrid > .picker-actions-stack {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 14px;
|
|
}
|
|
|
|
.picker-deck-column {
|
|
--picker-card-size: min(39vw, 660px, calc(100svh - 292px));
|
|
display: grid;
|
|
justify-items: center;
|
|
gap: 16px;
|
|
min-width: 0;
|
|
}
|
|
|
|
.picker-deck-stage {
|
|
position: relative;
|
|
width: calc(var(--picker-card-size) + 64px);
|
|
height: var(--picker-card-size);
|
|
perspective: 1000px;
|
|
perspective-origin: center 42%;
|
|
}
|
|
|
|
.picker-deck-scroller {
|
|
position: absolute;
|
|
inset: 0;
|
|
z-index: 1;
|
|
overflow-y: auto;
|
|
overflow-x: hidden;
|
|
scroll-snap-type: y mandatory;
|
|
scrollbar-width: none;
|
|
overscroll-behavior: contain;
|
|
}
|
|
|
|
.picker-deck-scroller::-webkit-scrollbar {
|
|
display: none;
|
|
}
|
|
|
|
.picker-snap-point {
|
|
height: var(--picker-card-size);
|
|
scroll-snap-align: start;
|
|
scroll-snap-stop: always;
|
|
}
|
|
|
|
.picker-card-layer {
|
|
position: absolute;
|
|
inset: 0;
|
|
z-index: 2;
|
|
pointer-events: none;
|
|
transform-style: preserve-3d;
|
|
}
|
|
|
|
.picker-card {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
z-index: 0;
|
|
width: var(--picker-card-size);
|
|
opacity: 0;
|
|
backface-visibility: hidden;
|
|
transform-origin: center center;
|
|
transform-style: preserve-3d;
|
|
transition:
|
|
opacity 240ms var(--ks-ease),
|
|
transform 240ms var(--ks-ease);
|
|
}
|
|
|
|
.picker-card[data-pos="0"] {
|
|
z-index: 5;
|
|
opacity: 1;
|
|
transform: translateX(0) rotateY(0) rotateZ(0) rotateX(0) translateZ(0);
|
|
}
|
|
|
|
.picker-card[data-pos="1"] {
|
|
z-index: 4;
|
|
opacity: 0.52;
|
|
transform: translateX(11%) rotateY(0) rotateZ(6deg) rotateX(0) translateZ(-160px);
|
|
}
|
|
|
|
.picker-card[data-pos="2"] {
|
|
z-index: 3;
|
|
opacity: 0.2;
|
|
transform: translateX(19%) rotateY(0) rotateZ(8deg) rotateX(0) translateZ(-180px);
|
|
}
|
|
|
|
.picker-card[data-pos="-1"] {
|
|
z-index: 4;
|
|
opacity: 0.52;
|
|
transform: translateX(-11%) rotateY(0) rotateZ(-6deg) rotateX(0) translateZ(-160px);
|
|
}
|
|
|
|
.picker-card[data-pos="-2"] {
|
|
z-index: 3;
|
|
opacity: 0.2;
|
|
transform: translateX(-19%) rotateY(0) rotateZ(-8deg) rotateX(0) translateZ(-180px);
|
|
}
|
|
|
|
.picker-card.is-far {
|
|
opacity: 0;
|
|
transform: translateX(0) rotateY(0) rotateZ(0) rotateX(0) translateZ(-220px);
|
|
}
|
|
|
|
.picker-card[data-exit="left"] {
|
|
z-index: 6;
|
|
animation: picker-card-exit-left 280ms var(--ks-ease) both;
|
|
}
|
|
|
|
.picker-card[data-exit="right"] {
|
|
z-index: 6;
|
|
animation: picker-card-exit-right 280ms var(--ks-ease) both;
|
|
}
|
|
|
|
@keyframes picker-card-exit-left {
|
|
0% {
|
|
opacity: 1;
|
|
transform: translateX(0) rotateY(0) rotateZ(0) rotateX(0) translateZ(0);
|
|
}
|
|
|
|
52% {
|
|
opacity: 0.78;
|
|
transform: translateX(-116%) rotateY(-24deg) rotateZ(0) rotateX(2deg) translateZ(-156px);
|
|
}
|
|
|
|
100% {
|
|
opacity: 0.52;
|
|
transform: translateX(-11%) rotateY(0) rotateZ(-6deg) rotateX(0) translateZ(-160px);
|
|
}
|
|
}
|
|
|
|
@keyframes picker-card-exit-right {
|
|
0% {
|
|
opacity: 1;
|
|
transform: translateX(0) rotateY(0) rotateZ(0) rotateX(0) translateZ(0);
|
|
}
|
|
|
|
52% {
|
|
opacity: 0.78;
|
|
transform: translateX(116%) rotateY(24deg) rotateZ(0) rotateX(2deg) translateZ(-156px);
|
|
}
|
|
|
|
100% {
|
|
opacity: 0.52;
|
|
transform: translateX(11%) rotateY(0) rotateZ(6deg) rotateX(0) translateZ(-160px);
|
|
}
|
|
}
|
|
|
|
.picker-card-face {
|
|
position: relative;
|
|
width: var(--picker-card-size);
|
|
aspect-ratio: 1;
|
|
overflow: hidden;
|
|
background: var(--ks-lacquer-raised);
|
|
border: 1px solid var(--ks-rule);
|
|
border-radius: 2px;
|
|
}
|
|
|
|
.picker-cue-face img {
|
|
width: 100%;
|
|
height: 100%;
|
|
display: block;
|
|
object-fit: cover;
|
|
}
|
|
|
|
.picker-seed-face {
|
|
display: grid;
|
|
grid-template-columns: repeat(4, 1fr);
|
|
}
|
|
|
|
.picker-seed-face span {
|
|
background: var(--seed-color);
|
|
}
|
|
|
|
.picker-ring {
|
|
position: absolute;
|
|
left: var(--x);
|
|
top: var(--y);
|
|
z-index: 2;
|
|
width: 45px;
|
|
height: 45px;
|
|
padding: 0;
|
|
border: 0;
|
|
border-radius: 50%;
|
|
background: transparent;
|
|
cursor: grab;
|
|
opacity: 1;
|
|
pointer-events: none;
|
|
transform: translate(-50%, -50%);
|
|
touch-action: none;
|
|
transition: opacity 180ms var(--ks-ease);
|
|
}
|
|
|
|
.picker-ring::before {
|
|
content: "";
|
|
position: absolute;
|
|
inset: 6px;
|
|
background: var(--marker-color);
|
|
border: 2px solid var(--ks-lacquer-raised);
|
|
border-radius: 50%;
|
|
box-shadow:
|
|
0 0 12px color-mix(in srgb, var(--ks-champagne) 16%, transparent),
|
|
inset 0 0 0 1px color-mix(in srgb, var(--ks-champagne) 40%, transparent);
|
|
}
|
|
|
|
.picker-ring::after {
|
|
content: "";
|
|
position: absolute;
|
|
inset: 1px;
|
|
border: 2px solid white;
|
|
border-radius: 50%;
|
|
/* Dark edge on both sides of the white hairline so the hover highlight
|
|
reads over the neutral's pale image regions, not only over saturated
|
|
ones. Fixed near-black, not a theme token: it sits on the photo. */
|
|
box-shadow:
|
|
0 0 0 1.5px oklch(15% 0.006 95 / 0.6),
|
|
inset 0 0 0 1.5px oklch(15% 0.006 95 / 0.6);
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
transform: scale(0.78);
|
|
transition:
|
|
opacity 180ms var(--ks-ease),
|
|
transform 180ms var(--ks-ease);
|
|
}
|
|
|
|
.picker-palette:has(.picker-band[data-band="primary"]:is(:hover, :focus-within))
|
|
.picker-card[data-pos="0"] .picker-ring[data-role="primary"]::after,
|
|
.picker-palette:has(.picker-band[data-band="secondary"]:is(:hover, :focus-within))
|
|
.picker-card[data-pos="0"] .picker-ring[data-role="secondary"]::after,
|
|
.picker-palette:has(.picker-band[data-band="tertiary"]:is(:hover, :focus-within))
|
|
.picker-card[data-pos="0"] .picker-ring[data-role="tertiary"]::after,
|
|
.picker-palette:has(.picker-band[data-band="neutral"]:is(:hover, :focus-within))
|
|
.picker-card[data-pos="0"] .picker-ring[data-role="neutral"]::after {
|
|
opacity: 1;
|
|
transform: scale(1);
|
|
}
|
|
|
|
.picker-card[data-pos="0"] .picker-ring {
|
|
pointer-events: auto;
|
|
}
|
|
|
|
.picker-ring:is(:focus-visible, [data-dragging]) {
|
|
z-index: 3;
|
|
}
|
|
|
|
.picker-ring[data-dragging] {
|
|
cursor: grabbing;
|
|
}
|
|
|
|
.picker-ring[data-detached] {
|
|
opacity: 0.4;
|
|
}
|
|
|
|
.picker-ring:focus-visible {
|
|
outline: none;
|
|
}
|
|
|
|
.picker-ring:focus-visible::before {
|
|
outline: 2px solid var(--ks-patina);
|
|
outline-offset: 3px;
|
|
}
|
|
|
|
.picker-deck-controls {
|
|
min-width: 260px;
|
|
display: grid;
|
|
grid-template-columns: 44px minmax(120px, 1fr) 44px;
|
|
gap: 14px;
|
|
align-items: center;
|
|
}
|
|
|
|
.picker-ring-guide {
|
|
min-height: 2.9em;
|
|
max-width: 38ch;
|
|
color: var(--ks-text-muted);
|
|
font-size: 0.82rem;
|
|
line-height: 1.45;
|
|
text-align: center;
|
|
transition: opacity 200ms var(--ks-ease);
|
|
}
|
|
|
|
.picker-ring-guide span {
|
|
display: block;
|
|
}
|
|
|
|
.picker-ring-guide[aria-hidden="true"] {
|
|
opacity: 0;
|
|
}
|
|
|
|
.picker-icon-button {
|
|
position: relative;
|
|
width: 44px;
|
|
height: 44px;
|
|
display: inline-grid;
|
|
place-items: center;
|
|
padding: 0;
|
|
background: transparent;
|
|
border: 1px solid var(--ks-rule);
|
|
border-radius: 2px;
|
|
color: var(--ks-champagne);
|
|
cursor: pointer;
|
|
transition:
|
|
background-color 180ms var(--ks-ease),
|
|
border-color 180ms var(--ks-ease),
|
|
color 180ms var(--ks-ease);
|
|
}
|
|
|
|
.picker-icon-button:hover {
|
|
background: var(--ks-graphite);
|
|
border-color: var(--ks-patina);
|
|
color: var(--ks-patina);
|
|
}
|
|
|
|
.picker-icon-button:focus-visible,
|
|
.picker-band:focus-visible,
|
|
.picker-band-tool:focus-visible,
|
|
.picker-tint:focus-visible,
|
|
.picker-tint-close:focus-visible {
|
|
outline: 2px solid var(--ks-patina);
|
|
outline-offset: 3px;
|
|
}
|
|
|
|
.picker-icon-button:disabled {
|
|
color: var(--ks-text-mute-deep);
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.picker-icon-button svg,
|
|
.picker-band-tool svg {
|
|
width: 18px;
|
|
height: 18px;
|
|
fill: none;
|
|
stroke: currentColor;
|
|
stroke-width: 1.5;
|
|
stroke-linecap: round;
|
|
stroke-linejoin: round;
|
|
}
|
|
|
|
.picker-deck-count {
|
|
color: var(--ks-text-muted);
|
|
font-family: var(--ks-mono);
|
|
font-size: var(--ks-type-mono-size);
|
|
line-height: 1.35;
|
|
text-align: center;
|
|
}
|
|
|
|
.picker-palette-panel {
|
|
position: relative;
|
|
min-width: 0;
|
|
display: grid;
|
|
gap: 18px;
|
|
padding: 30px;
|
|
background: var(--ks-lacquer-raised);
|
|
border: 1px solid var(--ks-rule);
|
|
border-radius: 2px;
|
|
}
|
|
|
|
.picker-palette-panel > legend {
|
|
position: absolute;
|
|
width: 1px;
|
|
height: 1px;
|
|
overflow: hidden;
|
|
clip: rect(0 0 0 0);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.picker-reset {
|
|
position: absolute;
|
|
top: -22px;
|
|
right: 18px;
|
|
z-index: 4;
|
|
background: var(--ks-lacquer-raised);
|
|
}
|
|
|
|
.picker-bands {
|
|
display: grid;
|
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
gap: 1px;
|
|
background: var(--ks-rule);
|
|
border: 1px solid var(--ks-rule);
|
|
}
|
|
|
|
.picker-band-item {
|
|
min-width: 0;
|
|
background: var(--ks-lacquer-raised);
|
|
}
|
|
|
|
.picker-band {
|
|
--band-color: var(--ks-graphite);
|
|
--band-ink: var(--ks-champagne);
|
|
position: relative;
|
|
height: 175px;
|
|
background: var(--band-color);
|
|
color: var(--band-ink);
|
|
}
|
|
|
|
.picker-band output {
|
|
position: absolute;
|
|
right: 14px;
|
|
bottom: 14px;
|
|
left: 14px;
|
|
font-family: var(--ks-mono);
|
|
font-size: var(--ks-type-mono-size);
|
|
font-weight: 500;
|
|
letter-spacing: 0.04em;
|
|
line-height: 1;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.picker-band-tools {
|
|
position: absolute;
|
|
top: 12px;
|
|
right: 10px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6px;
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
transition: opacity 180ms var(--ks-ease);
|
|
}
|
|
|
|
.picker-band:hover .picker-band-tools,
|
|
.picker-band:focus-within .picker-band-tools {
|
|
opacity: 1;
|
|
pointer-events: auto;
|
|
}
|
|
|
|
/* With the strip open the edit button is also its close button, so it is the
|
|
one tool that stays live. The other two would act on a color the strip is in
|
|
the middle of changing. */
|
|
.picker-band-item[data-tint-open] .picker-band-tool:is([data-copy-color], [data-custom-color]) {
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.picker-band-tool {
|
|
position: relative;
|
|
width: 32px;
|
|
height: 32px;
|
|
display: grid;
|
|
place-items: center;
|
|
padding: 0;
|
|
background: transparent;
|
|
border: 0;
|
|
border-radius: 2px;
|
|
color: inherit;
|
|
cursor: pointer;
|
|
opacity: 0.72;
|
|
transition: opacity 180ms var(--ks-ease);
|
|
}
|
|
|
|
.picker-band-tool:is(:hover, :focus-visible) {
|
|
opacity: 1;
|
|
}
|
|
|
|
.picker-band-tool svg {
|
|
width: 17px;
|
|
height: 17px;
|
|
}
|
|
|
|
.picker-tip::after {
|
|
content: attr(data-tip);
|
|
position: absolute;
|
|
left: 50%;
|
|
bottom: calc(100% + 8px);
|
|
z-index: 12;
|
|
width: max-content;
|
|
max-width: 210px;
|
|
padding: 7px 9px;
|
|
opacity: 0;
|
|
background: var(--ks-lacquer-raised);
|
|
border: 1px solid var(--ks-rule);
|
|
border-radius: 2px;
|
|
color: var(--ks-text);
|
|
font-size: 0.76rem;
|
|
line-height: 1.25;
|
|
pointer-events: none;
|
|
transform: translate(-50%, 3px);
|
|
transition:
|
|
opacity 180ms var(--ks-ease),
|
|
transform 180ms var(--ks-ease);
|
|
}
|
|
|
|
.picker-tip:hover::after,
|
|
.picker-tip:focus-visible::after {
|
|
opacity: 1;
|
|
transform: translate(-50%, 0);
|
|
}
|
|
|
|
.picker-band-tool.picker-tip::after {
|
|
top: 50%;
|
|
right: calc(100% + 8px);
|
|
bottom: auto;
|
|
left: auto;
|
|
transform: translate(3px, -50%);
|
|
}
|
|
|
|
.picker-band-tool.picker-tip:is(:hover, :focus-visible)::after {
|
|
transform: translate(0, -50%);
|
|
}
|
|
|
|
.picker-native-color {
|
|
position: absolute;
|
|
width: 1px;
|
|
height: 1px;
|
|
overflow: hidden;
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
}
|
|
|
|
/* Reorder grip. Dragging a band carries its color to another slot; the slots
|
|
keep their roles, so the label in the foot holds still while the band above
|
|
it travels. The affordance stays quiet because the color is the subject of
|
|
the row: no border of its own, and it strengthens on hover and on focus. */
|
|
.picker-band-grip {
|
|
position: absolute;
|
|
top: 5px;
|
|
left: 5px;
|
|
z-index: 2;
|
|
width: 36px;
|
|
height: 36px;
|
|
display: grid;
|
|
place-items: center;
|
|
padding: 0;
|
|
background: transparent;
|
|
border: 0;
|
|
border-radius: 2px;
|
|
color: inherit;
|
|
cursor: grab;
|
|
opacity: 0.42;
|
|
touch-action: none;
|
|
transition: opacity 180ms var(--ks-ease);
|
|
}
|
|
|
|
.picker-band-grip svg {
|
|
width: 22px;
|
|
height: 22px;
|
|
fill: currentColor;
|
|
stroke: none;
|
|
}
|
|
|
|
.picker-band:hover .picker-band-grip,
|
|
.picker-band:focus-within .picker-band-grip {
|
|
opacity: 0.9;
|
|
}
|
|
|
|
.picker-band-grip:focus-visible {
|
|
opacity: 1;
|
|
outline: 2px solid var(--ks-patina);
|
|
outline-offset: 3px;
|
|
}
|
|
|
|
.picker-band-grip[data-dragging] {
|
|
cursor: grabbing;
|
|
opacity: 1;
|
|
}
|
|
|
|
/* The leftmost grip sits against the panel's left edge, so its tip reads to the
|
|
side there is room on rather than to the left like the tools' tips do. */
|
|
.picker-band-grip.picker-tip::after {
|
|
top: 50%;
|
|
right: auto;
|
|
bottom: auto;
|
|
left: calc(100% + 8px);
|
|
transform: translate(-3px, -50%);
|
|
}
|
|
|
|
.picker-band-grip.picker-tip:is(:hover, :focus-visible)::after {
|
|
transform: translate(0, -50%);
|
|
}
|
|
|
|
/* The tip has said its piece by the time the band is moving, and under the
|
|
pointer it would cover the color being placed. */
|
|
.picker-band-grip[data-dragging].picker-tip::after {
|
|
opacity: 0;
|
|
transition: none;
|
|
}
|
|
|
|
/* A lifted band rides over its neighbours and carries a hairline of its own
|
|
ink, since this screen has no shadows to borrow. The ring fades rather than
|
|
cuts, so it is gone by the time the band hands its color to the slot it
|
|
landed on and the swap itself is invisible. */
|
|
.picker-band[data-dragging] {
|
|
z-index: 6;
|
|
cursor: grabbing;
|
|
}
|
|
|
|
.picker-band::after {
|
|
content: "";
|
|
position: absolute;
|
|
inset: 0;
|
|
opacity: 0;
|
|
outline: 1px solid color-mix(in srgb, currentColor 55%, transparent);
|
|
outline-offset: -1px;
|
|
pointer-events: none;
|
|
transition: opacity 140ms var(--ks-ease);
|
|
}
|
|
|
|
.picker-band[data-dragging]::after {
|
|
opacity: 1;
|
|
}
|
|
|
|
.picker-band-status {
|
|
position: absolute;
|
|
width: 1px;
|
|
height: 1px;
|
|
overflow: hidden;
|
|
clip: rect(0 0 0 0);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.picker-band-grip,
|
|
.picker-band::after {
|
|
transition: none;
|
|
}
|
|
}
|
|
|
|
.picker-band-foot {
|
|
position: relative;
|
|
height: 46px;
|
|
display: grid;
|
|
place-items: center;
|
|
}
|
|
|
|
.picker-band-foot h2 {
|
|
color: var(--ks-champagne);
|
|
font-size: var(--ks-type-title-size);
|
|
font-weight: var(--ks-type-title-weight);
|
|
line-height: var(--ks-type-title-line);
|
|
}
|
|
|
|
.picker-band-item[data-tint-open] .picker-band-foot h2 {
|
|
visibility: hidden;
|
|
}
|
|
|
|
/* The same band, restated on the strategy screen under the choices. It is a
|
|
reading of the palette and a handle to reorder it, not a place to edit it, so
|
|
the tools come off and what is left fits in the column the options leave
|
|
behind: a little over a third of the height the editable band needs. */
|
|
.picker-bands--compact .picker-band {
|
|
/* The strip's own height less the foot and the group's two hairlines. */
|
|
height: calc(var(--pk-band, 96px) - 32px);
|
|
}
|
|
|
|
/* Read from the far corner rather than under the grip: at the short end of the
|
|
band's range the swatch is not tall enough to stack them. */
|
|
.picker-bands--compact .picker-band output {
|
|
right: 8px;
|
|
bottom: 8px;
|
|
left: 8px;
|
|
text-align: right;
|
|
}
|
|
|
|
.picker-bands--compact .picker-band-grip {
|
|
top: 3px;
|
|
left: 3px;
|
|
width: 30px;
|
|
height: 30px;
|
|
}
|
|
|
|
.picker-bands--compact .picker-band-grip svg {
|
|
width: 19px;
|
|
height: 19px;
|
|
}
|
|
|
|
.picker-bands--compact .picker-band-foot {
|
|
height: 30px;
|
|
}
|
|
|
|
.picker-bands--compact .picker-band-foot h2 {
|
|
font-size: 0.96rem;
|
|
}
|
|
|
|
.picker-tint-strip {
|
|
position: absolute;
|
|
top: 9px;
|
|
left: 0;
|
|
z-index: 8;
|
|
display: flex;
|
|
background: var(--ks-lacquer-raised);
|
|
border: 1px solid var(--ks-rule);
|
|
}
|
|
|
|
.picker-band-item[data-band-item="secondary"] .picker-tint-strip {
|
|
left: -56px;
|
|
}
|
|
|
|
.picker-band-item:is([data-band-item="tertiary"], [data-band-item="neutral"]) .picker-tint-strip {
|
|
right: 0;
|
|
left: auto;
|
|
}
|
|
|
|
.picker-tint-strip[hidden] {
|
|
display: none;
|
|
}
|
|
|
|
.picker-tint,
|
|
.picker-tint-close {
|
|
position: relative;
|
|
width: 28px;
|
|
height: 28px;
|
|
padding: 0;
|
|
border: 0;
|
|
border-right: 1px solid var(--ks-rule);
|
|
border-radius: 0;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.picker-tint {
|
|
background: var(--tint-color);
|
|
}
|
|
|
|
.picker-tint[data-current]::after {
|
|
content: "";
|
|
position: absolute;
|
|
right: 8px;
|
|
bottom: 3px;
|
|
left: 8px;
|
|
height: 2px;
|
|
background: var(--ks-champagne);
|
|
}
|
|
|
|
.picker-tint-close {
|
|
background: var(--ks-lacquer-raised);
|
|
border-right: 0;
|
|
color: var(--ks-champagne);
|
|
font-size: 1.1rem;
|
|
line-height: 1;
|
|
}
|
|
|
|
.picker-preview {
|
|
--pv-primary: var(--ks-champagne);
|
|
--pv-secondary: var(--ks-patina);
|
|
--pv-tertiary: var(--ks-kinpaku);
|
|
--pv-neutral: var(--ks-lacquer-raised);
|
|
--pv-n-ink: var(--ks-champagne);
|
|
--pv-p-ink: var(--ks-champagne);
|
|
--pv-surface-2: color-mix(in oklab, var(--pv-neutral) 92%, var(--pv-n-ink));
|
|
--pv-bone: color-mix(in oklab, var(--pv-neutral) 62%, var(--pv-n-ink));
|
|
--pv-strong: color-mix(in oklab, var(--pv-neutral) 22%, var(--pv-n-ink));
|
|
--pv-secondary-wash: color-mix(in oklab, var(--pv-secondary) 28%, var(--pv-neutral));
|
|
--pv-tertiary-wash: color-mix(in oklab, var(--pv-tertiary) 22%, var(--pv-neutral));
|
|
width: 100%;
|
|
aspect-ratio: 16 / 7;
|
|
display: grid;
|
|
grid-template-columns: minmax(0, 1fr) auto;
|
|
gap: 16px;
|
|
}
|
|
|
|
/* Two artboards floating on the page itself: a desktop card and a phone card,
|
|
both in the same shell of shared chrome. What fills their bodies is the
|
|
variant the chosen surface brings, so the palette is judged on the kind of
|
|
page it is going to dress. The phone's fixed portrait ratio keeps the wide
|
|
frame from stretching a single page mock. */
|
|
.pv-desktop,
|
|
.pv-phone {
|
|
min-width: 0;
|
|
min-height: 0;
|
|
overflow: hidden;
|
|
background: var(--pv-neutral);
|
|
border: 1px solid var(--ks-rule);
|
|
}
|
|
|
|
.pv-desktop {
|
|
display: grid;
|
|
grid-template-rows: auto 1fr;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.pv-phone {
|
|
height: 100%;
|
|
aspect-ratio: 9 / 19;
|
|
display: grid;
|
|
grid-template-rows: auto 1fr auto;
|
|
border-radius: 10px;
|
|
}
|
|
|
|
.pv-nav {
|
|
display: grid;
|
|
grid-template-columns: auto 1fr auto;
|
|
gap: 10px;
|
|
align-items: center;
|
|
padding: 10px 12px;
|
|
}
|
|
|
|
.pv-logo {
|
|
width: 8px;
|
|
aspect-ratio: 1;
|
|
background: var(--pv-primary);
|
|
border-radius: 50%;
|
|
}
|
|
|
|
.pv-nav-bars {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: 8px;
|
|
}
|
|
|
|
.pv-nav-bars i {
|
|
display: block;
|
|
width: 18px;
|
|
height: 3px;
|
|
background: var(--pv-bone);
|
|
border-radius: 2px;
|
|
}
|
|
|
|
.pv-pill {
|
|
width: 32px;
|
|
height: 12px;
|
|
background: var(--pv-primary);
|
|
border-radius: 2px;
|
|
}
|
|
|
|
.pv-hero {
|
|
min-height: 0;
|
|
display: grid;
|
|
grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr);
|
|
gap: 8%;
|
|
align-items: center;
|
|
padding: 4px 8% 14px;
|
|
}
|
|
|
|
.pv-hero-copy {
|
|
display: grid;
|
|
gap: 8px;
|
|
}
|
|
|
|
.pv-eyebrow {
|
|
width: 22%;
|
|
height: 4px;
|
|
background: var(--pv-tertiary);
|
|
border-radius: 2px;
|
|
}
|
|
|
|
/* The headline is display type set in the brand color, not permanent black
|
|
ink: it must visibly react when the primary changes. */
|
|
.pv-headline {
|
|
display: grid;
|
|
gap: 7px;
|
|
margin-block: 2px 3px;
|
|
}
|
|
|
|
.pv-headline i {
|
|
display: block;
|
|
width: 88%;
|
|
height: 8px;
|
|
background: color-mix(in oklab, var(--pv-primary) 80%, var(--pv-strong));
|
|
border-radius: 2px;
|
|
}
|
|
|
|
.pv-headline i:last-child {
|
|
width: 58%;
|
|
}
|
|
|
|
.pv-copy-bar {
|
|
width: 64%;
|
|
height: 5px;
|
|
background: var(--pv-bone);
|
|
border-radius: 2px;
|
|
}
|
|
|
|
.pv-actions {
|
|
display: flex;
|
|
gap: 7px;
|
|
margin-top: 10px;
|
|
}
|
|
|
|
.pv-actions i {
|
|
width: 38px;
|
|
height: 13px;
|
|
background: var(--pv-primary);
|
|
border-radius: 2px;
|
|
}
|
|
|
|
.pv-actions i:last-child {
|
|
background: transparent;
|
|
border: 1px solid var(--pv-secondary);
|
|
}
|
|
|
|
.pv-image {
|
|
position: relative;
|
|
width: 100%;
|
|
aspect-ratio: 4 / 3;
|
|
background: linear-gradient(135deg, var(--pv-secondary-wash), color-mix(in oklab, var(--pv-secondary) 52%, var(--pv-neutral)));
|
|
border: 1px solid var(--pv-secondary);
|
|
border-radius: 2px;
|
|
}
|
|
|
|
.pv-image::after {
|
|
content: "";
|
|
position: absolute;
|
|
top: 9px;
|
|
right: 9px;
|
|
width: 7px;
|
|
aspect-ratio: 1;
|
|
background: var(--pv-tertiary);
|
|
border-radius: 50%;
|
|
}
|
|
|
|
.pv-avatar {
|
|
width: 9px;
|
|
aspect-ratio: 1;
|
|
border: 1px solid var(--pv-bone);
|
|
border-radius: 50%;
|
|
}
|
|
|
|
/* Phone artboard: the desktop page narrowed, same hero plate over the same two
|
|
blocks, so the palette is judged making one argument at two widths. The
|
|
blocks each carry a different role, keeping the secondary and tertiary
|
|
visible as accents rather than only as washes. */
|
|
.pv-phone-top {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 9px 10px;
|
|
background: var(--pv-surface-2);
|
|
border-bottom: 1px solid var(--ks-rule);
|
|
}
|
|
|
|
.pv-phone-body {
|
|
min-height: 0;
|
|
display: grid;
|
|
/* The plate takes the row that flexes and the two blocks keep their drawn
|
|
size, so a narrower window shortens the hero rather than pushing a block
|
|
out through the bottom of the phone. */
|
|
grid-template-rows: minmax(0, 1fr) auto auto;
|
|
gap: 7px;
|
|
padding: 9px 10px 10px;
|
|
}
|
|
|
|
/* Its 4 / 3 is honored wherever the row is tall enough to hold it, which is
|
|
every window past the width the component stops growing at. Below that the
|
|
plate gives up height instead of overflowing. */
|
|
.pv-phone-body > .pv-image {
|
|
align-self: center;
|
|
max-height: 100%;
|
|
}
|
|
|
|
.pv-mini-card {
|
|
display: grid;
|
|
gap: 5px;
|
|
padding: 8px 9px;
|
|
background: var(--pv-surface-2);
|
|
border: 1px solid var(--ks-rule);
|
|
border-radius: 2px;
|
|
}
|
|
|
|
.pv-mini-card i,
|
|
.pv-mini-card span {
|
|
display: block;
|
|
height: 3px;
|
|
background: var(--pv-bone);
|
|
border-radius: 2px;
|
|
}
|
|
|
|
.pv-mini-card span {
|
|
width: 62%;
|
|
height: 6px;
|
|
background: var(--pv-secondary);
|
|
}
|
|
|
|
/* Keyed off the pair rather than a position, so the hero plate sitting above
|
|
them cannot shift which block reads as tertiary. */
|
|
.pv-mini-card + .pv-mini-card span {
|
|
background: var(--pv-tertiary);
|
|
}
|
|
|
|
.pv-tabbar {
|
|
display: flex;
|
|
justify-content: space-evenly;
|
|
align-items: center;
|
|
padding: 8px 0;
|
|
border-top: 1px solid var(--ks-rule);
|
|
}
|
|
|
|
.pv-tabbar i {
|
|
width: 5px;
|
|
aspect-ratio: 1;
|
|
background: var(--pv-bone);
|
|
border-radius: 50%;
|
|
}
|
|
|
|
.pv-tabbar i:first-child {
|
|
background: var(--pv-primary);
|
|
}
|
|
|
|
/* ── Screen 01b: which surfaces ──────────────────────────────
|
|
Four tiles, one per mode the skill recognizes, chosen before any visual
|
|
question so every later answer knows what kind of page it is dressing.
|
|
Multi-select: a product is allowed to be an app with a landing page and
|
|
docs. All four previews are drawn in the one demo palette set on the frame
|
|
below: color has not been chosen yet, so no tile is allowed to read as a
|
|
better color decision than the three beside it. */
|
|
/* Everything on this screen is cut from one measure: the width the palette
|
|
screen's preview is currently rendering at, and the scale this tile reduces
|
|
it to. Every tile shows that component itself under a transform rather than
|
|
a second copy tuned by hand, which is the only way the two screens can
|
|
agree pixel for pixel.
|
|
|
|
--pk-pv-natural mirrors the palette screen's own preview width, which grows
|
|
at 5/12 of the viewport and pins at 495.5px once the window passes 1500px.
|
|
Measured at 1280 / 1366 / 1440 / 1512 wide, where the line is exact. Re-measure
|
|
it if screen 02's grid ever changes. The scale steps by viewport height
|
|
because CSS cannot divide one length by another to reach the unitless number
|
|
scale() needs, and the frame's height is derived from both so the reduced
|
|
component fills the frame exactly. */
|
|
.picker-modes {
|
|
--pk-pv-natural: min(495.5px, calc(41.667vw - 129.5px));
|
|
--pk-pv-scale: 0.62;
|
|
--pk-mode-frame: calc(var(--pk-pv-natural) * 7 / 16 * var(--pk-pv-scale));
|
|
--pk-mode-pad: clamp(14px, 2svh, 24px);
|
|
--pk-mode-gap: clamp(16px, 2.2svh, 24px);
|
|
display: grid;
|
|
gap: clamp(16px, 2.6svh, 36px);
|
|
justify-items: center;
|
|
padding-block: clamp(20px, 3svh, 44px) clamp(14px, 2.2svh, 34px);
|
|
}
|
|
|
|
@media (min-height: 1040px) {
|
|
.picker-modes {
|
|
--pk-pv-scale: 0.68;
|
|
}
|
|
}
|
|
|
|
/* Short windows spend their room on the frames rather than on the chrome
|
|
around them: below roughly 100px tall the drawn previews start pressing
|
|
their bars against the frame edge, so the page padding and gutters give way
|
|
first. */
|
|
@media (max-height: 760px) {
|
|
.picker-modes {
|
|
--pk-pv-scale: 0.58;
|
|
--pk-mode-pad: 12px;
|
|
--pk-mode-gap: 12px;
|
|
gap: 14px;
|
|
padding-block: 14px 10px;
|
|
}
|
|
}
|
|
|
|
/* A wide short window is the hard case: the reference width has already pinned
|
|
at its maximum, so only the scale is left to give. Narrow short windows need
|
|
no such step, because their reference width is smaller to begin with. */
|
|
@media (max-height: 760px) and (min-width: 1500px) {
|
|
.picker-modes {
|
|
--pk-pv-scale: 0.5;
|
|
}
|
|
}
|
|
|
|
@media (max-height: 700px) and (min-width: 1500px) {
|
|
.picker-modes {
|
|
--pk-pv-scale: 0.47;
|
|
}
|
|
}
|
|
|
|
.picker-modes-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
/* Both rows take the taller row's height, so a goal or chip row that wraps
|
|
in one tile cannot leave the other three sitting shorter than it. */
|
|
grid-template-rows: repeat(2, minmax(0, 1fr));
|
|
gap: var(--pk-mode-gap);
|
|
/* Two tiles wide at the frame's largest, so a tall viewport never drives a
|
|
frame wider than the tile holding it. Below that the frame is matted by
|
|
the tile rather than the tile shrinking, because a narrower tile wraps
|
|
the chips and goal onto extra lines and costs more height than the
|
|
matting saves. */
|
|
width: min(100%, 860px);
|
|
margin: 0;
|
|
padding: 0;
|
|
border: 0;
|
|
}
|
|
|
|
.picker-modes-grid legend {
|
|
position: absolute;
|
|
width: 1px;
|
|
height: 1px;
|
|
overflow: hidden;
|
|
clip: rect(0 0 0 0);
|
|
clip-path: inset(50%);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.picker-mode-tile {
|
|
position: relative;
|
|
display: grid;
|
|
grid-template-rows: auto 1fr;
|
|
gap: clamp(12px, 1.9svh, 20px);
|
|
padding: var(--pk-mode-pad);
|
|
background: var(--ks-lacquer-raised);
|
|
border: 1px solid var(--ks-rule);
|
|
cursor: pointer;
|
|
transition:
|
|
border-color 180ms var(--ks-ease),
|
|
background-color 180ms var(--ks-ease);
|
|
}
|
|
|
|
.picker-mode-tile input {
|
|
position: absolute;
|
|
width: 1px;
|
|
height: 1px;
|
|
overflow: hidden;
|
|
clip: rect(0 0 0 0);
|
|
clip-path: inset(50%);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.picker-mode-tile:hover {
|
|
border-color: color-mix(in oklab, var(--ks-patina) 45%, var(--ks-rule));
|
|
}
|
|
|
|
.picker-mode-tile:has(input:checked) {
|
|
background: color-mix(in oklab, var(--ks-patina) 9%, var(--ks-lacquer-raised));
|
|
border-color: var(--ks-patina);
|
|
}
|
|
|
|
.picker-mode-tile:has(input:focus-visible) {
|
|
outline: 2px solid var(--ks-patina-deep);
|
|
outline-offset: -2px;
|
|
z-index: 1;
|
|
}
|
|
|
|
/* The box every tile's preview is matted into: one measure, cut from the
|
|
palette screen's own preview width and handed to all four modes. */
|
|
.picker-mode-preview {
|
|
/* The height is the budget and the width follows it, at the same 16 / 7 the
|
|
palette screen's preview uses. Letting the tile set the width instead
|
|
stretched the desktop card to about five times its own ratio. */
|
|
height: var(--pk-mode-frame);
|
|
aspect-ratio: 16 / 7;
|
|
width: auto;
|
|
max-width: 100%;
|
|
justify-self: center;
|
|
}
|
|
|
|
/* Every mode shows the palette screen's preview itself, at that component's own
|
|
canonical size, reduced by a single transform. Nothing inside is restated
|
|
here: a second set of sizes tuned to this smaller box is how the two screens
|
|
drifted apart in the first place. The frame is sized from the same scale, so
|
|
the reduced component fills it exactly. */
|
|
.picker-mode-preview--exact {
|
|
display: block;
|
|
position: relative;
|
|
}
|
|
|
|
.picker-mode-preview--exact > .picker-preview {
|
|
/* The demo palette every tile is drawn in. It lives here rather than inline
|
|
on each tile so the four cannot drift: the palette screen has not been
|
|
reached yet, and without it the component falls back to the picker's own
|
|
dark chrome.
|
|
|
|
These four are hex and outside DESIGN.md on purpose. They stand in for the
|
|
palette the visitor is about to pick, so they are an example rather than a
|
|
system color, which is the carve-out DESIGN.md's OKLCH-Only Rule already
|
|
names. Do not move them into DESIGN.md: that file mirrors
|
|
kinpaku-tokens.css, and admitting a teal here would make every stray teal
|
|
in the repo pass. `detect skill/scripts/picker/` reports the first three as
|
|
design-system-color and is expected to; --pv-neutral lands 4 channels from
|
|
the documented neutral-94, inside the rule's 6-channel tolerance, so it
|
|
never registers. CLAUDE.md's Picker anti-pattern gate holds the count. */
|
|
--pv-primary: #195856;
|
|
--pv-secondary: #5f7c7a;
|
|
--pv-tertiary: #3a958b;
|
|
--pv-neutral: #edefef;
|
|
--pv-n-ink: var(--pk-ink-dark);
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: var(--pk-pv-natural);
|
|
transform: scale(var(--pk-pv-scale));
|
|
transform-origin: top left;
|
|
}
|
|
|
|
/* ── Operate variant of the preview ──────────────────────────
|
|
Same shell, same nav, same phone chrome as the landing tile. The body is an
|
|
app screen instead: a section rail, a work area holding a chart above a
|
|
selectable list, and a settings panel of fields and switches. Sizes come from
|
|
measuring the reference illustration and mapping its card box onto this one,
|
|
the same way the docs variant below was built. */
|
|
.picker-preview--ops {
|
|
--po-ink: var(--pv-bone);
|
|
--po-ink-soft: color-mix(in oklab, var(--pv-bone) 72%, var(--pv-neutral));
|
|
--po-wash: color-mix(in oklab, var(--pv-tertiary) 12%, var(--pv-neutral));
|
|
--po-rule: color-mix(in oklab, var(--pv-neutral) 86%, var(--pv-n-ink));
|
|
}
|
|
|
|
.picker-preview--ops .pv-nav {
|
|
border-bottom: 1px solid var(--po-rule);
|
|
}
|
|
|
|
/* Both bodies are size containers so the screen inside them scales with the
|
|
card rather than spilling out of it. Every `cqh` below is a canonical pixel
|
|
divided by the body height it sits in: 181.78 on the desktop, 133.75 inside
|
|
the phone's padded box. Chrome the exact tiles share (nav, phone top bar, tab
|
|
bar) stays in pixels so all the frames stay identical. */
|
|
.po-body {
|
|
container-type: size;
|
|
min-height: 0;
|
|
display: grid;
|
|
/* Percentages, not `cqh`: a container cannot measure itself, and the three
|
|
panels are horizontal dimensions anyway. */
|
|
grid-template-columns: 18.68% 51.84% minmax(0, 1fr);
|
|
}
|
|
|
|
.po-side {
|
|
display: grid;
|
|
align-content: start;
|
|
gap: 5.25cqh;
|
|
padding: 6.38cqh 2.86cqh 0 2.22cqh;
|
|
border-right: 1px solid var(--po-rule);
|
|
}
|
|
|
|
.po-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 3.39cqh;
|
|
padding-left: 2.84cqh;
|
|
}
|
|
|
|
.po-dot {
|
|
flex: none;
|
|
width: 4.07cqh;
|
|
aspect-ratio: 1;
|
|
background: var(--po-ink);
|
|
border-radius: 50%;
|
|
}
|
|
|
|
.po-line {
|
|
width: 13.23cqh;
|
|
height: 1.53cqh;
|
|
background: var(--po-ink);
|
|
border-radius: 2px;
|
|
}
|
|
|
|
/* The open section is a filled plate, not a tinted row: it is the mark that
|
|
tells a reader this rail belongs to an app and not to a document. */
|
|
.po-item--on {
|
|
padding-block: 2.97cqh;
|
|
background: var(--pv-primary);
|
|
border-radius: 2px;
|
|
}
|
|
|
|
.po-item--on .po-dot,
|
|
.po-item--on .po-line {
|
|
background: var(--pv-neutral);
|
|
}
|
|
|
|
.po-main {
|
|
min-width: 0;
|
|
display: grid;
|
|
align-content: start;
|
|
padding: 8.14cqh 4.68cqh 0 5.23cqh;
|
|
border-right: 1px solid var(--po-rule);
|
|
}
|
|
|
|
/* The bars stand on the content box and the axis rules it, so nothing can come
|
|
off the baseline at any window width. The padding below the axis is the zone
|
|
the category ticks hang in: they are positioned against the chart rather than
|
|
against a bar so no element ends up with a scrolling overflow. */
|
|
.po-chart {
|
|
position: relative;
|
|
display: flex;
|
|
align-items: flex-end;
|
|
gap: 7.23cqh;
|
|
height: 33.26cqh;
|
|
margin-inline: 9.52cqh 14.63cqh;
|
|
padding: 0 4.29cqh 2.56cqh 4.46cqh;
|
|
}
|
|
|
|
.po-chart::before {
|
|
content: "";
|
|
position: absolute;
|
|
right: 0;
|
|
bottom: 2.56cqh;
|
|
left: 0;
|
|
height: 1px;
|
|
background: var(--po-ink-soft);
|
|
}
|
|
|
|
.po-chart i {
|
|
flex: none;
|
|
width: 6.92cqh;
|
|
height: var(--h);
|
|
background: var(--po-wash);
|
|
}
|
|
|
|
.po-chart .po-peak {
|
|
background: var(--pv-primary);
|
|
}
|
|
|
|
/* No `left`, so the tick takes the static position it would have had inside its
|
|
bar and the margin walks it to that bar's centre. */
|
|
.po-chart i::after {
|
|
content: "";
|
|
position: absolute;
|
|
bottom: 0;
|
|
width: 1px;
|
|
height: 1.29cqh;
|
|
margin-left: 3.19cqh;
|
|
background: var(--po-ink-soft);
|
|
}
|
|
|
|
.po-divider {
|
|
height: 1px;
|
|
margin-top: 6.46cqh;
|
|
background: var(--po-rule);
|
|
}
|
|
|
|
.po-rows {
|
|
display: grid;
|
|
gap: 0.5cqh;
|
|
margin-top: 0.85cqh;
|
|
}
|
|
|
|
.po-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4.9cqh;
|
|
height: 11.61cqh;
|
|
padding-left: 3.41cqh;
|
|
}
|
|
|
|
.po-row .po-dot {
|
|
width: 3.85cqh;
|
|
}
|
|
|
|
.po-cell {
|
|
flex: none;
|
|
width: var(--w);
|
|
height: 1.45cqh;
|
|
background: var(--po-ink-soft);
|
|
border-radius: 2px;
|
|
}
|
|
|
|
/* The reference's two column gaps differ by less than half a canonical pixel,
|
|
so one step carries both. */
|
|
.po-cell + .po-cell {
|
|
margin-left: 12.71cqh;
|
|
}
|
|
|
|
.po-row--on {
|
|
background: var(--po-wash);
|
|
}
|
|
|
|
.po-row--on .po-dot {
|
|
background: var(--pv-primary);
|
|
}
|
|
|
|
.po-panel {
|
|
min-width: 0;
|
|
display: grid;
|
|
align-content: start;
|
|
padding: 6.82cqh 6.71cqh 0 7.21cqh;
|
|
}
|
|
|
|
.po-heading {
|
|
width: 17.38cqh;
|
|
height: 1.62cqh;
|
|
background: var(--pv-primary);
|
|
border-radius: 2px;
|
|
}
|
|
|
|
.po-field {
|
|
display: flex;
|
|
align-items: center;
|
|
height: 8.75cqh;
|
|
margin-top: 5.81cqh;
|
|
border: 1px solid var(--pv-primary);
|
|
border-radius: 2px;
|
|
}
|
|
|
|
/* A square stood on its corner, two borders lit. Half of it falls outside the
|
|
drawn chevron, so the margins are placing the tip rather than the box. */
|
|
.po-chev {
|
|
align-self: start;
|
|
width: 2cqh;
|
|
aspect-ratio: 1;
|
|
margin: 1.86cqh 3.14cqh 0 auto;
|
|
border-right: 1px solid var(--po-ink);
|
|
border-bottom: 1px solid var(--po-ink);
|
|
transform: rotate(45deg);
|
|
}
|
|
|
|
.po-switch {
|
|
display: flex;
|
|
align-items: center;
|
|
height: 5.34cqh;
|
|
margin-top: 8.39cqh;
|
|
}
|
|
|
|
.po-switch + .po-switch {
|
|
margin-top: 6.19cqh;
|
|
}
|
|
|
|
.po-label {
|
|
flex: none;
|
|
width: 17.22cqh;
|
|
height: 1.62cqh;
|
|
background: var(--po-ink);
|
|
border-radius: 2px;
|
|
}
|
|
|
|
.po-toggle {
|
|
position: relative;
|
|
flex: none;
|
|
width: 10.29cqh;
|
|
height: 5.34cqh;
|
|
margin-left: auto;
|
|
background: var(--po-ink);
|
|
border-radius: 999px;
|
|
}
|
|
|
|
.po-toggle--on {
|
|
background: var(--pv-primary);
|
|
}
|
|
|
|
.po-toggle::after {
|
|
content: "";
|
|
position: absolute;
|
|
top: 0.66cqh;
|
|
left: 0.66cqh;
|
|
width: 4.02cqh;
|
|
aspect-ratio: 1;
|
|
background: var(--pv-neutral);
|
|
border-radius: 50%;
|
|
}
|
|
|
|
.po-toggle--on::after {
|
|
right: 0.66cqh;
|
|
left: auto;
|
|
}
|
|
|
|
/* The phone is the same screen narrowed: one chart, the same field, two setting
|
|
rows. Its padded box is 133.75 tall, so every shared `cqh` above is restated
|
|
here against that number. The padding itself is a percentage of the phone's
|
|
width, since a container cannot measure itself. */
|
|
.po-phone-body {
|
|
container-type: size;
|
|
min-height: 0;
|
|
display: grid;
|
|
align-content: start;
|
|
padding: 16.19% 4.57% 14.63% 4.27%;
|
|
}
|
|
|
|
.po-phone-body .po-chart {
|
|
gap: 6.79cqh;
|
|
height: 36.93cqh;
|
|
margin-inline: 0.67cqh 2.92cqh;
|
|
padding: 0 3.51cqh 3.36cqh 3.36cqh;
|
|
}
|
|
|
|
.po-phone-body .po-chart::before {
|
|
bottom: 3.36cqh;
|
|
}
|
|
|
|
.po-phone-body .po-chart i {
|
|
width: 6.21cqh;
|
|
}
|
|
|
|
.po-phone-body .po-chart i::after {
|
|
height: 1.94cqh;
|
|
margin-left: 2.73cqh;
|
|
}
|
|
|
|
.po-phone-body .po-field {
|
|
height: 11.81cqh;
|
|
margin-top: 10.47cqh;
|
|
}
|
|
|
|
.po-phone-body .po-chev {
|
|
width: 2.77cqh;
|
|
margin: 2.65cqh 5.03cqh 0 auto;
|
|
}
|
|
|
|
.po-prow {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4.49cqh;
|
|
height: 18.47cqh;
|
|
padding-left: 2.24cqh;
|
|
}
|
|
|
|
.po-field + .po-prow {
|
|
margin-top: 3.85cqh;
|
|
}
|
|
|
|
.po-prow + .po-prow {
|
|
border-top: 1px solid var(--po-rule);
|
|
}
|
|
|
|
.po-phone-body .po-dot {
|
|
width: 5.76cqh;
|
|
}
|
|
|
|
.po-phone-body .po-label {
|
|
width: 18.62cqh;
|
|
height: 2.17cqh;
|
|
}
|
|
|
|
.po-phone-body .po-toggle {
|
|
width: 12.64cqh;
|
|
height: 7.25cqh;
|
|
}
|
|
|
|
.po-phone-body .po-toggle::after {
|
|
top: 0.9cqh;
|
|
left: 0.9cqh;
|
|
width: 5.46cqh;
|
|
}
|
|
|
|
.po-phone-body .po-toggle--on::after {
|
|
right: 0.9cqh;
|
|
left: auto;
|
|
}
|
|
|
|
/* The row that opens a subpage points instead of switching: same square, the
|
|
other two borders lit. */
|
|
.po-phone-body .po-chev--next {
|
|
align-self: center;
|
|
width: 2.99cqh;
|
|
margin: 0 4.86cqh 0 auto;
|
|
border-top: 1px solid var(--po-ink);
|
|
border-bottom: 0;
|
|
}
|
|
|
|
/* ── Docs variant of the preview ─────────────────────────────
|
|
Same shell as the landing preview, same nav, same phone chrome, so all four
|
|
tiles read as one drawing system. The body is a documentation page instead:
|
|
a section rail beside a measured column of prose, and a called-out note at
|
|
the end of it. Sizes are canonical pixels, matching how the rest of the
|
|
component is authored, and the proportions come from measuring the reference
|
|
illustration and mapping its card box onto this one. */
|
|
.picker-preview--docs {
|
|
--pd-ink: var(--pv-bone);
|
|
--pd-strong: color-mix(in oklab, var(--pv-primary) 80%, var(--pv-strong));
|
|
--pd-rule: color-mix(in oklab, var(--pv-neutral) 86%, var(--pv-n-ink));
|
|
}
|
|
|
|
.picker-preview--docs .pv-nav {
|
|
border-bottom: 1px solid var(--pd-rule);
|
|
}
|
|
|
|
/* Both bodies are size containers so the page inside them scales with the card
|
|
rather than spilling out of it. The card is a fixed ratio, so measuring
|
|
against the body's own height keeps the drawing self-similar at every window
|
|
width. Every `cqh` below is a canonical pixel divided by the body height it
|
|
sits in: 181.78 on the desktop, 146.5 inside the phone. Chrome the two exact
|
|
tiles share (nav, phone top bar, tab bar) stays in pixels so both tiles keep
|
|
identical frames. */
|
|
.pd-body {
|
|
container-type: size;
|
|
min-height: 0;
|
|
display: grid;
|
|
/* A percentage, not `cqh`: a container cannot measure itself, and the rail is
|
|
a horizontal dimension anyway. */
|
|
grid-template-columns: 23.75% minmax(0, 1fr);
|
|
}
|
|
|
|
.pd-side {
|
|
display: grid;
|
|
align-content: start;
|
|
gap: 8.69cqh;
|
|
padding: 9.24cqh 0 0 7.15cqh;
|
|
border-right: 1px solid var(--pd-rule);
|
|
}
|
|
|
|
.pd-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 3.03cqh;
|
|
}
|
|
|
|
.pd-item i {
|
|
width: 2.48cqh;
|
|
aspect-ratio: 1;
|
|
background: var(--pd-ink);
|
|
border-radius: 50%;
|
|
}
|
|
|
|
.pd-item span {
|
|
width: 19.36cqh;
|
|
height: 1.27cqh;
|
|
background: var(--pd-ink);
|
|
border-radius: 2px;
|
|
}
|
|
|
|
/* The open section reads in the palette rather than in ink, so the rail shows
|
|
where the reader is without needing a highlight plate. */
|
|
.pd-item--on i {
|
|
background: var(--pv-primary);
|
|
}
|
|
|
|
.pd-item--on span {
|
|
background: var(--pv-tertiary);
|
|
}
|
|
|
|
/* The right padding is the measure: prose stops well short of the card edge,
|
|
which is what makes the column read as a document rather than as a page. */
|
|
.pd-main {
|
|
min-width: 0;
|
|
display: grid;
|
|
align-content: start;
|
|
padding: 9.74cqh 50.06cqh 0 16.94cqh;
|
|
}
|
|
|
|
.pd-heading {
|
|
width: var(--w, 100%);
|
|
height: 2.53cqh;
|
|
background: var(--pd-strong);
|
|
border-radius: 2px;
|
|
}
|
|
|
|
.pd-para {
|
|
display: grid;
|
|
gap: 3.74cqh;
|
|
}
|
|
|
|
.pd-para--lede {
|
|
margin-top: 6.38cqh;
|
|
}
|
|
|
|
/* Two passage gaps, not one: the reference opens a slightly narrower gap after
|
|
the lede than it does between the passages that follow. */
|
|
.pd-para + .pd-para {
|
|
margin-top: 9.9cqh;
|
|
}
|
|
|
|
.pd-para--lede + .pd-para {
|
|
margin-top: 9.46cqh;
|
|
}
|
|
|
|
.pd-para i {
|
|
width: var(--w, 100%);
|
|
height: 1.1cqh;
|
|
background: var(--pd-ink);
|
|
border-radius: 2px;
|
|
}
|
|
|
|
.pd-note {
|
|
display: flex;
|
|
gap: 3.41cqh;
|
|
height: 14.03cqh;
|
|
margin-top: 7.37cqh;
|
|
padding: 3.52cqh 4.4cqh 0 5.17cqh;
|
|
background: var(--pv-surface-2);
|
|
border-left: 1.27cqh solid var(--pv-primary);
|
|
border-radius: 0 2px 2px 0;
|
|
}
|
|
|
|
.pd-note-dot {
|
|
flex: none;
|
|
align-self: start;
|
|
width: 2.64cqh;
|
|
aspect-ratio: 1;
|
|
background: var(--pv-tertiary);
|
|
border-radius: 50%;
|
|
}
|
|
|
|
.pd-note-lines {
|
|
min-width: 0;
|
|
flex: 1;
|
|
display: grid;
|
|
gap: 3.69cqh;
|
|
align-content: start;
|
|
}
|
|
|
|
.pd-note-lines i {
|
|
width: var(--w, 100%);
|
|
height: 0.83cqh;
|
|
background: var(--pv-tertiary);
|
|
border-radius: 2px;
|
|
}
|
|
|
|
/* The phone is the same document narrowed: breadcrumb, heading, the same two
|
|
passages, the same note. */
|
|
/* The phone body's own padding is a percentage of the phone's width, since a
|
|
container cannot measure itself. Everything inside it is `cqh` against the
|
|
padded box. */
|
|
.pd-phone-body {
|
|
container-type: size;
|
|
min-height: 0;
|
|
display: grid;
|
|
align-content: start;
|
|
padding: 8.84% 11.42% 9.33% 10.23%;
|
|
}
|
|
|
|
.pd-crumb {
|
|
width: 13.11cqh;
|
|
height: 1.77cqh;
|
|
margin-inline: auto;
|
|
background: var(--pd-ink);
|
|
border-radius: 2px;
|
|
}
|
|
|
|
.pd-phone-body .pd-heading {
|
|
height: 3.14cqh;
|
|
margin-top: 9.62cqh;
|
|
}
|
|
|
|
.pd-phone-body .pd-para {
|
|
gap: 4.1cqh;
|
|
}
|
|
|
|
.pd-phone-body .pd-para i {
|
|
height: 1.37cqh;
|
|
}
|
|
|
|
.pd-phone-body .pd-para--lede {
|
|
margin-top: 6.55cqh;
|
|
gap: 3.62cqh;
|
|
}
|
|
|
|
.pd-phone-body .pd-para + .pd-para {
|
|
margin-top: 10.03cqh;
|
|
}
|
|
|
|
.pd-phone-body .pd-para--lede + .pd-para {
|
|
margin-top: 11.4cqh;
|
|
}
|
|
|
|
.pd-phone-body .pd-note {
|
|
height: 14.27cqh;
|
|
gap: 3.34cqh;
|
|
margin-top: 6.76cqh;
|
|
padding: 4.37cqh 1.37cqh 0 3.14cqh;
|
|
border-left-width: 1.23cqh;
|
|
}
|
|
|
|
.pd-phone-body .pd-note-dot {
|
|
width: 3.34cqh;
|
|
}
|
|
|
|
.pd-phone-body .pd-note-lines {
|
|
gap: 2.8cqh;
|
|
}
|
|
|
|
.pd-phone-body .pd-note-lines i {
|
|
height: 1.37cqh;
|
|
}
|
|
|
|
/* ── Gallery variant of the preview ──────────────────────────
|
|
Same shell, same nav, same phone chrome as the other tiles. The body is a
|
|
portfolio index instead: two shots set alternately against their captions,
|
|
and a carousel rail under them saying the set continues. Sizes come from
|
|
measuring the reference illustration and mapping its card box onto this one,
|
|
the same way the docs variant was built.
|
|
|
|
Both bodies are size containers so the page inside scales with the card
|
|
rather than spilling out of it. Every `cqh` below is a canonical pixel
|
|
divided by the body height it sits in: 182.78 on the desktop, 148.53 inside
|
|
the phone's padded box. Chrome the tiles share (nav, phone top bar, tab bar)
|
|
stays in pixels so every frame stays identical. */
|
|
.picker-preview--gallery {
|
|
--pg-ink: var(--pv-bone);
|
|
}
|
|
|
|
/* The work is the page, so the plates carry the drawn size and the shared
|
|
4 / 3 gives way to it. */
|
|
.picker-preview--gallery .pv-image {
|
|
aspect-ratio: auto;
|
|
}
|
|
|
|
/* The illustration marks each shot in the brand color rather than the accent,
|
|
which is what ties the two rows back to the nav. */
|
|
.picker-preview--gallery .pv-image::after {
|
|
top: 3.41cqh;
|
|
right: 3.15cqh;
|
|
width: 3.37cqh;
|
|
background: var(--pv-primary);
|
|
}
|
|
|
|
.pg-body {
|
|
container-type: size;
|
|
min-height: 0;
|
|
display: grid;
|
|
align-content: start;
|
|
}
|
|
|
|
/* The insets differ per row because the illustration staggers the pair rather
|
|
than mirroring it, which is what keeps the index from reading as a grid. */
|
|
.pg-row {
|
|
display: grid;
|
|
grid-template-columns: 73.39cqh minmax(0, 1fr);
|
|
column-gap: 10.55cqh;
|
|
height: 37.31cqh;
|
|
margin-top: 2.31cqh;
|
|
padding-inline: 31.34cqh 32.13cqh;
|
|
}
|
|
|
|
.pg-row--flip {
|
|
grid-template-columns: minmax(0, 1fr) 73.39cqh;
|
|
column-gap: 11.18cqh;
|
|
margin-top: 7.28cqh;
|
|
padding-inline: 34.97cqh 33.07cqh;
|
|
}
|
|
|
|
.pg-row .pv-image {
|
|
height: 100%;
|
|
}
|
|
|
|
.pg-cap {
|
|
display: grid;
|
|
align-content: start;
|
|
gap: 3.1cqh;
|
|
padding-top: 12.39cqh;
|
|
}
|
|
|
|
.pg-cap i {
|
|
width: var(--w, 100%);
|
|
height: 1.5cqh;
|
|
background: var(--pg-ink);
|
|
border-radius: 2px;
|
|
}
|
|
|
|
.pg-cap i.pg-cap-title {
|
|
height: 2.01cqh;
|
|
margin-bottom: 0.93cqh;
|
|
}
|
|
|
|
/* The insets are measured to the chevrons' drawn tips, which sit outside their
|
|
layout boxes once the squares below are turned. */
|
|
.pg-rail {
|
|
display: flex;
|
|
align-items: center;
|
|
height: 3.25cqh;
|
|
margin-top: 4.95cqh;
|
|
padding-inline: 7.78cqh 8.24cqh;
|
|
}
|
|
|
|
/* A square stood on its corner: two borders draw the chevron without pulling
|
|
in an inline SVG nothing else in the component uses. */
|
|
.pg-arrow {
|
|
flex: none;
|
|
width: 2.3cqh;
|
|
aspect-ratio: 1;
|
|
border-left: 1px solid var(--pg-ink);
|
|
border-bottom: 1px solid var(--pg-ink);
|
|
transform: rotate(45deg);
|
|
}
|
|
|
|
.pg-arrow--next {
|
|
border-right: 1px solid var(--pg-ink);
|
|
border-top: 1px solid var(--pg-ink);
|
|
border-bottom: 0;
|
|
border-left: 0;
|
|
}
|
|
|
|
.pg-track {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 3.7cqh;
|
|
margin-left: 7.26cqh;
|
|
margin-right: auto;
|
|
}
|
|
|
|
.pg-track i {
|
|
flex: none;
|
|
width: var(--w);
|
|
height: 1.48cqh;
|
|
background: var(--pg-ink);
|
|
border-radius: 2px;
|
|
}
|
|
|
|
.pg-track i.pg-track-on {
|
|
background: var(--pv-primary);
|
|
}
|
|
|
|
/* The phone is the same index at one column: shot, caption, shot, caption. */
|
|
/* Its own padding is a percentage of the phone's width, since a container
|
|
cannot measure itself. Everything inside is `cqh` against the padded box. */
|
|
.pg-phone-body {
|
|
container-type: size;
|
|
min-height: 0;
|
|
display: grid;
|
|
align-content: start;
|
|
padding: 5.65% 8.34% 10.49%;
|
|
}
|
|
|
|
.pg-phone-body .pv-image {
|
|
height: 33.27cqh;
|
|
}
|
|
|
|
.pg-phone-body > .pg-cap + .pv-image {
|
|
height: 31.95cqh;
|
|
margin-top: 5.59cqh;
|
|
}
|
|
|
|
/* The phone container is shorter than the desktop one, so every shared `cqh`
|
|
has to be restated against it. */
|
|
.pg-phone-body .pv-image::after {
|
|
top: 3.02cqh;
|
|
right: 3.26cqh;
|
|
width: 3.4cqh;
|
|
}
|
|
|
|
.pg-phone-body .pg-cap {
|
|
gap: 2.93cqh;
|
|
margin-top: 4.15cqh;
|
|
padding-top: 0;
|
|
}
|
|
|
|
.pg-phone-body > .pg-cap ~ .pg-cap {
|
|
margin-top: 3.68cqh;
|
|
}
|
|
|
|
.pg-phone-body .pg-cap i {
|
|
height: 1.52cqh;
|
|
}
|
|
|
|
/* At this size the caption's first line is a shorter line, not a heavier one. */
|
|
.pg-phone-body .pg-cap i.pg-cap-title {
|
|
height: 1.52cqh;
|
|
margin-bottom: 0.28cqh;
|
|
}
|
|
|
|
/* Three page dots rather than four tabs, closer together than the tab bar's
|
|
even spread. */
|
|
.picker-preview--gallery .pv-tabbar {
|
|
justify-content: center;
|
|
gap: 11px;
|
|
}
|
|
|
|
.picker-mode-copy {
|
|
display: grid;
|
|
gap: 10px;
|
|
align-content: start;
|
|
}
|
|
|
|
.picker-mode-goal {
|
|
color: var(--ks-text-muted);
|
|
font-size: 16px;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
/* The examples name the tile now that the mode word is gone, so they lead the
|
|
copy block and carry its strongest weight. Sized to keep a four-chip row
|
|
within two lines at the narrowest tile the grid produces. */
|
|
.picker-mode-pills {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 6px;
|
|
}
|
|
|
|
.picker-mode-pills i {
|
|
padding: 4px 9px;
|
|
color: var(--ks-champagne);
|
|
font-size: 12.5px;
|
|
font-style: normal;
|
|
letter-spacing: 0.015em;
|
|
border: 1px solid color-mix(in oklab, var(--ks-champagne) 24%, var(--ks-rule));
|
|
border-radius: 2px;
|
|
transition:
|
|
color 180ms var(--ks-ease),
|
|
border-color 180ms var(--ks-ease);
|
|
}
|
|
|
|
.picker-mode-tile:hover .picker-mode-pills i {
|
|
border-color: color-mix(in oklab, var(--ks-patina) 30%, var(--ks-rule));
|
|
}
|
|
|
|
.picker-mode-tile:has(input:checked) .picker-mode-pills i {
|
|
color: color-mix(in oklab, var(--ks-patina) 85%, var(--ks-champagne));
|
|
border-color: color-mix(in oklab, var(--ks-patina) 55%, var(--ks-rule));
|
|
}
|
|
|
|
/* The wording here is doing careful work: every preview in the interview is
|
|
an isolating instrument, not a sketch of the deliverable, and this is the
|
|
one place the visitor is told so before meeting the rest of them. */
|
|
.picker-modes-note {
|
|
max-width: 52ch;
|
|
color: var(--ks-text-muted);
|
|
font-size: 1.08rem;
|
|
line-height: 1.5;
|
|
text-align: center;
|
|
text-wrap: balance;
|
|
}
|
|
|
|
.picker-modes-note strong,
|
|
.picker-mode-goal strong {
|
|
color: var(--ks-text);
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* Screen 03 mirrors screen 02's rhythm: the same centered headline class,
|
|
the same vertical gaps, options left and preview right. */
|
|
.picker-strategy {
|
|
display: grid;
|
|
gap: 48px;
|
|
padding-block: clamp(32px, 5svh, 48px) clamp(24px, 3.5svh, 48px);
|
|
}
|
|
|
|
.picker-strategy-grid {
|
|
/* Named so the option panel can cap itself against the same budget: its
|
|
height no longer comes from the column, so nothing else would stop it. */
|
|
--pk-column: calc(100svh - 346px);
|
|
max-height: var(--pk-column);
|
|
display: grid;
|
|
grid-template-columns: minmax(340px, 420px) minmax(0, 1fr);
|
|
gap: 50px;
|
|
align-items: stretch;
|
|
}
|
|
|
|
/* One raised panel, hairline rows — same vocabulary as the palette bands
|
|
on screen 02, not four floating SaaS cards. Screen 04 reuses the base
|
|
shell; screen 03 opts into the choices treatment below. */
|
|
.picker-strategy-options {
|
|
min-width: 0;
|
|
display: grid;
|
|
gap: 0;
|
|
padding: 0;
|
|
background: var(--ks-lacquer-raised);
|
|
border: 1px solid var(--ks-rule);
|
|
border-radius: 2px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.picker-strategy-options > legend,
|
|
.picker-strategy-option input {
|
|
position: absolute;
|
|
width: 1px;
|
|
height: 1px;
|
|
overflow: hidden;
|
|
clip: rect(0 0 0 0);
|
|
clip-path: inset(50%);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.picker-strategy-option {
|
|
position: relative;
|
|
display: grid;
|
|
gap: 6px;
|
|
align-items: start;
|
|
padding: 20px 24px 20px 28px;
|
|
background: transparent;
|
|
border: 0;
|
|
border-bottom: 1px solid var(--ks-rule);
|
|
border-radius: 0;
|
|
cursor: pointer;
|
|
transition:
|
|
background-color 180ms var(--ks-ease),
|
|
color 180ms var(--ks-ease);
|
|
}
|
|
|
|
.picker-strategy-option:last-child {
|
|
border-bottom: 0;
|
|
}
|
|
|
|
/* ============================================================
|
|
The standard option row. Every list of choices in the picker wears this
|
|
and nothing else: a patina spine that previews under the cursor and fills
|
|
in once the answer is committed, a wash of the same patina behind the row
|
|
at two strengths, and the row's own heading taking the patina in both
|
|
states. It came from screen 04's type rail; screens 03 and 05 read these
|
|
same rules.
|
|
|
|
A new question brings its own copy scale and its own row content. It does
|
|
not bring its own state colors, its own spine, or its own hover. If a
|
|
screen appears to need a different treatment, change it here for all of
|
|
them rather than adding a fourth variant.
|
|
============================================================ */
|
|
.picker-strategy-option::before {
|
|
content: "";
|
|
position: absolute;
|
|
inset: 0 auto 0 0;
|
|
width: 3px;
|
|
background: transparent;
|
|
transition: background-color 180ms var(--ks-ease);
|
|
}
|
|
|
|
/* Mixes are keyed to --ks-patina, not --ks-patina-deep: the deep oxide sits at
|
|
49% lightness and a single-digit mix of it into an 11% panel is invisible. */
|
|
.picker-strategy-option:hover {
|
|
background: color-mix(in oklab, var(--ks-patina) 9%, var(--ks-lacquer-raised));
|
|
}
|
|
|
|
.picker-strategy-option:hover::before {
|
|
background: color-mix(in oklab, var(--ks-patina) 55%, transparent);
|
|
}
|
|
|
|
.picker-strategy-option:has(input:checked) {
|
|
background: color-mix(in oklab, var(--ks-patina) 14%, var(--ks-lacquer-raised));
|
|
}
|
|
|
|
.picker-strategy-option:has(input:checked)::before {
|
|
background: var(--ks-patina);
|
|
}
|
|
|
|
/* Whatever names the row is what takes the accent: a strategy label, a scale
|
|
name, or the face a font pair is set in. */
|
|
.picker-strategy-option:hover :is(.picker-strategy-title, .picker-type-sample-heading),
|
|
.picker-strategy-option:has(input:checked) :is(.picker-strategy-title, .picker-type-sample-heading) {
|
|
color: var(--ks-patina);
|
|
}
|
|
|
|
.picker-strategy-option:has(input:focus-visible) {
|
|
outline: 2px solid var(--ks-patina-deep);
|
|
outline-offset: -2px;
|
|
z-index: 1;
|
|
}
|
|
|
|
.picker-strategy-copy {
|
|
display: grid;
|
|
gap: 6px;
|
|
}
|
|
|
|
.picker-strategy-title {
|
|
color: var(--ks-champagne);
|
|
font-family: var(--ks-font-display);
|
|
font-size: 1.22rem;
|
|
font-weight: 400;
|
|
line-height: 1.15;
|
|
letter-spacing: -0.01em;
|
|
transition: color 180ms var(--ks-ease);
|
|
}
|
|
|
|
.picker-strategy-desc {
|
|
color: var(--ks-text-muted);
|
|
font-size: 0.88rem;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
/* Screen 03 uses the same rail shell as screen 04: scrollable fieldset inside
|
|
a height-capped column. Row rhythm, hover, and checked states come from the
|
|
standard option row above; only the copy scale differs, because these are
|
|
strategy labels and not font specimens. Four rows share the preview column's
|
|
height equally so a one-line description does not leave a short band beside
|
|
a taller neighbor. */
|
|
.picker-strategy-grid > .picker-type-rail {
|
|
min-height: 0;
|
|
height: 100%;
|
|
grid-template-rows: minmax(0, 1fr);
|
|
}
|
|
|
|
/* Screen 03 carries the palette under its choices, so its rail is two rows and
|
|
the options are measured against what is left of the column rather than
|
|
against all of it. Without the second measure the panel keeps its own 434 and
|
|
overflows the row the band now shares. Scoped to this screen because the cap
|
|
only holds where there are four options to divide. */
|
|
/* What this screen has to spend between the heading and the fold. Both columns
|
|
read it: the rail sizes its rows against it and the frame takes what the rail
|
|
leaves, so whichever of the two is taller is still the one that decides where
|
|
the CTA lands. The shared --pk-column is measured for a row an artboard sets
|
|
and runs about 30px short of what this screen spends around its grid; at a
|
|
900px viewport that difference is the whole overhang. Taken at 1600x900, the
|
|
tightest of the common sizes. */
|
|
.picker-screen[data-screen="03"] .picker-strategy-grid {
|
|
--pk-fit: calc(100svh - 376px);
|
|
}
|
|
|
|
.picker-screen[data-screen="03"] .picker-strategy-grid > .picker-type-rail {
|
|
grid-template-rows: minmax(0, 1fr) auto;
|
|
/* The rail is measured against the viewport rather than against the frame
|
|
beside it. The frame draws the chosen surface at that drawing's own ratio,
|
|
which is wider than the artboard this column used to hold, so matching its
|
|
height would spend the whole column on four rows of 78px.
|
|
|
|
546px is the comfortable rail: four rows at 108, the band at 96, the gap,
|
|
and the panel's hairline. Below that the band is the first thing to give,
|
|
down to a floor where the palette is still readable, because a shorter
|
|
reading of four colors costs less than a squeezed sentence. */
|
|
--pk-band: clamp(72px, calc(var(--pk-fit) - 450px), 96px);
|
|
}
|
|
|
|
/* And the rows come in under what the artboard beside them asks for, so the row
|
|
they share keeps the height the artboard sets and the CTA underneath stays
|
|
where it is on every other screen.
|
|
|
|
Floored at 88px, which is the tallest option's two lines plus the air they
|
|
need to read as a paragraph rather than a block. Below that the row has no
|
|
padding left to give and the sentence is what gets cut, so past the floor the
|
|
rail is allowed to be taller than the artboard instead: the same trade this
|
|
column made before it carried a band. */
|
|
.picker-screen[data-screen="03"] .picker-strategy-choices.picker-type-options {
|
|
/* The row formula below is the only cap this panel needs. The shared one
|
|
measures against the column budget alone, which on a short viewport asks
|
|
for less than the floor and clips the fourth option to reach it. */
|
|
max-height: none;
|
|
grid-auto-rows: max(88px, min(108px, calc((var(--pk-fit) - var(--pk-band) - 18px) / 4)));
|
|
}
|
|
|
|
/* Equal rows, sized by the tallest one's copy rather than by how many there
|
|
are. Stretching the tracks to the artboard's height instead makes a row on a
|
|
three-option screen half again as tall as the same row on a four-option one,
|
|
and the difference is spent on empty space under two lines of text. The
|
|
panel hugs the rows it has, so nothing draws an empty band underneath. */
|
|
.picker-strategy-choices.picker-type-options {
|
|
align-self: start;
|
|
max-height: var(--pk-column);
|
|
/* Four is the largest option count in the run, so the row is a quarter of the
|
|
column whether a screen has four of them or three, and one formula gives
|
|
every screen in the run the same band. Below 432px of column they shrink
|
|
together; the row padding absorbs it before the copy does. The 2px is the
|
|
panel's own hairline, which the column budget does not account for. */
|
|
grid-template-rows: none;
|
|
grid-auto-rows: min(108px, calc((var(--pk-column) - 2px) / 4));
|
|
}
|
|
|
|
/* Centred rather than top-aligned: whatever the row has left over after the
|
|
label and its sentence reads as air around them, not as a gap underneath. */
|
|
.picker-strategy-choices.picker-type-options .picker-strategy-option {
|
|
align-content: center;
|
|
}
|
|
|
|
.picker-strategy-choices .picker-strategy-copy {
|
|
gap: 2px;
|
|
}
|
|
|
|
.picker-strategy-choices .picker-strategy-title {
|
|
color: var(--ks-champagne);
|
|
font-family: var(--ks-font);
|
|
font-size: 24px;
|
|
font-weight: 400;
|
|
line-height: 1.15;
|
|
letter-spacing: -0.015em;
|
|
transition: color 180ms var(--ks-ease);
|
|
}
|
|
|
|
.picker-strategy-choices .picker-strategy-desc {
|
|
font-size: 15px;
|
|
line-height: 1.4;
|
|
max-width: 34ch;
|
|
}
|
|
|
|
/* 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 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);
|
|
--pv-neutral: var(--ks-lacquer-raised);
|
|
--pv-n-ink: var(--ks-champagne);
|
|
--pv-p-ink: var(--ks-champagne);
|
|
--pv-t-ink: var(--ks-dark-ink);
|
|
/* The accents again, moved in lightness until they can be read as words on
|
|
the ground named in the suffix. The script publishes them; these values
|
|
stand in for the run before a palette is committed, where the accents are
|
|
the picker's own and already legible. */
|
|
--pv-p-on-n: var(--pv-primary);
|
|
--pv-t-on-n: var(--pv-tertiary);
|
|
--pv-t-on-p: var(--pv-tertiary);
|
|
--pv-p-on-p: var(--pk-ink-dark);
|
|
--pv-t-on-t: var(--pk-ink-light);
|
|
--pv-p-on-i: var(--pk-ink-light);
|
|
--pv-surface-2: color-mix(in oklab, var(--pv-neutral) 92%, var(--pv-n-ink));
|
|
--pv-bone: color-mix(in oklab, var(--pv-neutral) 62%, var(--pv-n-ink));
|
|
--pv-muted: color-mix(in oklab, var(--pv-neutral) 84%, var(--pv-n-ink));
|
|
--pv-strong: color-mix(in oklab, var(--pv-neutral) 22%, var(--pv-n-ink));
|
|
--pvs-ground: var(--pv-neutral);
|
|
--pvs-ink: var(--pv-n-ink);
|
|
--pvs-surface: var(--pv-surface-2);
|
|
--pvs-headline: var(--pv-strong);
|
|
--pvs-eyebrow: var(--pv-bone);
|
|
--pvs-copy: var(--pv-muted);
|
|
--pvs-title: var(--pv-strong);
|
|
--pvs-cta: var(--pv-primary);
|
|
--pvs-cta-ink: var(--pv-p-ink);
|
|
--pvs-cta-text: var(--pv-p-on-p);
|
|
--pvs-signal: var(--pv-primary);
|
|
--pvs-signal-ink: var(--pv-p-ink);
|
|
/* Three slots the wireframe never reads: where screen 04 turns a bar into
|
|
glyphs, the slot that filled the bar is not the one that can set the
|
|
type. The mix percentages are the same idea for running copy, tightened
|
|
by whichever strategy leaves the least room between ground and ink. */
|
|
--pvs-headline-text: var(--pvs-headline);
|
|
--pvs-mark-text: var(--pv-p-on-n);
|
|
--pvs-accent-text: var(--pv-p-on-n);
|
|
--pvs-signal-text: var(--pv-p-on-p);
|
|
--pvs-copy-mix: 34%;
|
|
--pvs-bars-mix: 42%;
|
|
--pvs-ghost: color-mix(in oklab, var(--pv-neutral) 88%, var(--pv-secondary));
|
|
--pvs-ghost-edge: color-mix(in oklab, var(--pv-secondary) 55%, var(--pv-neutral));
|
|
--pvs-image: var(--pv-surface-2);
|
|
--pvs-image-a: color-mix(in oklab, var(--pv-secondary) 26%, var(--pv-neutral));
|
|
--pvs-image-b: color-mix(in oklab, var(--pv-secondary) 50%, var(--pv-neutral));
|
|
--pvs-image-edge: color-mix(in oklab, var(--pv-secondary) 58%, var(--pv-neutral));
|
|
--pvs-bars: var(--pv-muted);
|
|
--pvs-rule: var(--ks-rule);
|
|
--pvs-accent-a: var(--pv-bone);
|
|
--pvs-accent-b: var(--pv-bone);
|
|
--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;
|
|
grid-template-columns: minmax(0, 1fr) auto;
|
|
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;
|
|
}
|
|
|
|
/* ============================================================
|
|
The strategy stage. Screen 03 colors the surfaces chosen on screen 01b, so
|
|
the frame holds one drawing per chosen surface and shows one at a time. The
|
|
drawings are lifted whole from the tiles, which is why the frame takes their
|
|
ratio rather than imposing the artboard's: two of the four are drawn in
|
|
pixels and two scale off their own height, and stretching either to a taller
|
|
frame breaks the drawing rather than filling it.
|
|
|
|
The rail beside it is the taller column, so the frame is centred against it
|
|
and the tabs ride in the space above, where they cover none of the page they
|
|
are switching between.
|
|
============================================================ */
|
|
.picker-strategy-stage {
|
|
min-width: 0;
|
|
display: grid;
|
|
grid-template-rows: auto auto;
|
|
/* Level with the top of the rail beside it. Centring it against the taller
|
|
column opened a gap under the heading that read as a missing element. */
|
|
align-content: start;
|
|
gap: 12px;
|
|
}
|
|
|
|
/* One drawing is shown and the rest stay mounted, so switching costs an
|
|
attribute rather than a rebuild. [hidden] is a display: none that the
|
|
component's own display: grid outranks, so it is restated here. */
|
|
.picker-preview[hidden] {
|
|
display: none;
|
|
}
|
|
|
|
/* The group only appears with a second surface to switch to; a single surface
|
|
is already the only thing the frame can be showing. */
|
|
.picker-surface-tabs {
|
|
justify-self: end;
|
|
display: flex;
|
|
gap: 2px;
|
|
padding: 4px;
|
|
background: var(--ks-lacquer-raised);
|
|
border: 1px solid var(--ks-rule);
|
|
border-radius: 2px;
|
|
}
|
|
|
|
.picker-surface-tabs[hidden] {
|
|
display: none;
|
|
}
|
|
|
|
.picker-surface-tab {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 13px 22px;
|
|
color: var(--ks-text-muted);
|
|
background: transparent;
|
|
border: 0;
|
|
border-radius: 2px;
|
|
font-family: inherit;
|
|
font-size: 1.08rem;
|
|
line-height: 1.2;
|
|
cursor: pointer;
|
|
transition:
|
|
background-color 180ms var(--ks-ease),
|
|
color 180ms var(--ks-ease);
|
|
}
|
|
|
|
.picker-surface-tab:hover {
|
|
color: var(--ks-champagne);
|
|
background: color-mix(in oklab, var(--ks-patina) 9%, var(--ks-lacquer-raised));
|
|
}
|
|
|
|
.picker-surface-tab[aria-pressed="true"] {
|
|
color: var(--ks-champagne);
|
|
background: color-mix(in oklab, var(--ks-patina) 14%, var(--ks-lacquer-raised));
|
|
}
|
|
|
|
.picker-surface-tab:focus-visible {
|
|
outline: 2px solid var(--ks-patina-deep);
|
|
outline-offset: -2px;
|
|
}
|
|
|
|
/* The whole report on state: filled once the surface has been answered
|
|
deliberately, hollow while it is still carrying the default it was given. */
|
|
.picker-surface-dot {
|
|
width: 8px;
|
|
height: 8px;
|
|
border: 1px solid color-mix(in oklab, var(--ks-patina) 55%, transparent);
|
|
border-radius: 50%;
|
|
}
|
|
|
|
.picker-surface-tab[data-set="yes"] .picker-surface-dot {
|
|
background: var(--ks-patina);
|
|
border-color: var(--ks-patina);
|
|
}
|
|
|
|
/* A strategy the surface in the frame cannot carry. The row keeps its place so
|
|
the list does not reshuffle as you move between surfaces, and its sentence
|
|
is replaced by the reason it is out. */
|
|
.picker-strategy-option.is-blocked {
|
|
opacity: 0.42;
|
|
/* Deaf to the cursor as well as to the click, so a row the surface cannot
|
|
carry cannot preview itself either. It also keeps the hover and checked
|
|
branches below written the way the artboard's are: no blocked row can be
|
|
the one being hovered, so neither branch has to ask. */
|
|
pointer-events: none;
|
|
}
|
|
|
|
/* ============================================================
|
|
The strategy, applied to the surface in the frame.
|
|
|
|
The drawings read the palette through role names, and every shade they mix
|
|
is mixed from those same names, so a strategy is a remapping of the roles
|
|
rather than a second set of rules per drawing. Redistributing four roles
|
|
reaches all four surfaces at once, and a new surface arrives already
|
|
answering to it.
|
|
|
|
The committed colors are painted onto the stage under --pkc-*, which gives
|
|
each remap a fixed handle on what was chosen. Reading --pv-* instead would
|
|
read whatever the remap had just written, which is how drenched would end
|
|
up mixing its ground out of its own ink.
|
|
============================================================ */
|
|
.picker-strategy-stage > .picker-preview {
|
|
--pv-primary: var(--pkc-primary, var(--ks-champagne));
|
|
--pv-secondary: var(--pkc-secondary, var(--ks-patina));
|
|
--pv-tertiary: var(--pkc-tertiary, var(--ks-kinpaku));
|
|
--pv-neutral: var(--pkc-neutral, var(--ks-lacquer-raised));
|
|
--pv-n-ink: var(--pkc-n-ink, var(--ks-champagne));
|
|
--pv-p-ink: var(--pkc-p-ink, var(--ks-champagne));
|
|
}
|
|
|
|
/* Restrained: the page is its neutral and the primary is the only color that
|
|
arrives. The other two stand down into the ground rather than disappearing,
|
|
so what they were holding is still legible as structure. */
|
|
#picker-form:has(.picker-strategy-option:hover input[name="color-strategy"][value="restrained"]) .picker-strategy-stage > .picker-preview,
|
|
#picker-form:not(:has(.picker-strategy-option:hover input[name="color-strategy"])):has(input[name="color-strategy"][value="restrained"]:checked) .picker-strategy-stage > .picker-preview {
|
|
--pv-secondary: color-mix(in oklab, var(--pkc-neutral) 78%, var(--pkc-n-ink));
|
|
--pv-tertiary: color-mix(in oklab, var(--pkc-neutral) 86%, var(--pkc-n-ink));
|
|
--pv-t-ink: var(--pkc-n-ink);
|
|
--pv-t-on-t: var(--pkc-n-ink);
|
|
--pv-t-on-n: color-mix(in oklab, var(--pkc-n-ink) 62%, var(--pkc-neutral));
|
|
--pv-t-on-p: var(--pkc-p-ink);
|
|
}
|
|
|
|
/* Committed: one color doing all the work the other two were doing, and a
|
|
trace of it in the paper as well. Without the paper, a surface whose accents
|
|
are small (a dashboard, a doc) separates from restrained by a few tinted
|
|
pixels, which is not what "does real work across the page" describes. */
|
|
#picker-form:has(.picker-strategy-option:hover input[name="color-strategy"][value="committed"]) .picker-strategy-stage > .picker-preview,
|
|
#picker-form:not(:has(.picker-strategy-option:hover input[name="color-strategy"])):has(input[name="color-strategy"][value="committed"]:checked) .picker-strategy-stage > .picker-preview {
|
|
--pv-neutral: color-mix(in oklab, var(--pkc-neutral) 95%, var(--pkc-primary));
|
|
--pv-secondary: var(--pkc-primary);
|
|
--pv-tertiary: var(--pkc-primary);
|
|
--pv-t-ink: var(--pkc-p-ink);
|
|
--pv-t-on-t: var(--pkc-p-on-p);
|
|
--pv-t-on-n: var(--pkc-p-on-n);
|
|
--pv-t-on-p: var(--pkc-p-on-p);
|
|
}
|
|
|
|
/* Full palette is the drawing as it was authored: four roles, each already
|
|
holding a different part of the page. It needs no block of its own, which
|
|
is the same reason the artboard's restrained does not have one. */
|
|
|
|
/* Drenched: the ground becomes the primary, and everything that was picked out
|
|
in the primary is now picked out against it in ink. The shades the drawings
|
|
mix from the neutral follow the ground without being told, which is what
|
|
keeps the images and panels tinted rather than left as white holes. */
|
|
#picker-form:has(.picker-strategy-option:hover input[name="color-strategy"][value="drenched"]) .picker-strategy-stage > .picker-preview,
|
|
#picker-form:not(:has(.picker-strategy-option:hover input[name="color-strategy"])):has(input[name="color-strategy"][value="drenched"]:checked) .picker-strategy-stage > .picker-preview {
|
|
--pv-neutral: var(--pkc-primary);
|
|
--pv-n-ink: var(--pkc-p-ink);
|
|
--pv-primary: var(--pkc-p-ink);
|
|
--pv-p-ink: var(--pkc-primary);
|
|
--pv-p-on-n: var(--pkc-p-ink);
|
|
--pv-p-on-p: var(--pkc-primary);
|
|
--pv-secondary: var(--pkc-tertiary);
|
|
--pv-tertiary: var(--pkc-tertiary);
|
|
--pv-t-on-n: var(--pkc-t-on-p);
|
|
}
|
|
|
|
/* 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 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: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);
|
|
--pvs-ghost: color-mix(in oklab, var(--pv-primary) 14%, var(--pv-neutral));
|
|
--pvs-ghost-edge: var(--pv-primary);
|
|
--pvs-image: var(--pv-primary);
|
|
--pvs-image-a: var(--pv-primary);
|
|
--pvs-image-b: color-mix(in oklab, var(--pv-primary) 72%, var(--pv-neutral));
|
|
--pvs-image-edge: var(--pv-primary);
|
|
--pvs-accent-a: var(--pv-primary);
|
|
--pvs-accent-b: color-mix(in oklab, var(--pv-primary) 70%, var(--pv-neutral));
|
|
--pvs-accent-c: var(--pv-primary);
|
|
--pvs-accent-d: color-mix(in oklab, var(--pv-primary) 18%, var(--pv-neutral));
|
|
}
|
|
|
|
/* Full palette: every role on duty. Primary carries the headline, image,
|
|
and CTA; secondary the ghost action and gallery; tertiary the eyebrow,
|
|
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: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);
|
|
--pvs-eyebrow: var(--pv-tertiary);
|
|
--pvs-ghost: color-mix(in oklab, var(--pv-secondary) 18%, var(--pv-neutral));
|
|
--pvs-ghost-edge: var(--pv-secondary);
|
|
--pvs-image: var(--pv-primary);
|
|
--pvs-image-a: var(--pv-primary);
|
|
--pvs-image-b: color-mix(in oklab, var(--pv-secondary) 32%, var(--pv-primary));
|
|
--pvs-image-edge: var(--pv-primary);
|
|
--pvs-signal: var(--pv-tertiary);
|
|
--pvs-signal-ink: var(--pv-t-ink);
|
|
--pvs-signal-text: var(--pv-t-on-t);
|
|
--pvs-accent-a: var(--pv-secondary);
|
|
--pvs-accent-b: color-mix(in oklab, var(--pv-secondary) 42%, var(--pv-neutral));
|
|
--pvs-accent-c: var(--pv-primary);
|
|
--pvs-accent-d: var(--pv-tertiary);
|
|
}
|
|
|
|
/* Drenched: the primary is the page. Everything on top is either ink,
|
|
an ink wash of the primary, or the secondary and tertiary doing accent
|
|
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: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));
|
|
--pvs-headline: var(--pv-p-ink);
|
|
--pvs-eyebrow: color-mix(in oklab, var(--pv-p-ink) 70%, var(--pv-primary));
|
|
--pvs-copy: color-mix(in oklab, var(--pv-p-ink) 55%, var(--pv-primary));
|
|
--pvs-title: var(--pv-p-ink);
|
|
--pvs-cta: var(--pv-p-ink);
|
|
--pvs-cta-ink: var(--pv-primary);
|
|
--pvs-cta-text: var(--pv-p-on-i);
|
|
--pvs-mark-text: var(--pv-p-ink);
|
|
--pvs-accent-text: var(--pv-t-on-p);
|
|
/* A saturated primary sits far from both ends of the range, so copy mixed
|
|
toward it at the paper percentages stops being readable. The drenched
|
|
page holds its running text closer to the ink. */
|
|
--pvs-copy-mix: 14%;
|
|
--pvs-bars-mix: 16%;
|
|
--pvs-ghost: color-mix(in oklab, var(--pv-p-ink) 22%, var(--pv-primary));
|
|
--pvs-ghost-edge: color-mix(in oklab, var(--pv-p-ink) 45%, var(--pv-primary));
|
|
--pvs-image: color-mix(in oklab, var(--pv-p-ink) 20%, var(--pv-primary));
|
|
--pvs-image-a: color-mix(in oklab, var(--pv-p-ink) 16%, var(--pv-primary));
|
|
--pvs-image-b: color-mix(in oklab, var(--pv-p-ink) 28%, var(--pv-primary));
|
|
--pvs-image-edge: color-mix(in oklab, var(--pv-p-ink) 40%, var(--pv-primary));
|
|
--pvs-signal: var(--pv-tertiary);
|
|
--pvs-signal-ink: var(--pv-t-ink);
|
|
--pvs-signal-text: var(--pv-t-on-t);
|
|
--pvs-bars: color-mix(in oklab, var(--pv-p-ink) 45%, var(--pv-primary));
|
|
--pvs-rule: color-mix(in oklab, var(--pv-p-ink) 28%, var(--pv-primary));
|
|
--pvs-accent-a: var(--pv-secondary);
|
|
--pvs-accent-b: var(--pv-tertiary);
|
|
--pvs-accent-c: color-mix(in oklab, var(--pv-p-ink) 30%, var(--pv-primary));
|
|
--pvs-accent-d: color-mix(in oklab, var(--pv-p-ink) 14%, var(--pv-primary));
|
|
}
|
|
|
|
.ps-desktop,
|
|
.ps-phone {
|
|
min-width: 0;
|
|
min-height: 0;
|
|
overflow: hidden;
|
|
background-color: var(--pvs-ground);
|
|
border: var(--pvs-frame-w) solid var(--pvs-frame-c);
|
|
}
|
|
|
|
.ps-desktop {
|
|
display: grid;
|
|
grid-template-rows: var(--pvs-rows);
|
|
border-radius: var(--pvs-radius-frame);
|
|
}
|
|
|
|
.ps-phone {
|
|
height: 100%;
|
|
aspect-ratio: 8 / 19;
|
|
align-self: stretch;
|
|
display: grid;
|
|
grid-template-rows: 7.2% 1fr 7.5%;
|
|
border-radius: var(--pvs-radius-frame-phone);
|
|
}
|
|
|
|
.ps-nav {
|
|
display: grid;
|
|
grid-template-columns: var(--pvs-nav-cols);
|
|
gap: 24px;
|
|
align-items: center;
|
|
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 {
|
|
width: clamp(8px, 1.15vw, 12px);
|
|
height: auto;
|
|
aspect-ratio: 1;
|
|
background-color: var(--pvs-cta);
|
|
border-radius: var(--pvs-radius-dot);
|
|
}
|
|
|
|
.ps-nav-bars {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: 27px;
|
|
}
|
|
|
|
.ps-nav-bars i {
|
|
display: block;
|
|
width: clamp(18px, 7%, 34px);
|
|
height: 4px;
|
|
background-color: var(--pvs-bars);
|
|
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: var(--pvs-radius-control);
|
|
box-shadow: var(--pvs-shadow-control);
|
|
}
|
|
|
|
.ps-hero {
|
|
min-height: 0;
|
|
display: grid;
|
|
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 {
|
|
display: grid;
|
|
gap: 0;
|
|
}
|
|
|
|
.ps-eyebrow {
|
|
width: 54%;
|
|
height: 10px;
|
|
margin-bottom: 14px;
|
|
background-color: var(--pvs-eyebrow);
|
|
border-radius: var(--pvs-radius-bar);
|
|
}
|
|
|
|
.ps-headline {
|
|
display: grid;
|
|
gap: 8px;
|
|
margin: 0 0 14px;
|
|
}
|
|
|
|
.ps-headline i {
|
|
display: block;
|
|
width: 100%;
|
|
height: 15px;
|
|
background-color: var(--pvs-headline);
|
|
border-radius: var(--pvs-radius-bar);
|
|
}
|
|
|
|
.ps-headline i:last-child {
|
|
width: 72%;
|
|
}
|
|
|
|
.ps-copy {
|
|
display: grid;
|
|
gap: 8px;
|
|
margin-bottom: 18px;
|
|
}
|
|
|
|
.ps-copy i {
|
|
display: block;
|
|
width: 64%;
|
|
height: 8px;
|
|
background-color: var(--pvs-copy);
|
|
border-radius: var(--pvs-radius-bar);
|
|
}
|
|
|
|
.ps-copy i:last-child {
|
|
width: 48%;
|
|
}
|
|
|
|
.ps-actions {
|
|
display: flex;
|
|
gap: clamp(5px, 1vw, 10px);
|
|
margin-top: 0;
|
|
}
|
|
|
|
.ps-actions i {
|
|
width: clamp(42px, 42%, 86px);
|
|
height: clamp(12px, 2.35vw, 29px);
|
|
background-color: var(--pvs-cta);
|
|
border-radius: var(--pvs-radius-control);
|
|
box-shadow: var(--pvs-shadow-control);
|
|
}
|
|
|
|
.ps-actions i:last-child {
|
|
background-color: var(--pvs-ghost);
|
|
border: 1px solid var(--pvs-ghost-edge);
|
|
}
|
|
|
|
.ps-image {
|
|
position: relative;
|
|
width: 100%;
|
|
height: 100%;
|
|
min-height: 0;
|
|
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: var(--pvs-radius-surface);
|
|
box-shadow: var(--pvs-shadow-surface);
|
|
}
|
|
|
|
.ps-image::after {
|
|
content: "";
|
|
position: absolute;
|
|
top: 8%;
|
|
right: 8%;
|
|
width: clamp(6px, 1.1vw, 10px);
|
|
aspect-ratio: 1;
|
|
background-color: var(--pvs-accent-d);
|
|
border-radius: var(--pvs-radius-dot);
|
|
}
|
|
|
|
.ps-proof {
|
|
min-width: 0;
|
|
display: grid;
|
|
grid-template-columns: 1.05fr 1fr 1fr 1.15fr;
|
|
align-items: stretch;
|
|
column-gap: var(--ps-proof-gap);
|
|
padding: 2% var(--pvs-gutter-end) 2% var(--pvs-gutter);
|
|
background-color: var(--pvs-band-alt);
|
|
position: relative;
|
|
}
|
|
|
|
.ps-proof::before,
|
|
.ps-proof::after {
|
|
content: "";
|
|
position: absolute;
|
|
left: var(--pvs-gutter);
|
|
right: var(--pvs-gutter-end);
|
|
height: var(--pvs-divider-w);
|
|
background-color: var(--pvs-divider-c);
|
|
}
|
|
|
|
.ps-proof::before {
|
|
top: 0;
|
|
}
|
|
|
|
.ps-proof::after {
|
|
bottom: 0;
|
|
}
|
|
|
|
.ps-proof-item {
|
|
min-width: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10%;
|
|
height: 100%;
|
|
padding: 0;
|
|
}
|
|
|
|
.ps-proof-item + .ps-proof-item {
|
|
border-left: var(--pvs-divider-w) solid var(--pvs-divider-c);
|
|
}
|
|
|
|
.ps-proof-item i {
|
|
flex: none;
|
|
width: clamp(7px, 1.5vw, 13px);
|
|
aspect-ratio: 1;
|
|
background-color: var(--pvs-accent-a);
|
|
border-radius: var(--pvs-radius-dot);
|
|
}
|
|
|
|
.picker-artboard:not(.picker-preview-type) .ps-proof {
|
|
grid-template-columns:
|
|
minmax(0, 1fr) auto
|
|
minmax(0, 1fr) auto
|
|
minmax(0, 1fr) auto
|
|
minmax(0, 1fr);
|
|
}
|
|
|
|
.picker-artboard:not(.picker-preview-type) .ps-proof-item + .ps-proof-item {
|
|
border-left: 0;
|
|
}
|
|
|
|
.picker-artboard:not(.picker-preview-type) .ps-proof-divider {
|
|
width: var(--pvs-divider-w);
|
|
align-self: stretch;
|
|
background-color: var(--pvs-divider-c);
|
|
}
|
|
|
|
.picker-artboard:not(.picker-preview-type) .ps-phone-body .ps-proof {
|
|
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
|
|
}
|
|
|
|
.picker-artboard:not(.picker-preview-type) .ps-proof-item:nth-of-type(2) i {
|
|
background-color: var(--pvs-accent-b);
|
|
}
|
|
|
|
.picker-artboard:not(.picker-preview-type) .ps-proof-item:nth-of-type(3) i {
|
|
background-color: var(--pvs-accent-c);
|
|
}
|
|
|
|
.picker-artboard:not(.picker-preview-type) .ps-proof-item:nth-of-type(4) i {
|
|
background-color: var(--pvs-accent-d);
|
|
}
|
|
|
|
/* The label takes whatever the dot leaves, so a claim reads as one line of
|
|
the cell rather than a bar sized to a share of it. */
|
|
.ps-proof-item span {
|
|
flex: 1;
|
|
height: 6px;
|
|
background-color: var(--pvs-bars);
|
|
border-radius: var(--pvs-radius-bar);
|
|
}
|
|
|
|
.ps-editorial {
|
|
min-height: 0;
|
|
display: grid;
|
|
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 {
|
|
display: grid;
|
|
gap: clamp(3px, 0.55vw, 6px);
|
|
}
|
|
|
|
.ps-editorial-copy strong {
|
|
width: 78%;
|
|
height: 11px;
|
|
background-color: var(--pvs-title);
|
|
border-radius: var(--pvs-radius-bar);
|
|
}
|
|
|
|
.ps-editorial-copy span {
|
|
display: grid;
|
|
gap: 6px;
|
|
}
|
|
|
|
.ps-editorial-copy span i {
|
|
display: block;
|
|
width: 90%;
|
|
height: 6px;
|
|
background-color: var(--pvs-bars);
|
|
border-radius: var(--pvs-radius-bar);
|
|
}
|
|
|
|
.ps-editorial-copy span i:last-child {
|
|
width: 80%;
|
|
}
|
|
|
|
.ps-editorial-copy > em {
|
|
width: 30%;
|
|
height: 6px;
|
|
margin-top: 3px;
|
|
background-color: var(--pvs-signal);
|
|
border-radius: var(--pvs-radius-bar);
|
|
}
|
|
|
|
.ps-gallery {
|
|
min-width: 0;
|
|
height: 100%;
|
|
display: grid;
|
|
grid-template-columns: var(--pvs-gallery-cols);
|
|
gap: var(--pvs-gallery-gap);
|
|
}
|
|
|
|
.ps-gallery-item {
|
|
min-width: 0;
|
|
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 {
|
|
gap: 9px;
|
|
}
|
|
|
|
.ps-desktop .ps-editorial-copy strong {
|
|
height: 11px;
|
|
}
|
|
|
|
.ps-desktop .ps-editorial-copy > em {
|
|
height: 5px;
|
|
margin-top: 9px;
|
|
}
|
|
|
|
.ps-gallery-item > i {
|
|
display: block;
|
|
min-height: 0;
|
|
background-color: var(--pvs-accent-a);
|
|
border-radius: var(--pvs-radius-surface);
|
|
}
|
|
|
|
.ps-gallery-item:nth-child(2) > i {
|
|
background-color: var(--pvs-accent-b);
|
|
}
|
|
|
|
.ps-gallery-item:nth-child(3) > i {
|
|
background-color: var(--pvs-accent-c);
|
|
}
|
|
|
|
.ps-gallery-item:nth-child(4) > i {
|
|
background-color: var(--pvs-accent-d);
|
|
}
|
|
|
|
.ps-gallery-item span {
|
|
display: grid;
|
|
gap: 6px;
|
|
}
|
|
|
|
.ps-gallery-item b {
|
|
display: block;
|
|
width: 76%;
|
|
height: 6px;
|
|
background-color: var(--pvs-bars);
|
|
border-radius: var(--pvs-radius-bar);
|
|
}
|
|
|
|
.ps-gallery-item b:last-child {
|
|
width: 58%;
|
|
}
|
|
|
|
.ps-footer {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding-inline: var(--pvs-gutter);
|
|
background-color: var(--pvs-band);
|
|
border-top: var(--pvs-divider-w) solid var(--pvs-divider-c);
|
|
}
|
|
|
|
.ps-footer-links {
|
|
display: flex;
|
|
gap: clamp(8px, 2vw, 18px);
|
|
}
|
|
|
|
.ps-footer-links i,
|
|
.ps-phone-footer i {
|
|
width: clamp(12px, 3vw, 28px);
|
|
height: 4px;
|
|
background-color: var(--pvs-bars);
|
|
border-radius: var(--pvs-radius-bar);
|
|
}
|
|
|
|
.ps-footer-mark {
|
|
width: 7%;
|
|
height: 5px;
|
|
background-color: var(--pvs-bars);
|
|
border-radius: var(--pvs-radius-bar);
|
|
}
|
|
|
|
/* Phone artboard: the same site on mobile. */
|
|
.ps-phone-top {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
min-height: 0;
|
|
padding: 0 6%;
|
|
background-color: var(--pvs-surface);
|
|
border-bottom: var(--pvs-divider-w) solid var(--pvs-divider-c);
|
|
box-shadow: var(--pvs-shadow-chrome);
|
|
}
|
|
|
|
.ps-phone-top .ps-brand-block {
|
|
width: 9px;
|
|
}
|
|
|
|
.ps-phone-top .ps-nav-action {
|
|
width: 16%;
|
|
height: 6px;
|
|
}
|
|
|
|
.ps-phone-body {
|
|
min-height: 0;
|
|
display: grid;
|
|
grid-template-rows: 31% auto auto auto 34px auto minmax(0, 1fr);
|
|
gap: 6px;
|
|
padding: 6%;
|
|
}
|
|
|
|
.ps-phone-body .ps-image {
|
|
margin: 0;
|
|
min-height: 0;
|
|
}
|
|
|
|
.ps-phone-body .ps-headline i {
|
|
height: clamp(5px, 0.75vw, 8px);
|
|
}
|
|
|
|
.ps-phone-body .ps-headline {
|
|
margin: 8px 0 0;
|
|
gap: 7px;
|
|
}
|
|
|
|
.ps-phone-body .ps-headline i:first-child {
|
|
width: 54%;
|
|
height: 9px;
|
|
}
|
|
|
|
.ps-phone-body .ps-headline i:last-child {
|
|
width: 82%;
|
|
height: 8px;
|
|
}
|
|
|
|
.ps-phone-body .ps-copy {
|
|
margin: 4px 0 0;
|
|
gap: 7px;
|
|
}
|
|
|
|
.ps-phone-body .ps-copy i {
|
|
width: 68%;
|
|
height: 6px;
|
|
}
|
|
|
|
.ps-phone-body .ps-copy i:last-child {
|
|
display: block;
|
|
width: 52%;
|
|
}
|
|
|
|
.ps-phone-body .ps-actions {
|
|
gap: 7px;
|
|
margin-top: 6px;
|
|
}
|
|
|
|
.ps-phone-body .ps-actions i {
|
|
width: 43%;
|
|
height: 18px;
|
|
}
|
|
|
|
.ps-phone-body .ps-proof {
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
align-items: stretch;
|
|
margin-inline: 0;
|
|
margin-top: 5px;
|
|
padding: 2% 3.2% 2% 5.5%;
|
|
}
|
|
|
|
.ps-phone-body .ps-proof-item {
|
|
height: 100%;
|
|
padding: 0;
|
|
}
|
|
|
|
.ps-phone-body .ps-proof-item i {
|
|
width: 10px;
|
|
}
|
|
|
|
.ps-phone-body .ps-proof-item span {
|
|
height: 6px;
|
|
}
|
|
|
|
.ps-phone-body .ps-editorial-copy strong {
|
|
width: 38%;
|
|
height: 7px;
|
|
}
|
|
|
|
.ps-phone-body .ps-editorial-copy {
|
|
margin-top: 10px;
|
|
}
|
|
|
|
.ps-phone-body .ps-editorial-copy span i {
|
|
height: 6px;
|
|
}
|
|
|
|
.ps-phone-body .ps-proof::before,
|
|
.ps-phone-body .ps-proof::after {
|
|
left: 0;
|
|
right: 0;
|
|
}
|
|
|
|
.ps-phone-body .ps-gallery {
|
|
grid-template-columns: 1.05fr 0.95fr;
|
|
gap: 10%;
|
|
margin-left: 1px;
|
|
margin-top: 6px;
|
|
}
|
|
|
|
.ps-phone-body .ps-gallery-item {
|
|
grid-template-rows: 36px auto;
|
|
gap: 6px;
|
|
}
|
|
|
|
.ps-phone-body .ps-gallery-item b {
|
|
height: 5px;
|
|
}
|
|
|
|
.ps-phone-body .ps-gallery-item span {
|
|
align-content: start;
|
|
gap: 6px;
|
|
}
|
|
|
|
.ps-phone-footer {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-around;
|
|
min-height: 0;
|
|
padding-inline: 6%;
|
|
background-color: var(--pvs-surface);
|
|
border-top: var(--pvs-divider-w) solid var(--pvs-divider-c);
|
|
}
|
|
|
|
.picker-artboard:not(.picker-preview-type) :is(
|
|
.ps-desktop,
|
|
.ps-phone,
|
|
.ps-nav,
|
|
.ps-proof,
|
|
.ps-proof::before,
|
|
.ps-proof::after,
|
|
.ps-proof-item,
|
|
.ps-footer,
|
|
.ps-phone-top,
|
|
.ps-phone-footer,
|
|
.ps-brand-block,
|
|
.ps-nav-bars i,
|
|
.ps-nav-action,
|
|
.ps-eyebrow,
|
|
.ps-headline i,
|
|
.ps-copy i,
|
|
.ps-actions i,
|
|
.ps-image,
|
|
.ps-image::after,
|
|
.ps-proof-item i,
|
|
.ps-proof-item span,
|
|
.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,
|
|
.ps-footer-mark,
|
|
.ps-phone-footer i
|
|
) {
|
|
transition:
|
|
background-color 280ms var(--ks-ease),
|
|
background-image 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(
|
|
.ps-desktop,
|
|
.ps-phone,
|
|
.ps-nav,
|
|
.ps-proof,
|
|
.ps-proof::before,
|
|
.ps-proof::after,
|
|
.ps-proof-item,
|
|
.ps-footer,
|
|
.ps-phone-top,
|
|
.ps-phone-footer,
|
|
.ps-brand-block,
|
|
.ps-nav-bars i,
|
|
.ps-nav-action,
|
|
.ps-eyebrow,
|
|
.ps-headline i,
|
|
.ps-copy i,
|
|
.ps-actions i,
|
|
.ps-image,
|
|
.ps-image::after,
|
|
.ps-proof-item i,
|
|
.ps-proof-item span,
|
|
.ps-editorial-copy strong,
|
|
.ps-editorial-copy > em,
|
|
.ps-gallery-item > i,
|
|
.ps-gallery-item b,
|
|
.ps-footer-links i,
|
|
.ps-footer-mark,
|
|
.ps-phone-footer i,
|
|
.pt-headline,
|
|
.pt-body,
|
|
.pt-section-body
|
|
) {
|
|
transition:
|
|
background-color 280ms var(--ks-ease),
|
|
background-image 280ms var(--ks-ease),
|
|
border-color 280ms var(--ks-ease),
|
|
color 280ms var(--ks-ease);
|
|
}
|
|
|
|
/* Every screen past the start ends the same way: the commitment centered and
|
|
foiled, retreat parked underneath it and deliberately quiet. */
|
|
.picker-actions-stack {
|
|
display: grid;
|
|
justify-items: center;
|
|
gap: 14px;
|
|
}
|
|
|
|
.picker-back {
|
|
padding: 4px 8px;
|
|
border: 0;
|
|
background: none;
|
|
color: var(--ks-text-muted);
|
|
font-family: var(--ks-font);
|
|
font-size: 0.86rem;
|
|
letter-spacing: 0.02em;
|
|
cursor: pointer;
|
|
transition: color 180ms var(--ks-ease);
|
|
}
|
|
|
|
.picker-back:hover {
|
|
color: var(--ks-champagne);
|
|
}
|
|
|
|
.picker-back:focus-visible {
|
|
outline: 1px solid var(--ks-kinpaku);
|
|
outline-offset: 3px;
|
|
}
|
|
|
|
/* Screen 04 mirrors screen 03's frame exactly: same centered headline, same
|
|
options-left preview-right grid, same dual artboards. Only two things are
|
|
its own: the option cards are large type specimens, and the wireframe's
|
|
headline and body are real text so the pair is judged as glyphs. */
|
|
.picker-type {
|
|
display: grid;
|
|
gap: 48px;
|
|
padding-block: clamp(32px, 5svh, 48px) clamp(24px, 3.5svh, 48px);
|
|
}
|
|
|
|
/* Six pair cards are taller than four strategy rows, and left to themselves
|
|
they set the row height and push Select this pair below the fold. The row
|
|
is capped against the viewport instead, leaving the list to scroll inside
|
|
it; the subtraction covers the title above and the CTA stack below. */
|
|
.picker-type-grid {
|
|
max-height: calc(100svh - 346px);
|
|
display: grid;
|
|
grid-template-columns: minmax(340px, 420px) minmax(0, 1fr);
|
|
gap: 50px;
|
|
align-items: stretch;
|
|
}
|
|
|
|
/* The rail keeps the row height owned by the preview: the fieldset scrolls
|
|
inside it instead of stretching the page when six tall cards stack up. The
|
|
control row underneath is what tells you the list runs past its frame. On
|
|
wide screens the rail stops growing at 570px so a tall preview does not
|
|
turn six font cards into a page-length column. */
|
|
.picker-type-rail {
|
|
min-width: 0;
|
|
min-height: 0;
|
|
display: grid;
|
|
grid-template-rows: minmax(0, 1fr) auto;
|
|
gap: 14px;
|
|
}
|
|
|
|
@media (min-width: 1200px) {
|
|
.picker-type-rail {
|
|
max-height: 570px;
|
|
}
|
|
}
|
|
|
|
.picker-type-options {
|
|
min-height: 0;
|
|
overflow-y: auto;
|
|
align-content: start;
|
|
scrollbar-width: thin;
|
|
scrollbar-color: color-mix(in oklab, var(--ks-patina-deep) 35%, transparent) transparent;
|
|
}
|
|
|
|
/* The rail keeps the chosen pair in the first row, and it makes that move only
|
|
once the pointer and focus have both left the list, so no row is ever pulled
|
|
out from under a cursor and arrow keys can cross the group without it
|
|
re-sorting between presses. Waiting costs the user the sight of the travel,
|
|
so the row that has taken the top says so once: its wash and its spine come
|
|
up bright and settle to the resting checked state. Both keyframes animate
|
|
color and nothing else, so the acknowledgement cannot move the list it is
|
|
drawing the eye to. */
|
|
@keyframes picker-pair-hoisted {
|
|
from {
|
|
background-color: color-mix(in oklab, var(--ks-patina) 32%, var(--ks-lacquer-raised));
|
|
}
|
|
}
|
|
|
|
@keyframes picker-pair-hoisted-spine {
|
|
from {
|
|
background-color: var(--ks-patina-pale);
|
|
}
|
|
}
|
|
|
|
.picker-type-option[data-hoisted] {
|
|
animation: picker-pair-hoisted 640ms var(--ks-ease);
|
|
}
|
|
|
|
.picker-type-option[data-hoisted]::before {
|
|
animation: picker-pair-hoisted-spine 640ms var(--ks-ease);
|
|
}
|
|
|
|
.picker-type-controls {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
}
|
|
|
|
/* Screen 05 keeps screen 04's frame and swaps the artboards for a specimen
|
|
sheet. It is the one question that needs explaining before it can be
|
|
answered, so the title carries a lede; every other screen asks something the
|
|
options themselves make obvious. */
|
|
.picker-scale {
|
|
display: grid;
|
|
gap: clamp(24px, 4.4svh, 40px);
|
|
padding-block: clamp(32px, 5svh, 48px) clamp(24px, 3.5svh, 48px);
|
|
}
|
|
|
|
.picker-question-head {
|
|
display: grid;
|
|
justify-items: center;
|
|
gap: clamp(12px, 2svh, 18px);
|
|
}
|
|
|
|
/* Screen 05 is the only question that carries a lede, and one sentence at
|
|
caption size under a display headline reads as a footnote. Set above the
|
|
body size instead, with the measure pulled in so the sentence breaks into
|
|
two balanced lines rather than one long ruler. */
|
|
.picker-question-lede {
|
|
max-width: 46ch;
|
|
color: var(--ks-text);
|
|
font-size: 1.18rem;
|
|
line-height: 1.5;
|
|
text-align: center;
|
|
text-wrap: balance;
|
|
}
|
|
|
|
/* Eight intervals and a lede leave less room than screen 04 has, so the sheet
|
|
is capped harder. The subtraction covers the title, the lede, and the CTA
|
|
stack, which is what keeps Select this scale above the fold. */
|
|
.picker-scale-grid {
|
|
max-height: calc(100svh - 420px);
|
|
display: grid;
|
|
grid-template-columns: minmax(340px, 420px) minmax(0, 1fr);
|
|
gap: 50px;
|
|
align-items: stretch;
|
|
}
|
|
|
|
/* Eight rows scroll rather than share the height four strategy rows can split,
|
|
so this list takes screen 04's rail and screen 03's copy scale, not screen
|
|
03's fixed four-row grid. States come from the standard option row; the only
|
|
thing this list adds is the ratio sitting in front of the name. */
|
|
.picker-scale-options .picker-strategy-copy {
|
|
gap: 4px;
|
|
}
|
|
|
|
.picker-scale-options .picker-strategy-title {
|
|
display: flex;
|
|
align-items: baseline;
|
|
font-family: var(--ks-font);
|
|
font-size: 20px;
|
|
letter-spacing: -0.015em;
|
|
}
|
|
|
|
.picker-scale-options .picker-strategy-desc {
|
|
max-width: 38ch;
|
|
font-size: 14px;
|
|
line-height: 1.45;
|
|
}
|
|
|
|
/* The ratio leads the name: it is the thing being chosen, and eight of them
|
|
in a column read as a ladder when the numbers line up. */
|
|
.picker-scale-ratio {
|
|
margin-inline-end: 12px;
|
|
color: var(--ks-text-faint);
|
|
font-family: var(--ks-mono);
|
|
font-size: 0.94rem;
|
|
font-weight: 400;
|
|
font-variant-numeric: tabular-nums;
|
|
letter-spacing: 0;
|
|
transition: color 180ms var(--ks-ease);
|
|
}
|
|
|
|
/* The ratio answers to the same two states as the name it sits in front of,
|
|
held one step back so the name still leads the row. */
|
|
.picker-strategy-option:hover .picker-scale-ratio,
|
|
.picker-strategy-option:has(input:checked) .picker-scale-ratio {
|
|
color: color-mix(in oklab, var(--ks-patina) 70%, var(--ks-champagne));
|
|
}
|
|
|
|
.picker-scale-note {
|
|
color: var(--ks-text-faint);
|
|
font-size: 0.76rem;
|
|
letter-spacing: 0.04em;
|
|
}
|
|
|
|
/* The sheet is picker chrome, not an artboard: it answers a question about
|
|
size, and every other screen already showed the palette. So it belongs to
|
|
the picker's own dark theme and reads the --ks tokens directly, matching the
|
|
raised panel the options sit in on the left. The one thing it borrows from
|
|
the run is the pair chosen on screen 04, because a scale is judged in the
|
|
faces that will set it.
|
|
|
|
Rows are not equal: the top three are the ones being judged, so they take a
|
|
larger share of the sheet and leave the small steps as the tail they are. */
|
|
.picker-scale-sheet {
|
|
--ts-fit: 1;
|
|
--pt-heading: var(--ks-font-display);
|
|
--pt-body: var(--ks-font);
|
|
--pt-heading-weight: 400;
|
|
min-width: 0;
|
|
min-height: 0;
|
|
display: grid;
|
|
grid-template-rows: 1.34fr 1.2fr 1.1fr repeat(4, minmax(0, 1fr));
|
|
padding: clamp(14px, 2.4%, 28px) clamp(18px, 3.2%, 36px);
|
|
background: var(--ks-lacquer-raised);
|
|
color: var(--ks-champagne);
|
|
border: 1px solid var(--ks-rule);
|
|
border-radius: 2px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* Fixed side columns, because each row is its own grid: sized to their content
|
|
they would land wherever that row's specimen and numbers happened to end,
|
|
and the tag column would step left and right down the sheet and again on
|
|
every font change. Rem holds them still; the sheet's own face does not. */
|
|
.ts-row {
|
|
min-width: 0;
|
|
min-height: 0;
|
|
display: grid;
|
|
grid-template-columns: minmax(0, 1fr) 2.4rem 4.4rem;
|
|
align-items: center;
|
|
gap: clamp(12px, 2vw, 24px);
|
|
border-bottom: 1px dotted var(--ks-rule);
|
|
}
|
|
|
|
.ts-row:last-child {
|
|
border-bottom: 0;
|
|
}
|
|
|
|
.ts-sample {
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
color: var(--ks-text-muted);
|
|
font-family: var(--pt-heading);
|
|
font-weight: var(--pt-heading-weight);
|
|
font-size: calc(var(--ts-size) * var(--ts-fit) * 1px);
|
|
line-height: 1.12;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* The three steps a display scale is actually chosen on. They carry the
|
|
brightest ink in the sheet; everything below them is there to show the
|
|
ratio continuing. */
|
|
.ts-row[data-scale-row="6"] .ts-sample,
|
|
.ts-row[data-scale-row="5"] .ts-sample,
|
|
.ts-row[data-scale-row="4"] .ts-sample {
|
|
color: var(--ks-champagne);
|
|
}
|
|
|
|
.ts-row[data-scale-row="0"] .ts-sample {
|
|
color: var(--ks-text-faint);
|
|
font-family: var(--pt-body);
|
|
font-weight: 400;
|
|
}
|
|
|
|
/* The tag and the numbers hold one size while everything to their left moves,
|
|
which is what makes the movement legible. Both are trimmed to their glyphs
|
|
so they center against the specimen rather than against a line box whose
|
|
height belongs to the font's metrics.
|
|
|
|
The tag is not chrome. It is the name of the step being judged, so it takes
|
|
the sheet's brightest ink and enough weight to be read across the row
|
|
without leaning in. The numbers under it are reference, and sit one step
|
|
back, but a size that needs a squint is not reference either. */
|
|
.ts-tag {
|
|
color: var(--ks-champagne);
|
|
font-family: var(--ks-font);
|
|
font-size: 1.02rem;
|
|
font-weight: 600;
|
|
font-variant-numeric: tabular-nums;
|
|
letter-spacing: 0.04em;
|
|
text-box: trim-both cap alphabetic;
|
|
}
|
|
|
|
.ts-values {
|
|
display: grid;
|
|
justify-items: end;
|
|
gap: 5px;
|
|
font-family: var(--ks-mono);
|
|
font-size: 0.8rem;
|
|
font-variant-numeric: tabular-nums;
|
|
line-height: 1.25;
|
|
}
|
|
|
|
.ts-values b,
|
|
.ts-values i {
|
|
text-box: trim-both cap alphabetic;
|
|
}
|
|
|
|
.ts-values b {
|
|
color: var(--ks-text-muted);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.ts-values i {
|
|
color: var(--ks-text-faint);
|
|
font-style: normal;
|
|
}
|
|
|
|
/* ============================================================
|
|
Screen 11: the icon set. Screen 05's frame again, because the question has
|
|
the same shape: more candidates than fit the rail, and one specimen that
|
|
redraws as the cursor moves down the list.
|
|
============================================================ */
|
|
.picker-icons {
|
|
display: grid;
|
|
gap: clamp(24px, 4.4svh, 40px);
|
|
padding-block: clamp(32px, 5svh, 48px) clamp(24px, 3.5svh, 48px);
|
|
}
|
|
|
|
.picker-icons-grid {
|
|
max-height: calc(100svh - 420px);
|
|
display: grid;
|
|
grid-template-columns: minmax(340px, 420px) minmax(0, 1fr);
|
|
gap: 50px;
|
|
align-items: stretch;
|
|
}
|
|
|
|
/* Eleven rows scroll, so this list takes screen 04's rail rather than screen
|
|
03's fixed row split. States come from the standard option row; what the
|
|
row adds is the pack's own numbers and the way out to its site. */
|
|
.picker-icon-options .picker-strategy-option {
|
|
padding-block: 16px;
|
|
/* The link sits in this gutter. */
|
|
padding-inline-end: 60px;
|
|
}
|
|
|
|
.picker-icon-options .picker-strategy-copy {
|
|
gap: 5px;
|
|
}
|
|
|
|
.picker-icon-options .picker-strategy-title {
|
|
font-family: var(--ks-font);
|
|
font-size: 21px;
|
|
letter-spacing: -0.015em;
|
|
}
|
|
|
|
.picker-icon-options .picker-strategy-desc {
|
|
max-width: 40ch;
|
|
font-size: 13px;
|
|
line-height: 1.45;
|
|
}
|
|
|
|
/* Grid, weight, license: the three facts that decide this question before any
|
|
description does. Measurements, so they are set in the mono face. */
|
|
.picker-icon-meta {
|
|
color: var(--ks-text-faint);
|
|
font-family: var(--ks-mono);
|
|
font-size: 0.7rem;
|
|
}
|
|
|
|
/* An anchor inside a <label> still checks the radio on its way to the new tab,
|
|
so the way out is a sibling of the row and sits above it. */
|
|
.picker-icon-row {
|
|
position: relative;
|
|
display: grid;
|
|
}
|
|
|
|
.picker-icon-options .picker-icon-row:not(:last-child) .picker-strategy-option {
|
|
border-bottom: 1px solid var(--ks-rule);
|
|
}
|
|
|
|
.picker-icon-out {
|
|
position: absolute;
|
|
top: 14px;
|
|
right: 16px;
|
|
width: 30px;
|
|
height: 30px;
|
|
display: grid;
|
|
place-items: center;
|
|
color: var(--ks-text-mute-deep);
|
|
border: 1px solid transparent;
|
|
border-radius: 2px;
|
|
transition:
|
|
color 180ms var(--ks-ease),
|
|
border-color 180ms var(--ks-ease);
|
|
}
|
|
|
|
.picker-icon-out svg {
|
|
width: 15px;
|
|
height: 15px;
|
|
fill: none;
|
|
stroke: currentColor;
|
|
stroke-width: 1.4;
|
|
stroke-linecap: round;
|
|
stroke-linejoin: round;
|
|
}
|
|
|
|
.picker-icon-out:hover {
|
|
color: var(--ks-patina);
|
|
border-color: var(--ks-rule);
|
|
}
|
|
|
|
.picker-icon-out:focus-visible {
|
|
outline: 2px solid var(--ks-patina);
|
|
outline-offset: 2px;
|
|
}
|
|
|
|
/* Like the scale sheet, the specimen is picker chrome in the picker's own
|
|
theme: it answers a question about drawing, and the palette has had its
|
|
screens already. One field divided by hairlines, not twenty-four bordered
|
|
tiles on a dark ground, which is the icon grid this project spends its
|
|
detector telling people not to ship. */
|
|
.picker-icon-sheet {
|
|
min-width: 0;
|
|
min-height: 0;
|
|
display: grid;
|
|
grid-template-rows: minmax(0, 1fr) auto;
|
|
background: var(--ks-lacquer-raised);
|
|
border: 1px solid var(--ks-rule);
|
|
border-radius: 2px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.picker-icon-strip {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
/* A label row, so it gives height back to the field on a short screen instead
|
|
of holding a comfortable padding the specimen has to pay for. */
|
|
padding: clamp(9px, 1.5svh, 14px) clamp(16px, 2.2%, 24px);
|
|
}
|
|
|
|
/* The division is the grid's own gap over a rule-colored ground, so it reads as
|
|
one continuous hairline and no cell owns an edge. */
|
|
.picker-icon-field {
|
|
min-height: 0;
|
|
display: grid;
|
|
grid-template-columns: repeat(6, minmax(0, 1fr));
|
|
grid-auto-rows: minmax(0, 1fr);
|
|
gap: 1px;
|
|
background: var(--ks-rule);
|
|
border-bottom: 1px solid var(--ks-rule);
|
|
}
|
|
|
|
/* Nothing to divide when the fetch fails, so the field drops the hairlines and
|
|
carries the reason in the space the glyphs would have used. */
|
|
.picker-icon-field[data-empty] {
|
|
grid-template-columns: minmax(0, 1fr);
|
|
place-items: center;
|
|
padding: 24px;
|
|
background: var(--ks-lacquer-raised);
|
|
color: var(--ks-text-muted);
|
|
text-align: center;
|
|
}
|
|
|
|
.picker-icon-cell {
|
|
min-width: 0;
|
|
min-height: 0;
|
|
display: grid;
|
|
place-items: center;
|
|
background: var(--ks-lacquer-raised);
|
|
color: var(--ks-champagne);
|
|
}
|
|
|
|
.picker-icon-cell svg {
|
|
width: clamp(22px, 2.2vw, 32px);
|
|
height: auto;
|
|
}
|
|
|
|
/* The frame is short and wide before it is anything else, so on a laptop the
|
|
same twenty-four glyphs lie down into three long rows rather than four
|
|
letterbox ones. Nothing is dropped and nothing is scaled down. */
|
|
@media (max-height: 830px) {
|
|
.picker-icon-field {
|
|
grid-template-columns: repeat(8, minmax(0, 1fr));
|
|
}
|
|
}
|
|
|
|
/* A pack is chosen for interface work, and interface work happens at 16px.
|
|
The row underneath is the same twenty-four glyphs at the size they will
|
|
actually be read, which is where a busy set gives itself away. */
|
|
.picker-icon-strip-label {
|
|
flex: none;
|
|
color: var(--ks-text-faint);
|
|
font-family: var(--ks-mono);
|
|
font-size: 0.7rem;
|
|
}
|
|
|
|
.picker-icon-strip-row {
|
|
min-width: 0;
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 8px 11px;
|
|
color: var(--ks-text-muted);
|
|
}
|
|
|
|
.picker-icon-strip-row svg {
|
|
width: 16px;
|
|
height: 16px;
|
|
}
|
|
|
|
.picker-type-scroll {
|
|
display: flex;
|
|
gap: 8px;
|
|
}
|
|
|
|
.picker-type-custom {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
height: 36px;
|
|
padding: 0 16px;
|
|
background: transparent;
|
|
border: 1px solid var(--ks-rule);
|
|
border-radius: 2px;
|
|
color: var(--ks-text-muted);
|
|
font-family: var(--ks-font);
|
|
font-size: 0.8rem;
|
|
letter-spacing: 0.04em;
|
|
cursor: pointer;
|
|
transition:
|
|
color 180ms var(--ks-ease),
|
|
border-color 180ms var(--ks-ease),
|
|
background-color 180ms var(--ks-ease);
|
|
}
|
|
|
|
.picker-type-custom svg {
|
|
width: 14px;
|
|
height: 14px;
|
|
fill: none;
|
|
stroke: currentColor;
|
|
stroke-width: 1.4;
|
|
stroke-linecap: square;
|
|
}
|
|
|
|
.picker-type-custom:hover {
|
|
color: var(--ks-champagne);
|
|
border-color: color-mix(in oklab, var(--ks-patina) 45%, transparent);
|
|
background: color-mix(in oklab, var(--ks-patina) 8%, transparent);
|
|
}
|
|
|
|
.picker-type-custom:focus-visible {
|
|
outline: 1px solid var(--ks-kinpaku);
|
|
outline-offset: 2px;
|
|
}
|
|
|
|
/* Custom font sheet. A native <dialog> carries the modal behaviour, so the
|
|
styling only has to dress it. */
|
|
.picker-modal {
|
|
width: min(520px, calc(100vw - 48px));
|
|
margin: auto;
|
|
padding: 0;
|
|
background: var(--ks-lacquer-raised);
|
|
border: 1px solid var(--ks-rule);
|
|
border-radius: 2px;
|
|
color: var(--ks-text);
|
|
}
|
|
|
|
.picker-modal::backdrop {
|
|
background: oklch(2% 0.004 95 / 0.72);
|
|
backdrop-filter: blur(3px);
|
|
}
|
|
|
|
.picker-modal-inner {
|
|
display: grid;
|
|
gap: 24px;
|
|
padding: 32px;
|
|
}
|
|
|
|
.picker-modal-head {
|
|
display: grid;
|
|
gap: 8px;
|
|
}
|
|
|
|
.picker-modal-head h2 {
|
|
color: var(--ks-champagne);
|
|
font-family: var(--ks-font);
|
|
font-size: 1.7rem;
|
|
font-weight: 400;
|
|
line-height: 1.1;
|
|
}
|
|
|
|
.picker-modal-head p {
|
|
color: var(--ks-text-muted);
|
|
font-size: 0.9rem;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.picker-modal-field {
|
|
display: grid;
|
|
gap: 8px;
|
|
padding: 20px;
|
|
border: 1px solid var(--ks-rule);
|
|
border-radius: 2px;
|
|
}
|
|
|
|
.picker-modal-field legend {
|
|
padding-inline: 8px;
|
|
margin-inline-start: -8px;
|
|
color: var(--ks-kinpaku);
|
|
font-size: 0.68rem;
|
|
letter-spacing: 0.16em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.picker-modal-label {
|
|
color: var(--ks-text-muted);
|
|
font-size: 0.78rem;
|
|
letter-spacing: 0.02em;
|
|
}
|
|
|
|
.picker-modal-input {
|
|
height: 40px;
|
|
padding: 0 12px;
|
|
background: var(--ks-lacquer-deep);
|
|
border: 1px solid var(--ks-rule);
|
|
border-radius: 2px;
|
|
color: var(--ks-champagne);
|
|
font-family: var(--ks-font);
|
|
font-size: 0.88rem;
|
|
}
|
|
|
|
.picker-modal-input::placeholder {
|
|
color: color-mix(in oklab, var(--ks-text-muted) 65%, transparent);
|
|
}
|
|
|
|
.picker-modal-input:focus-visible {
|
|
outline: 1px solid var(--ks-patina);
|
|
outline-offset: 1px;
|
|
border-color: transparent;
|
|
}
|
|
|
|
.picker-modal-or {
|
|
color: var(--ks-text-muted);
|
|
font-size: 0.68rem;
|
|
letter-spacing: 0.16em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.picker-modal-file-block {
|
|
display: grid;
|
|
gap: 10px;
|
|
}
|
|
|
|
.picker-modal-file {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
min-width: 0;
|
|
}
|
|
|
|
.picker-modal-file input {
|
|
position: absolute;
|
|
width: 1px;
|
|
height: 1px;
|
|
overflow: hidden;
|
|
clip-path: inset(50%);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.picker-modal-file-button {
|
|
flex: none;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
padding: 8px 14px;
|
|
border: 1px solid var(--ks-rule);
|
|
border-radius: 2px;
|
|
color: var(--ks-champagne);
|
|
font-size: 0.8rem;
|
|
cursor: pointer;
|
|
transition: border-color 180ms var(--ks-ease), color 180ms var(--ks-ease);
|
|
}
|
|
|
|
.picker-modal-file:hover .picker-modal-file-button {
|
|
border-color: color-mix(in oklab, var(--ks-patina) 45%, transparent);
|
|
color: var(--ks-patina);
|
|
}
|
|
|
|
.picker-modal-file input:focus-visible + .picker-modal-file-button {
|
|
outline: 1px solid var(--ks-kinpaku);
|
|
outline-offset: 2px;
|
|
}
|
|
|
|
.picker-modal-file-list {
|
|
display: grid;
|
|
gap: 6px;
|
|
margin: 0;
|
|
padding: 0;
|
|
list-style: none;
|
|
}
|
|
|
|
.picker-modal-file-item {
|
|
display: grid;
|
|
grid-template-columns: minmax(0, 1fr) auto;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 8px 10px;
|
|
background: var(--ks-lacquer-deep);
|
|
border: 1px solid var(--ks-rule);
|
|
border-radius: 2px;
|
|
}
|
|
|
|
.picker-modal-file-item span {
|
|
min-width: 0;
|
|
overflow: hidden;
|
|
color: var(--ks-text);
|
|
font-size: 0.8rem;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.picker-modal-file-remove {
|
|
flex: none;
|
|
width: 24px;
|
|
height: 24px;
|
|
padding: 0;
|
|
border: 0;
|
|
border-radius: 2px;
|
|
background: transparent;
|
|
color: var(--ks-text-muted);
|
|
font-size: 1.1rem;
|
|
line-height: 1;
|
|
cursor: pointer;
|
|
transition: color 180ms var(--ks-ease), background-color 180ms var(--ks-ease);
|
|
}
|
|
|
|
.picker-modal-file-remove:hover {
|
|
color: var(--ks-champagne);
|
|
background: color-mix(in oklab, var(--ks-patina) 10%, transparent);
|
|
}
|
|
|
|
.picker-modal-file-remove:focus-visible {
|
|
outline: 1px solid var(--ks-kinpaku);
|
|
outline-offset: 2px;
|
|
}
|
|
|
|
.picker-modal-status:empty {
|
|
display: none;
|
|
}
|
|
|
|
.picker-modal-status {
|
|
color: var(--ks-patina);
|
|
font-size: 0.82rem;
|
|
}
|
|
|
|
.picker-modal-hint:empty {
|
|
display: none;
|
|
}
|
|
|
|
/* What the sheet is still waiting for, in the voice of the field labels above
|
|
it rather than an error. Nothing is wrong yet: the form is unfinished, so
|
|
this is muted caption ink and carries no vermilion and no icon. It sits in
|
|
the footer because that is where the button it explains is. */
|
|
.picker-modal-hint {
|
|
color: var(--ks-text-muted);
|
|
font-size: 0.82rem;
|
|
line-height: 1.45;
|
|
}
|
|
|
|
/* Cancel then commit, both hard against the right padding edge. The dialog is a
|
|
fixed 520px behind a 1200px width gate and the pair needs 247px of it, so
|
|
wrapping can only ever fire on a stray long label; nowrap keeps the two
|
|
together and lets them shrink rather than stranding the commit button on a
|
|
line of its own under Cancel. */
|
|
.picker-modal-actions {
|
|
display: flex;
|
|
flex-wrap: nowrap;
|
|
align-items: center;
|
|
justify-content: flex-end;
|
|
gap: 18px;
|
|
}
|
|
|
|
/* Screen 04 takes the standard option row and only tightens it: a specimen
|
|
needs less air around it than a label plus a sentence. */
|
|
.picker-type-options .picker-strategy-option {
|
|
gap: 0;
|
|
padding: 18px 26px 18px 28px;
|
|
}
|
|
|
|
.picker-type-copy {
|
|
min-width: 0;
|
|
display: grid;
|
|
gap: 2px;
|
|
}
|
|
|
|
/* Each face names itself in its own glyphs: the display family large, the
|
|
text family under it at reading size. An uploaded pair can be named after
|
|
its file, so both lines are allowed to break inside a word rather than
|
|
push the marker out of the row. */
|
|
.picker-type-sample-heading {
|
|
min-width: 0;
|
|
color: var(--ks-champagne);
|
|
font-family: var(--pair-heading, var(--ks-font-display));
|
|
font-size: 26px;
|
|
font-weight: var(--pair-heading-weight, 400);
|
|
line-height: 1.15;
|
|
letter-spacing: -0.015em;
|
|
overflow-wrap: anywhere;
|
|
transition: color 180ms var(--ks-ease);
|
|
}
|
|
|
|
.picker-type-sample-body {
|
|
min-width: 0;
|
|
color: var(--ks-text);
|
|
font-family: var(--pair-body, var(--ks-font));
|
|
font-size: 15px;
|
|
font-weight: var(--pair-body-weight, 400);
|
|
line-height: 1.4;
|
|
overflow-wrap: anywhere;
|
|
}
|
|
|
|
/* The role trails the family it names, on the same baseline and in the UI
|
|
font, the way a foundry annotates a specimen. Leading it would put the
|
|
chrome where the face belongs: the reader is judging glyphs first and only
|
|
then asking which slot they fill. */
|
|
.picker-type-role {
|
|
margin-left: 9px;
|
|
color: var(--ks-text-faint);
|
|
font-family: var(--ks-font);
|
|
font-size: 11px;
|
|
font-weight: 400;
|
|
line-height: 1.2;
|
|
letter-spacing: 0.02em;
|
|
white-space: nowrap;
|
|
transition: color 180ms var(--ks-ease);
|
|
}
|
|
|
|
/* The markers take the row's accent so they belong to the selected state,
|
|
mixed back toward faint ink so they never rise to the specimen's level. */
|
|
.picker-strategy-option:hover .picker-type-role,
|
|
.picker-strategy-option:has(input:checked) .picker-type-role {
|
|
color: color-mix(in oklab, var(--ks-patina) 45%, var(--ks-text-faint));
|
|
}
|
|
|
|
/* The descriptor is a phrase about the pairing, so it is set as one. Tracked
|
|
caps read as a category label and made a short sentence look shouted. */
|
|
.picker-type-desc {
|
|
margin-top: 7px;
|
|
color: var(--ks-text-muted);
|
|
font-family: var(--ks-font);
|
|
font-size: 13px;
|
|
font-weight: 400;
|
|
line-height: 1.45;
|
|
}
|
|
|
|
/* One golden-ratio ladder rules every piece of text on the artboards:
|
|
micro-copy sits one step below the body size, the headline two steps
|
|
above it. The phone artboard re-bases the same ladder smaller, so both
|
|
artboards stay on scale. A future type-scale step replaces the ratio;
|
|
until then the preview follows this one everywhere. */
|
|
.picker-preview-type {
|
|
--pt-heading: var(--ks-font-display);
|
|
--pt-body: var(--ks-font);
|
|
--pt-heading-weight: 600;
|
|
--pt-ratio: 1.618;
|
|
--pt-base: clamp(0.72rem, 0.86vw, 0.84rem);
|
|
/* Slot colors were mixed for bars, where a pale fill still reads as a
|
|
shape. Once those bars carry glyphs the same mix disappears, so the type
|
|
artboards re-derive every ink slot at reading contrast. The mix is taken
|
|
against the ground and the ink the strategy left in place, not against
|
|
the neutral: under Drenched the page is the primary, and running text
|
|
mixed toward a neutral it no longer sits on would go invisible. */
|
|
--pvs-copy: color-mix(in oklab, var(--pvs-ground) var(--pvs-copy-mix), var(--pvs-ink));
|
|
--pvs-bars: color-mix(in oklab, var(--pvs-ground) var(--pvs-bars-mix), var(--pvs-ink));
|
|
}
|
|
|
|
/* Drenched is the one page that raises a surface out of its own primary
|
|
rather than out of paper. A bar could take a heavy wash of the page ink and
|
|
still read as a shape; a label printed on that wash cannot, so the second
|
|
button keeps its fill near the ground and lets the border say button, and
|
|
the phone's chrome bands lift by less than they do in the wireframe. */
|
|
:where(#picker-form:has(input[name="color-strategy"][value="drenched"]:checked)) .picker-preview-type {
|
|
--pvs-ghost: color-mix(in oklab, var(--pv-p-ink) 12%, var(--pv-primary));
|
|
--pvs-surface: color-mix(in oklab, var(--pv-p-ink) 5%, var(--pv-primary));
|
|
}
|
|
|
|
/* The phone's chrome bands are the other raised surface, and their labels are
|
|
mixed for the ground the rest of the page sets type on. Same derivation,
|
|
taken against the band they actually sit on. */
|
|
.picker-preview-type :is(.ps-phone-top, .ps-phone-footer) {
|
|
--pvs-bars: color-mix(in oklab, var(--pvs-surface) var(--pvs-bars-mix), var(--pvs-ink));
|
|
}
|
|
|
|
/* The derived steps are declared on both artboard roots because custom
|
|
properties resolve their var() references where they are declared: the
|
|
phone re-bases --pt-base, so it needs its own ladder computation. The
|
|
micro step is floored rather than divided: strict scale fidelity puts it
|
|
under 8px, and this preview exists to be read.
|
|
|
|
The spacing scale hangs off the same base, so an artboard that sets its
|
|
type smaller gets proportionally tighter bands without a second set of
|
|
numbers. The gutter is the one exception: it is a share of the frame,
|
|
because a band's inset reads against the frame edge, not the type. */
|
|
.picker-preview-type,
|
|
.picker-preview-type .ps-phone {
|
|
--pt-micro: max(0.62rem, calc(var(--pt-base) / var(--pt-ratio)));
|
|
--pt-title: calc(var(--pt-base) * var(--pt-ratio));
|
|
--pt-display: calc(var(--pt-base) * var(--pt-ratio) * var(--pt-ratio));
|
|
--ps-band-y: calc(var(--pt-base) * 0.9);
|
|
--ps-gap-xs: calc(var(--pt-base) * 0.4);
|
|
--ps-gap-sm: calc(var(--pt-base) * 0.75);
|
|
--ps-gap-md: calc(var(--pt-base) * 1.25);
|
|
--ps-gap-lg: calc(var(--pt-base) * 2);
|
|
}
|
|
|
|
.picker-preview-type {
|
|
--ps-gutter: 5.5%;
|
|
}
|
|
|
|
.picker-preview-type .ps-phone {
|
|
--ps-gutter: 6%;
|
|
}
|
|
|
|
/* Centering a text box centers its em box, and every family puts its caps
|
|
somewhere else inside that box, so a centered label moves each time the
|
|
pair changes. Trimming to the cap and baseline edges makes the box the
|
|
glyphs, which is what the eye is aligning to. Browsers without it fall
|
|
back to today's line-box centering. */
|
|
.picker-preview-type :is(
|
|
.ps-brand-block,
|
|
.ps-nav-bars i,
|
|
.ps-nav-action,
|
|
.ps-actions i,
|
|
.ps-proof-item span,
|
|
.ps-footer-links i,
|
|
.ps-footer-mark,
|
|
.ps-phone-footer i,
|
|
.ps-gallery-item b
|
|
) {
|
|
text-box: trim-both cap alphabetic;
|
|
}
|
|
|
|
/* One rhythm down the column: the headline, the sentence, and the buttons are
|
|
spaced by the same scale rather than three margins that each drifted. */
|
|
.picker-preview-type .ps-hero-copy {
|
|
align-content: center;
|
|
gap: var(--ps-gap-md);
|
|
}
|
|
|
|
/* The pair is the subject and the image is a placeholder for one, so the copy
|
|
takes the column and the image takes what it needs to read as a picture. */
|
|
.picker-preview-type .ps-hero {
|
|
grid-template-columns: minmax(0, 1fr) 26%;
|
|
gap: var(--ps-gap-lg);
|
|
padding: var(--ps-band-y) var(--ps-gutter);
|
|
}
|
|
|
|
/* The column sets the width and the ratio sets the height, so the picture
|
|
stays a picture instead of stretching to whatever height the band has left.
|
|
The band is only a ceiling, for the pair whose copy runs long. */
|
|
.picker-preview-type .ps-hero > .ps-image {
|
|
width: 100%;
|
|
height: auto;
|
|
aspect-ratio: 4 / 5;
|
|
max-height: 100%;
|
|
align-self: center;
|
|
}
|
|
|
|
/* The artboard's height comes from the frame's aspect ratio, so the bands stay
|
|
proportional and each one has to hold its text rather than push the next
|
|
one down. Real copy needs more of the page than bar glyphs did: the hero
|
|
gives height to the editorial band, which now carries a wrapping title. */
|
|
.picker-preview-type .ps-desktop {
|
|
grid-template-rows: 9.4% 45% 9% 28.3% 8.3%;
|
|
}
|
|
|
|
/* The editorial band splits differently once its bars are words. Four cards
|
|
left the copy column at 147px, under the 169px the widest pairing needs to
|
|
set a four-word title in two lines, and left the last card a pixel short of
|
|
its label. Three cards, in even columns, clears both with room to spare. */
|
|
/* The two columns start together at the top of the row, and the row itself is
|
|
what centers in the band, so a pair that sets a shorter title does not leave
|
|
the whole section sitting against the rule above it. */
|
|
.picker-preview-type .ps-editorial {
|
|
grid-template-columns: minmax(0, 1fr) 38%;
|
|
gap: var(--ps-gap-lg);
|
|
align-content: center;
|
|
align-items: start;
|
|
padding: var(--ps-band-y) var(--ps-gutter);
|
|
}
|
|
|
|
.picker-preview-type .ps-editorial-copy {
|
|
gap: var(--ps-gap-sm);
|
|
}
|
|
|
|
.picker-preview-type .ps-desktop .ps-gallery {
|
|
height: auto;
|
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
gap: var(--ps-gap-md);
|
|
}
|
|
|
|
/* The thumbnail stands in for a picture, not for the band: left to fill the
|
|
leftover height it grew taller than the copy beside it. A square sized off
|
|
the type scale keeps the three cards reading as a set of labels. */
|
|
.picker-preview-type .ps-gallery-item {
|
|
grid-template-rows: auto auto;
|
|
gap: var(--ps-gap-sm);
|
|
align-content: start;
|
|
}
|
|
|
|
.picker-preview-type .ps-gallery-item > i {
|
|
width: 100%;
|
|
max-width: calc(var(--pt-base) * 4);
|
|
aspect-ratio: 1;
|
|
}
|
|
|
|
.picker-preview-type .ps-desktop .ps-gallery-item:nth-child(4) {
|
|
display: none;
|
|
}
|
|
|
|
/* Words set taller than bars, and the phone is the narrowest frame they have
|
|
to fit. Its card row goes: nav, hero, proof and a section of running copy
|
|
already show every face at both sizes, and the row that was left over is
|
|
the one that would clip.
|
|
|
|
Every row after the image sizes to its own content. The band that held the
|
|
proof row at a fixed 34px was the one place a tall pair had nowhere to go.
|
|
Whatever height is left over is spread between the sections rather than
|
|
pooled under the last one, so a compact pair reads as a page with air in it
|
|
and a wide one closes up to its own gap without spilling past the frame. */
|
|
.picker-preview-type .ps-phone-body {
|
|
grid-template-rows: 18% repeat(5, auto);
|
|
gap: var(--ps-gap-sm);
|
|
align-content: space-between;
|
|
padding: var(--ps-band-y) var(--ps-gutter);
|
|
}
|
|
|
|
/* One gap owns the spacing down the phone. The margins underneath it were a
|
|
second, unrelated scale, and every element that carried one landed on a
|
|
different interval from its neighbour. */
|
|
.picker-preview-type .ps-phone-body > * {
|
|
margin: 0;
|
|
}
|
|
|
|
.picker-preview-type .ps-phone-body .ps-actions {
|
|
gap: var(--ps-gap-sm);
|
|
}
|
|
|
|
/* The band already sits inside the body's gutter; its own inset put it a
|
|
step to the right of everything it lines up with. */
|
|
.picker-preview-type .ps-phone-body .ps-proof {
|
|
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
|
|
padding-inline: 0;
|
|
}
|
|
|
|
.picker-preview-type .ps-phone-body .ps-editorial-copy {
|
|
align-content: start;
|
|
}
|
|
|
|
.picker-preview-type .ps-phone-top,
|
|
.picker-preview-type .ps-phone-footer {
|
|
padding-inline: var(--ps-gutter);
|
|
}
|
|
|
|
/* The card row stays off the handset. The widest pairs set the headline in
|
|
three lines here, and the cards are the section that would be pushed past
|
|
the frame; every face the phone has to prove is already shown above. */
|
|
.picker-preview-type .ps-phone-body .ps-gallery {
|
|
display: none;
|
|
}
|
|
|
|
.pt-headline {
|
|
max-width: 16ch;
|
|
margin: 0;
|
|
color: var(--pvs-headline-text);
|
|
font-family: var(--pt-heading);
|
|
font-size: var(--pt-display);
|
|
font-weight: var(--pt-heading-weight);
|
|
line-height: 1.15;
|
|
letter-spacing: -0.02em;
|
|
overflow-wrap: break-word;
|
|
text-wrap: balance;
|
|
}
|
|
|
|
.pt-body {
|
|
max-width: 42ch;
|
|
margin: 0;
|
|
color: var(--pvs-copy);
|
|
font-family: var(--pt-body);
|
|
font-size: var(--pt-base);
|
|
line-height: 1.55;
|
|
}
|
|
|
|
/* The section paragraph is one element, so its leading is the face's own and
|
|
runs unbroken from the first line to the last. It was two stacked bars in
|
|
the wireframe, and two elements set two lines with the container's gap
|
|
between them, which read as a pair of unrelated lines rather than a
|
|
paragraph. Micro size over a near-full-column measure wants more leading
|
|
than the single-line labels around it, hence the step past their 1.4.
|
|
|
|
No measure cap: the artboard is a fixed-proportion frame with a ceiling of
|
|
its own, so the copy column stops growing before the line does. Measured
|
|
67 to 75 characters on the desktop board and 38 to 51 on the handset,
|
|
across every width from 1280 up. */
|
|
.pt-section-body {
|
|
margin: 0;
|
|
color: var(--pvs-copy);
|
|
font-family: var(--pt-body);
|
|
font-size: var(--pt-micro);
|
|
line-height: 1.6;
|
|
}
|
|
|
|
/* Phone artboard type: the same ladder re-based to handset scale, held above
|
|
the legibility floor rather than scaled straight down. */
|
|
.picker-preview-type .ps-phone {
|
|
--pt-base: clamp(0.66rem, 0.72vw, 0.74rem);
|
|
}
|
|
|
|
.ps-phone .pt-headline,
|
|
.ps-phone .pt-body {
|
|
max-width: none;
|
|
margin: 0;
|
|
}
|
|
|
|
/* On the type screen the wireframe's bar glyphs become micro-copy, so the
|
|
pair is judged on real glyphs in every slot: nav, buttons, proof row,
|
|
cards, footer. Each element keeps the color slot its bar used, moved
|
|
from background to ink; sizes come from the ladder above.
|
|
|
|
Overflow stays visible: a bar had to be kept inside its box, but a line of
|
|
text hangs its descenders below the baseline, and the trimmed box ends at
|
|
the baseline. Hiding the overflow would cut the tail off every g and p.
|
|
|
|
The nowrap below is the default for a one-line label, and the proof and
|
|
card labels opt back out of it further down. Every selector in this list
|
|
is one class and one type, so those two rules win on source order. Adding
|
|
a more specific one here (a descendant pair, say) silently raises the whole
|
|
:is() above them and puts the clipping back. */
|
|
.picker-preview-type :is(
|
|
.ps-nav-bars i,
|
|
.ps-proof-item span,
|
|
.ps-editorial-copy > em,
|
|
.ps-gallery-item b,
|
|
.ps-footer-links i,
|
|
.ps-footer-mark,
|
|
.ps-phone-footer i
|
|
) {
|
|
width: auto;
|
|
height: auto;
|
|
overflow: visible;
|
|
background: none;
|
|
border-radius: 0;
|
|
font-family: var(--pt-body);
|
|
font-size: var(--pt-micro);
|
|
font-style: normal;
|
|
font-weight: 500;
|
|
line-height: 1.4;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.picker-preview-type .ps-nav-bars i,
|
|
.picker-preview-type .ps-footer-links i,
|
|
.picker-preview-type .ps-footer-mark,
|
|
.picker-preview-type .ps-phone-footer i {
|
|
color: var(--pvs-bars);
|
|
}
|
|
|
|
/* Same construction as the nav: one baseline across the row, the row itself
|
|
centered in the band, both edges on the shared gutter. */
|
|
.picker-preview-type .ps-footer {
|
|
display: grid;
|
|
grid-template-columns: auto auto;
|
|
justify-content: space-between;
|
|
align-content: center;
|
|
align-items: baseline;
|
|
padding-inline: var(--ps-gutter);
|
|
}
|
|
|
|
.picker-preview-type .ps-footer-links {
|
|
align-items: baseline;
|
|
gap: var(--ps-gap-md);
|
|
}
|
|
|
|
/* The mark, the links, and the button are three sizes of the same row, so
|
|
they hang from one baseline instead of being centered against each other;
|
|
the row as a whole is what centers in the band. The button is a shape
|
|
rather than a line of running text, so it centers on the row. */
|
|
.picker-preview-type .ps-nav {
|
|
gap: var(--ps-gap-lg);
|
|
align-content: center;
|
|
align-items: baseline;
|
|
padding-inline: var(--ps-gutter);
|
|
}
|
|
|
|
.picker-preview-type .ps-nav-bars {
|
|
justify-content: flex-start;
|
|
align-items: baseline;
|
|
gap: var(--ps-gap-md);
|
|
}
|
|
|
|
.picker-preview-type .ps-nav-action {
|
|
align-self: center;
|
|
}
|
|
|
|
/* The lettermark takes the heading face: a logo is display type. The circle's
|
|
square ratio has to go with its fill, or the mark keeps a box taller than
|
|
its glyphs and rides above the links it should sit beside. */
|
|
.picker-preview-type .ps-brand-block {
|
|
width: auto;
|
|
height: auto;
|
|
aspect-ratio: auto;
|
|
background: none;
|
|
border-radius: 0;
|
|
color: var(--pvs-mark-text);
|
|
font-family: var(--pt-heading);
|
|
font-size: var(--pt-title);
|
|
font-weight: var(--pt-heading-weight);
|
|
letter-spacing: 0.04em;
|
|
line-height: 1;
|
|
}
|
|
|
|
/* Buttons keep their fills; a filled label takes the ink of the fill it sits
|
|
on, which is a different color per strategy: Drenched paints the button in
|
|
the page's own ink, and Full palette hands the nav button to the tertiary.
|
|
The label is centered by the box being cut to the glyphs, so this stays a
|
|
block: a grid container has no line box of its own to trim. The filled
|
|
button carries a transparent border so it resolves to the same height as
|
|
the outlined one sitting next to it. */
|
|
.picker-preview-type .ps-nav-action,
|
|
.picker-preview-type .ps-actions i {
|
|
width: auto;
|
|
height: auto;
|
|
display: block;
|
|
padding: 0.7em 1.3em;
|
|
border: 1px solid transparent;
|
|
color: var(--pvs-cta-text);
|
|
font-family: var(--pt-body);
|
|
font-size: var(--pt-micro);
|
|
font-weight: 600;
|
|
line-height: 1;
|
|
text-align: center;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* The nav button is the one fill a strategy can hand to the tertiary, so its
|
|
label reads that fill's reading ink and not the hero button's. */
|
|
.picker-preview-type .ps-nav-action {
|
|
color: var(--pvs-signal-text);
|
|
}
|
|
|
|
.picker-preview-type .ps-actions {
|
|
align-items: center;
|
|
gap: var(--ps-gap-sm);
|
|
}
|
|
|
|
/* The ghost button sits on the ground, so ground-colored ink would erase it. */
|
|
.picker-preview-type .ps-actions i:last-child {
|
|
color: var(--pvs-title);
|
|
}
|
|
|
|
/* The band's rules become its own borders, so they run the full width of the
|
|
frame and meet the nav's bottom edge and the footer's top edge instead of
|
|
stopping short of the right margin. */
|
|
.picker-preview-type .ps-proof {
|
|
grid-template-columns:
|
|
minmax(0, 1fr) auto
|
|
minmax(0, 1fr) auto
|
|
minmax(0, 1fr) auto
|
|
minmax(0, 1fr);
|
|
padding: var(--ps-band-y) var(--ps-gutter);
|
|
border-block: 1px solid var(--pvs-rule);
|
|
}
|
|
|
|
.picker-preview-type .ps-proof::before,
|
|
.picker-preview-type .ps-proof::after {
|
|
content: none;
|
|
}
|
|
|
|
/* A share of the cell put a different gap in every column. */
|
|
.picker-preview-type .ps-proof-item {
|
|
gap: var(--ps-gap-sm);
|
|
}
|
|
|
|
.picker-preview-type .ps-proof-item i {
|
|
width: calc(var(--pt-base) * 0.55);
|
|
}
|
|
|
|
.picker-preview-type .ps-proof-item + .ps-proof-item {
|
|
border-left: 0;
|
|
}
|
|
|
|
.picker-preview-type .ps-proof-divider {
|
|
width: 1px;
|
|
align-self: stretch;
|
|
background-color: var(--pvs-rule);
|
|
}
|
|
|
|
.picker-preview-type .ps-phone-body .ps-proof {
|
|
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
|
|
}
|
|
|
|
.picker-preview-type .ps-proof-item:nth-of-type(2) i {
|
|
background-color: var(--pvs-accent-b);
|
|
}
|
|
|
|
.picker-preview-type .ps-proof-item:nth-of-type(3) i {
|
|
background-color: var(--pvs-accent-c);
|
|
}
|
|
|
|
.picker-preview-type .ps-proof-item:nth-of-type(4) i {
|
|
background-color: var(--pvs-accent-d);
|
|
}
|
|
|
|
/* A wide display face wraps the label rather than losing the end of a word to
|
|
the cell edge with no sign it went missing. */
|
|
.picker-preview-type .ps-proof-item span {
|
|
min-width: 0;
|
|
color: var(--pvs-bars);
|
|
white-space: normal;
|
|
}
|
|
|
|
/* Wraps rather than truncates: a section title is the product's own words and
|
|
its length is not ours to guarantee. */
|
|
.picker-preview-type .ps-editorial-copy strong {
|
|
width: auto;
|
|
height: auto;
|
|
overflow: visible;
|
|
background: none;
|
|
color: var(--pvs-title);
|
|
font-family: var(--pt-heading);
|
|
font-size: var(--pt-title);
|
|
font-weight: var(--pt-heading-weight);
|
|
line-height: 1.25;
|
|
white-space: normal;
|
|
text-wrap: balance;
|
|
}
|
|
|
|
.picker-preview-type .ps-editorial-copy > em {
|
|
width: auto;
|
|
height: auto;
|
|
margin-top: var(--ps-gap-xs);
|
|
background: none;
|
|
color: var(--pvs-accent-text);
|
|
}
|
|
|
|
.picker-preview-type .ps-gallery-item span {
|
|
gap: var(--ps-gap-xs);
|
|
}
|
|
|
|
/* Card labels wrap instead of truncating: a two-line label still reads, and a
|
|
clipped word is exactly the kind of flaw this preview exists to rule out. */
|
|
.picker-preview-type .ps-gallery-item b {
|
|
overflow: visible;
|
|
color: var(--pvs-bars);
|
|
white-space: normal;
|
|
}
|
|
|
|
.picker-preview-type .ps-gallery-item b:first-child {
|
|
color: var(--pvs-title);
|
|
}
|
|
|
|
.picker-palette-hint {
|
|
min-height: 3.2em;
|
|
padding-inline-end: 48px;
|
|
color: var(--ks-text-muted);
|
|
font-size: 0.84rem;
|
|
line-height: 1.6;
|
|
opacity: 1;
|
|
transition: opacity 180ms var(--ks-ease);
|
|
}
|
|
|
|
.picker-palette-hint.is-changing {
|
|
opacity: 0;
|
|
}
|
|
|
|
.picker-select {
|
|
width: auto;
|
|
}
|
|
|
|
.picker-placeholder {
|
|
display: grid;
|
|
justify-items: start;
|
|
gap: 28px;
|
|
color: var(--ks-text-muted);
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.picker-hero-art,
|
|
.picker-progress,
|
|
.picker-progress span,
|
|
.picker-card,
|
|
.picker-ring,
|
|
.picker-ring::after,
|
|
.picker-ring-guide,
|
|
.picker-icon-button,
|
|
.picker-band-tools,
|
|
.picker-tip::after,
|
|
.picker-palette-hint,
|
|
.picker-strategy-option,
|
|
.picker-artboard:not(.picker-preview-type) :is(
|
|
.ps-desktop,
|
|
.ps-phone,
|
|
.ps-nav,
|
|
.ps-proof,
|
|
.ps-proof-item,
|
|
.ps-footer,
|
|
.ps-phone-top,
|
|
.ps-phone-footer,
|
|
.ps-brand-block,
|
|
.ps-nav-bars i,
|
|
.ps-nav-action,
|
|
.ps-eyebrow,
|
|
.ps-headline i,
|
|
.ps-copy i,
|
|
.ps-actions i,
|
|
.ps-image,
|
|
.ps-proof-item i,
|
|
.ps-proof-item span,
|
|
.ps-editorial-copy strong,
|
|
.ps-editorial-copy span i,
|
|
.ps-editorial-copy > em,
|
|
.ps-gallery-item > i,
|
|
.ps-gallery-item b,
|
|
.ps-footer-links i,
|
|
.ps-footer-mark,
|
|
.ps-phone-footer i
|
|
),
|
|
.picker-preview-type :is(
|
|
.ps-desktop,
|
|
.ps-phone,
|
|
.ps-nav,
|
|
.ps-proof,
|
|
.ps-proof-item,
|
|
.ps-footer,
|
|
.ps-phone-top,
|
|
.ps-phone-footer,
|
|
.ps-brand-block,
|
|
.ps-nav-bars i,
|
|
.ps-nav-action,
|
|
.ps-eyebrow,
|
|
.ps-headline i,
|
|
.ps-copy i,
|
|
.ps-actions i,
|
|
.ps-image,
|
|
.ps-proof-item i,
|
|
.ps-proof-item span,
|
|
.ps-editorial-copy strong,
|
|
.ps-editorial-copy > em,
|
|
.ps-gallery-item > i,
|
|
.ps-gallery-item b,
|
|
.ps-footer-links i,
|
|
.ps-footer-mark,
|
|
.ps-phone-footer i,
|
|
.pt-headline,
|
|
.pt-body,
|
|
.pt-section-body
|
|
) {
|
|
transition: none;
|
|
}
|
|
|
|
.picker-card[data-exit] {
|
|
animation: none;
|
|
}
|
|
|
|
.picker-type-option[data-hoisted],
|
|
.picker-type-option[data-hoisted]::before {
|
|
animation: none;
|
|
}
|
|
}
|
|
|
|
.picker-width-gate {
|
|
display: none;
|
|
}
|
|
|
|
@media (max-width: 1199px) {
|
|
.picker-width-gate {
|
|
position: fixed;
|
|
inset: 0;
|
|
z-index: 10;
|
|
display: grid;
|
|
place-items: center;
|
|
padding: 40px;
|
|
background: var(--ks-lacquer);
|
|
color: var(--ks-text);
|
|
text-align: center;
|
|
}
|
|
|
|
.picker-width-gate > div {
|
|
max-width: 560px;
|
|
display: grid;
|
|
gap: 12px;
|
|
justify-items: center;
|
|
}
|
|
|
|
.picker-width-gate h2 {
|
|
color: var(--ks-champagne);
|
|
font-family: var(--ks-font-display);
|
|
font-size: var(--ks-type-headline-size);
|
|
font-weight: var(--ks-type-headline-weight);
|
|
line-height: var(--ks-type-headline-line);
|
|
}
|
|
|
|
.picker-width-gate p {
|
|
color: var(--ks-text-muted);
|
|
}
|
|
|
|
.picker-copy-link {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 14px;
|
|
width: min(100%, 480px);
|
|
min-width: 0;
|
|
margin-top: 14px;
|
|
padding: 13px 12px 13px 18px;
|
|
background: var(--ks-lacquer-raised);
|
|
border: 1px solid var(--ks-rule);
|
|
border-radius: 2px;
|
|
text-align: left;
|
|
}
|
|
|
|
.picker-copy-link code {
|
|
flex: 1 1 auto;
|
|
min-width: 18ch;
|
|
overflow-x: auto;
|
|
color: var(--ks-patina);
|
|
font-family: var(--ks-mono);
|
|
font-size: var(--ks-type-body-size);
|
|
letter-spacing: 0;
|
|
white-space: nowrap;
|
|
background: transparent;
|
|
border: 0;
|
|
padding: 0;
|
|
user-select: all;
|
|
}
|
|
|
|
.picker-copy-link-button {
|
|
position: relative;
|
|
flex: none;
|
|
display: inline-grid;
|
|
place-items: center;
|
|
width: 40px;
|
|
height: 40px;
|
|
padding: 0;
|
|
border: 1px solid var(--ks-rule);
|
|
border-radius: 2px;
|
|
background: transparent;
|
|
color: var(--ks-patina);
|
|
cursor: pointer;
|
|
transition:
|
|
background 180ms var(--ks-ease),
|
|
border-color 180ms var(--ks-ease),
|
|
color 180ms var(--ks-ease),
|
|
transform 120ms var(--ks-ease);
|
|
}
|
|
|
|
.picker-copy-link-button:hover,
|
|
.picker-copy-link-button.is-copied {
|
|
border-color: currentColor;
|
|
background: var(--ks-lacquer);
|
|
}
|
|
|
|
.picker-copy-link-button:active {
|
|
transform: scale(0.96);
|
|
}
|
|
|
|
.picker-copy-link-button:focus-visible {
|
|
outline: 2px solid var(--ks-patina);
|
|
outline-offset: 3px;
|
|
}
|
|
|
|
.picker-copy-link-icon,
|
|
.picker-copy-link-check {
|
|
grid-area: 1 / 1;
|
|
transition:
|
|
opacity 140ms var(--ks-ease),
|
|
transform 180ms var(--ks-ease);
|
|
}
|
|
|
|
.picker-copy-link-icon {
|
|
opacity: 1;
|
|
transform: scale(1);
|
|
}
|
|
|
|
.picker-copy-link-check {
|
|
opacity: 0;
|
|
transform: scale(0.86);
|
|
}
|
|
|
|
.picker-copy-link-button.is-copied .picker-copy-link-icon {
|
|
opacity: 0;
|
|
transform: scale(0.82);
|
|
}
|
|
|
|
.picker-copy-link-button.is-copied .picker-copy-link-check {
|
|
opacity: 1;
|
|
transform: scale(1);
|
|
}
|
|
|
|
.picker-copy-link-button span {
|
|
position: absolute;
|
|
width: 1px;
|
|
height: 1px;
|
|
overflow: hidden;
|
|
clip: rect(0 0 0 0);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.picker-copy-status {
|
|
min-height: 1.5em;
|
|
font-size: var(--ks-type-mono-size);
|
|
}
|
|
|
|
@media (max-width: 520px) {
|
|
.picker-width-gate {
|
|
padding-inline: 24px;
|
|
}
|
|
|
|
.picker-copy-link {
|
|
gap: 10px;
|
|
padding-left: 14px;
|
|
}
|
|
|
|
.picker-copy-link code {
|
|
overflow-x: visible;
|
|
overflow-wrap: anywhere;
|
|
white-space: normal;
|
|
}
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.picker-copy-link-button,
|
|
.picker-copy-link-icon,
|
|
.picker-copy-link-check {
|
|
transition: none;
|
|
}
|
|
}
|
|
}
|