mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-12 06:06:37 +03:00
Let questionnaire runs finish early or jump into the seven remaining questions from a summary card grid, with hub navigation and per-answer summaries. Co-authored-by: Cursor <cursoragent@cursor.com>
7696 lines
227 KiB
CSS
7696 lines
227 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;
|
|
}
|
|
|
|
/* Three screens judge their answer on the same drawing, so what the transition
|
|
carries is one component in three sizes: it grows out of the surface tile
|
|
into the palette screen's panel, and again onto the strategy stage, rather
|
|
than dissolving into a different page at each step. The hidden surfaces are
|
|
display: none and so are not rendered, which is what keeps the name unique
|
|
while every chosen surface stays mounted.
|
|
|
|
On 01b the frame is named rather than the component inside it. The tile draws
|
|
that component at its canonical width under a scale transform, and a group
|
|
animating its own size as well as that transform counts the reduction twice.
|
|
The frame is the drawing's real box at tile scale and carries the same 16 / 7
|
|
the other two do, so the pair interpolates without distortion. Only the
|
|
leading tile is marked, which is both the drawing screen 02 goes on to show
|
|
and what keeps the name on one element. */
|
|
.picker-screen[data-screen="01b"] .picker-mode-tile[data-lead] .picker-mode-preview,
|
|
.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.
|
|
|
|
Eleven is the ceiling rather than the count. A question no chosen surface
|
|
takes is not a step, so the navigation writes the run's own count here and
|
|
the ticks past it leave the grid: the row still measures the same distance,
|
|
divided between the questions this run is actually asked. */
|
|
.picker-progress-track {
|
|
width: 232px;
|
|
display: grid;
|
|
grid-template-columns: repeat(var(--pk-steps, 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-track i[data-off] {
|
|
display: none;
|
|
}
|
|
|
|
.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;
|
|
/* A share of the frame rather than a fixed channel: on screen 03 the two
|
|
cards are drawn near twice the size they are on 01b, and a gutter that
|
|
stays at 16px there reads as a seam between them. */
|
|
gap: 3.229163%;
|
|
}
|
|
|
|
/* 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.
|
|
|
|
Both cards are inline-size containers, so the chrome each one carries is cut
|
|
from the card it sits on. A nav band is a drawn object rather than page
|
|
length, which is why its own vertical padding is counted in `--pv-x` with
|
|
the rest: a taller frame is a longer page behind the same window, not a
|
|
thicker window. Every count below is a canonical pixel over the card's own
|
|
height, 214.78 on both, counted the way the four bodies are counted against
|
|
theirs. The two corner radii are the exception and stay in pixels: a
|
|
container measures for what is inside it, so a card cannot spend its own
|
|
`--pv-x` on itself. */
|
|
/* A line of type is drawn as a bar, and how heavy that bar reads is not settled
|
|
by its authored count: the phone spends nearly four times the share of its
|
|
card on the same number, which is why the two cards side by side can be cut
|
|
from one drawing and still disagree about how present the text is. The factor
|
|
is the room to say that, per card and without moving anything else in the
|
|
drawing. It stays 1 wherever a card was signed off, so a card only carries a
|
|
different weight where something turned it on by name. */
|
|
.pv-desktop,
|
|
.pv-phone {
|
|
--pv-text: 1;
|
|
container-type: inline-size;
|
|
min-width: 0;
|
|
min-height: 0;
|
|
overflow: hidden;
|
|
background: var(--pv-neutral);
|
|
border: 1px solid var(--ks-rule);
|
|
}
|
|
|
|
.pv-desktop {
|
|
/* 1 / 1.745162, this card's width over its height at the component's authored
|
|
16 / 7, measured on the built picker. */
|
|
--pv-x: 0.573013cqw;
|
|
display: grid;
|
|
grid-template-rows: auto 1fr;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.pv-phone {
|
|
/* 1 / 0.468718: the portrait card is narrower than it is tall, so the same
|
|
count spends a larger share of this card's width than of the desktop's. */
|
|
--pv-x: 2.133479cqw;
|
|
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: calc(4.6559 * var(--pv-x));
|
|
align-items: center;
|
|
padding: calc(4.6559 * var(--pv-x)) calc(5.58708 * var(--pv-x));
|
|
}
|
|
|
|
.pv-logo {
|
|
width: calc(3.72472 * var(--pv-x));
|
|
aspect-ratio: 1;
|
|
background: var(--pv-primary);
|
|
border-radius: 50%;
|
|
}
|
|
|
|
.pv-nav-bars {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: calc(3.72472 * var(--pv-x));
|
|
}
|
|
|
|
.pv-nav-bars i {
|
|
display: block;
|
|
width: calc(8.38062 * var(--pv-x));
|
|
height: calc(1.39677 * var(--pv-x));
|
|
background: var(--pv-bone);
|
|
border-radius: 2px;
|
|
}
|
|
|
|
.pv-pill {
|
|
width: calc(14.89888 * var(--pv-x));
|
|
height: calc(5.58708 * var(--pv-x));
|
|
background: var(--pv-primary);
|
|
border-radius: 2px;
|
|
}
|
|
|
|
/* The hero is a size container so the page it draws scales with the card rather
|
|
than sitting at one drawn size inside it. Its counts are canonical pixels
|
|
divided by the box's own height, 164.78. Down the page they are spent in
|
|
`cqh`, so a taller card is a longer hero rather than a magnified one; across
|
|
it they are spent in `--pv-x` against the width, as the ops variant below
|
|
sets out, so a bar keeps its thickness when the frame changes shape.
|
|
Its own padding is a percentage, since a container cannot measure itself. */
|
|
.pv-hero {
|
|
container-type: size;
|
|
min-height: 0;
|
|
display: grid;
|
|
grid-template-columns: minmax(0, 1.05fr) minmax(0, 0.95fr);
|
|
gap: 8%;
|
|
align-items: center;
|
|
padding: 1.06729% 8% 3.735179%;
|
|
/* 1 / 1.910772, this box's width over its height. */
|
|
--pv-x: 0.523349cqw;
|
|
}
|
|
|
|
.pv-hero-copy {
|
|
display: grid;
|
|
gap: 4.85493cqh;
|
|
}
|
|
|
|
.pv-eyebrow {
|
|
width: 22%;
|
|
height: calc(2.42746 * var(--pv-text) * var(--pv-x));
|
|
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: 4.24806cqh;
|
|
margin-block: 1.21374cqh 1.8206cqh;
|
|
}
|
|
|
|
.pv-headline i {
|
|
display: block;
|
|
width: 88%;
|
|
height: calc(4.85492 * var(--pv-text) * var(--pv-x));
|
|
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: calc(3.03433 * var(--pv-text) * var(--pv-x));
|
|
background: var(--pv-bone);
|
|
border-radius: 2px;
|
|
}
|
|
|
|
.pv-actions {
|
|
display: flex;
|
|
gap: calc(4.24806 * var(--pv-x));
|
|
margin-top: 6.06866cqh;
|
|
}
|
|
|
|
/* Buttons are objects on the page rather than page rhythm, so they keep their
|
|
drawn size and the room above them takes the extra height instead. */
|
|
.pv-actions i {
|
|
width: calc(23.06088 * var(--pv-x));
|
|
height: calc(7.88925 * var(--pv-x));
|
|
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: calc(5.46179 * var(--pv-x));
|
|
right: calc(5.46179 * var(--pv-x));
|
|
width: calc(4.24806 * var(--pv-x));
|
|
aspect-ratio: 1;
|
|
background: var(--pv-tertiary);
|
|
border-radius: 50%;
|
|
}
|
|
|
|
.pv-avatar {
|
|
width: calc(4.19031 * var(--pv-x));
|
|
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: calc(4.19031 * var(--pv-x)) calc(4.6559 * var(--pv-x));
|
|
background: var(--pv-surface-2);
|
|
border-bottom: 1px solid var(--ks-rule);
|
|
}
|
|
|
|
/* A size container counted the way the hero is: canonical pixels over this
|
|
padded box's own height, 145.78, spent in `cqh` down the page and in
|
|
`--pv-x` across it. The padding itself is a percentage of the phone's width,
|
|
since a container cannot measure itself. */
|
|
.pv-phone-body {
|
|
container-type: size;
|
|
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;
|
|
/* A percentage of this box's own height rather than a count in `cqh`: a
|
|
container unit on the container's own property is measured against the box
|
|
outside it, which here is the phone card, and the row gap would be cut from
|
|
the wrong page. */
|
|
row-gap: 4.802058%;
|
|
padding: 8.940432% 9.933758% 9.933758%;
|
|
/* 1 / 0.553376, this box's width over its height. */
|
|
--pv-x: 1.807089cqw;
|
|
}
|
|
|
|
/* The desktop plate's mark is counted against a taller box than this one, so
|
|
the phone restates it. */
|
|
.pv-phone-body .pv-image::after {
|
|
top: calc(6.17364 * var(--pv-x));
|
|
right: calc(6.17364 * var(--pv-x));
|
|
width: calc(4.80172 * var(--pv-x));
|
|
}
|
|
|
|
/* 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%;
|
|
}
|
|
|
|
/* A block is a drawn object rather than a share of the page, so everything
|
|
inside it is counted across the box: the row it sits in is `auto`, and a
|
|
block that took its rhythm from the card's height would grow into the plate
|
|
above it every time the frame got taller. */
|
|
.pv-mini-card {
|
|
display: grid;
|
|
gap: calc(3.4298 * var(--pv-x));
|
|
padding: calc(5.48768 * var(--pv-x)) calc(6.17364 * var(--pv-x));
|
|
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: calc(2.05788 * var(--pv-x));
|
|
background: var(--pv-bone);
|
|
border-radius: 2px;
|
|
}
|
|
|
|
.pv-mini-card span {
|
|
width: 62%;
|
|
height: calc(4.11576 * var(--pv-x));
|
|
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: calc(3.72472 * var(--pv-x)) 0;
|
|
border-top: 1px solid var(--ks-rule);
|
|
}
|
|
|
|
.pv-tabbar i {
|
|
width: calc(2.32795 * var(--pv-x));
|
|
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 measure below is a canonical pixel
|
|
read off the reference illustration, divided by the body height it sits in:
|
|
181.78 on the desktop, 133.77 inside the phone's padded box. Vertical ones
|
|
spend that count in `cqh`, so a taller card is a taller page. Horizontal ones
|
|
spend it in `--pv-x`, so the same count holds when the frame's proportions
|
|
move: the width of a column and the thickness of a bar are not the frame's
|
|
height to give away. The chrome all four variants share (nav, phone top bar,
|
|
tab bar) is counted on the cards themselves, above. */
|
|
.po-body {
|
|
container-type: size;
|
|
min-height: 0;
|
|
display: grid;
|
|
/* Percentages, not a container unit: a container cannot measure itself, and
|
|
the three panels are horizontal dimensions anyway. */
|
|
grid-template-columns: 18.68% 51.84% minmax(0, 1fr);
|
|
/* One canonical measure counted against this body's width rather than its
|
|
height: 1 / 2.061974, this box's width over its height at the component's
|
|
authored 16 / 7, measured on the built picker. Every horizontal measure is
|
|
then a fixed fraction of the width it has to fit inside, so no column can
|
|
be squeezed out and no row can reach past the card no matter what the frame
|
|
does with its height. Each body is a different box, so each restates it.
|
|
|
|
The count is what the reference illustration was measured in, so a value
|
|
stays comparable to its vertical neighbours: `13.23` is the same 13.23 in
|
|
both axes. */
|
|
--pv-x: 0.484972cqw;
|
|
}
|
|
|
|
.po-side {
|
|
display: grid;
|
|
align-content: start;
|
|
gap: 5.25cqh;
|
|
padding: 6.38cqh calc(2.86 * var(--pv-x)) 0 calc(2.22 * var(--pv-x));
|
|
border-right: 1px solid var(--po-rule);
|
|
}
|
|
|
|
.po-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: calc(3.39 * var(--pv-x));
|
|
padding-left: calc(2.84 * var(--pv-x));
|
|
}
|
|
|
|
.po-dot {
|
|
flex: none;
|
|
width: calc(4.07 * var(--pv-x));
|
|
aspect-ratio: 1;
|
|
background: var(--po-ink);
|
|
border-radius: 50%;
|
|
}
|
|
|
|
.po-line {
|
|
width: calc(13.23 * var(--pv-x));
|
|
height: calc(1.53 * var(--pv-text) * var(--pv-x));
|
|
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 calc(4.68 * var(--pv-x)) 0 calc(5.23 * var(--pv-x));
|
|
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: calc(7.23 * var(--pv-x));
|
|
height: 33.26cqh;
|
|
margin-inline: calc(9.52 * var(--pv-x)) calc(14.63 * var(--pv-x));
|
|
/* The bottom is the tick zone rather than page rhythm, so it is measured with
|
|
the ticks it holds. */
|
|
padding: 0 calc(4.29 * var(--pv-x)) calc(2.56 * var(--pv-x)) calc(4.46 * var(--pv-x));
|
|
}
|
|
|
|
.po-chart::before {
|
|
content: "";
|
|
position: absolute;
|
|
right: 0;
|
|
bottom: calc(2.56 * var(--pv-x));
|
|
left: 0;
|
|
height: 1px;
|
|
background: var(--po-ink-soft);
|
|
}
|
|
|
|
.po-chart i {
|
|
flex: none;
|
|
width: calc(6.92 * var(--pv-x));
|
|
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: calc(1.29 * var(--pv-x));
|
|
margin-left: calc(3.19 * var(--pv-x));
|
|
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: calc(4.9 * var(--pv-x));
|
|
height: 11.61cqh;
|
|
padding-left: calc(3.41 * var(--pv-x));
|
|
}
|
|
|
|
.po-row .po-dot {
|
|
width: calc(3.85 * var(--pv-x));
|
|
}
|
|
|
|
/* The column widths differ per cell, so they arrive on the element. Only the
|
|
count does: a container unit reached through a second custom property is
|
|
dropped when it arrives in an inline style, which left every cell here at
|
|
auto width and so at nothing, since a cell has no content to be wide for.
|
|
Spending the unit in the sheet keeps the drawn width and leaves the markup
|
|
carrying the same counts the rest of the drawing is authored in. */
|
|
.po-cell {
|
|
flex: none;
|
|
width: calc(var(--w) * var(--pv-x));
|
|
height: calc(1.45 * var(--pv-text) * var(--pv-x));
|
|
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: calc(12.71 * var(--pv-x));
|
|
}
|
|
|
|
.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 calc(6.71 * var(--pv-x)) 0 calc(7.21 * var(--pv-x));
|
|
}
|
|
|
|
.po-heading {
|
|
width: calc(17.38 * var(--pv-x));
|
|
height: calc(1.62 * var(--pv-text) * var(--pv-x));
|
|
background: var(--pv-primary);
|
|
border-radius: 2px;
|
|
}
|
|
|
|
/* The controls in this panel are drawn objects rather than page rhythm, so the
|
|
field, the switches and the toggles keep their measured size while the
|
|
margins between them are what a taller card spends its extra height on. */
|
|
.po-field {
|
|
display: flex;
|
|
align-items: center;
|
|
height: calc(8.75 * var(--pv-x));
|
|
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: calc(2 * var(--pv-x));
|
|
aspect-ratio: 1;
|
|
margin: calc(1.86 * var(--pv-x)) calc(3.14 * var(--pv-x)) 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: calc(5.34 * var(--pv-x));
|
|
margin-top: 8.39cqh;
|
|
}
|
|
|
|
.po-switch + .po-switch {
|
|
margin-top: 6.19cqh;
|
|
}
|
|
|
|
.po-label {
|
|
flex: none;
|
|
width: calc(17.22 * var(--pv-x));
|
|
height: calc(1.62 * var(--pv-text) * var(--pv-x));
|
|
background: var(--po-ink);
|
|
border-radius: 2px;
|
|
}
|
|
|
|
.po-toggle {
|
|
position: relative;
|
|
flex: none;
|
|
width: calc(10.29 * var(--pv-x));
|
|
height: calc(5.34 * var(--pv-x));
|
|
margin-left: auto;
|
|
background: var(--po-ink);
|
|
border-radius: 999px;
|
|
}
|
|
|
|
.po-toggle--on {
|
|
background: var(--pv-primary);
|
|
}
|
|
|
|
.po-toggle::after {
|
|
content: "";
|
|
position: absolute;
|
|
top: calc(0.66 * var(--pv-x));
|
|
left: calc(0.66 * var(--pv-x));
|
|
width: calc(4.02 * var(--pv-x));
|
|
aspect-ratio: 1;
|
|
background: var(--pv-neutral);
|
|
border-radius: 50%;
|
|
}
|
|
|
|
.po-toggle--on::after {
|
|
right: calc(0.66 * var(--pv-x));
|
|
left: auto;
|
|
}
|
|
|
|
/* The phone is the same screen narrowed: one chart, the same field, two setting
|
|
rows. Its padded box is 133.77 tall, so every shared measure above is
|
|
restated here against that number. The padding itself is a percentage of the
|
|
phone's width, since a container cannot measure itself.
|
|
|
|
The single column is stated rather than left implicit. The chart's five bars
|
|
and four gaps come to the full width of this box, and an implicit `auto`
|
|
column takes its floor from its widest item: it opens past the card and
|
|
carries the field and the setting rows out through the right edge with it. */
|
|
.po-phone-body {
|
|
container-type: size;
|
|
min-height: 0;
|
|
display: grid;
|
|
grid-template-columns: minmax(0, 1fr);
|
|
align-content: start;
|
|
padding: 16.19% 4.57% 14.63% 4.27%;
|
|
--pv-x: 1.457438cqw;
|
|
}
|
|
|
|
.po-phone-body .po-chart {
|
|
gap: calc(6.79 * var(--pv-x));
|
|
height: 36.93cqh;
|
|
margin-inline: calc(0.67 * var(--pv-x)) calc(2.92 * var(--pv-x));
|
|
padding: 0 calc(3.51 * var(--pv-x)) calc(3.36 * var(--pv-x)) calc(3.36 * var(--pv-x));
|
|
}
|
|
|
|
.po-phone-body .po-chart::before {
|
|
bottom: calc(3.36 * var(--pv-x));
|
|
}
|
|
|
|
.po-phone-body .po-chart i {
|
|
width: calc(6.21 * var(--pv-x));
|
|
}
|
|
|
|
.po-phone-body .po-chart i::after {
|
|
height: calc(1.94 * var(--pv-x));
|
|
margin-left: calc(2.73 * var(--pv-x));
|
|
}
|
|
|
|
.po-phone-body .po-field {
|
|
height: calc(11.81 * var(--pv-x));
|
|
margin-top: 10.47cqh;
|
|
}
|
|
|
|
.po-phone-body .po-chev {
|
|
width: calc(2.77 * var(--pv-x));
|
|
margin: calc(2.65 * var(--pv-x)) calc(5.03 * var(--pv-x)) 0 auto;
|
|
}
|
|
|
|
.po-prow {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: calc(4.49 * var(--pv-x));
|
|
height: 18.47cqh;
|
|
padding-left: calc(2.24 * var(--pv-x));
|
|
}
|
|
|
|
.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: calc(5.76 * var(--pv-x));
|
|
}
|
|
|
|
.po-phone-body .po-label {
|
|
width: calc(18.62 * var(--pv-x));
|
|
height: calc(2.17 * var(--pv-x));
|
|
}
|
|
|
|
.po-phone-body .po-toggle {
|
|
width: calc(12.64 * var(--pv-x));
|
|
height: calc(7.25 * var(--pv-x));
|
|
}
|
|
|
|
.po-phone-body .po-toggle::after {
|
|
top: calc(0.9 * var(--pv-x));
|
|
left: calc(0.9 * var(--pv-x));
|
|
width: calc(5.46 * var(--pv-x));
|
|
}
|
|
|
|
.po-phone-body .po-toggle--on::after {
|
|
right: calc(0.9 * var(--pv-x));
|
|
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: calc(2.99 * var(--pv-x));
|
|
margin: 0 calc(4.86 * var(--pv-x)) 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. Every measure below is a canonical pixel
|
|
divided by the body height it sits in: 181.78 on the desktop, 146.5 inside
|
|
the phone. Vertical ones spend that count in `cqh`, so a taller card is a
|
|
longer article rather than a magnified one, and horizontal ones spend it in
|
|
`--pv-x` against the width, as the ops variant above sets out. The chrome
|
|
around both bodies is counted on the cards themselves. */
|
|
.pd-body {
|
|
container-type: size;
|
|
min-height: 0;
|
|
display: grid;
|
|
/* A percentage, not a container unit: a container cannot measure itself, and
|
|
the rail is a horizontal dimension anyway. */
|
|
grid-template-columns: 23.75% minmax(0, 1fr);
|
|
--pv-x: 0.484972cqw;
|
|
}
|
|
|
|
.pd-side {
|
|
display: grid;
|
|
align-content: start;
|
|
gap: 8.69cqh;
|
|
padding: 9.24cqh 0 0 calc(7.15 * var(--pv-x));
|
|
border-right: 1px solid var(--pd-rule);
|
|
}
|
|
|
|
.pd-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: calc(3.03 * var(--pv-x));
|
|
}
|
|
|
|
.pd-item i {
|
|
width: calc(2.48 * var(--pv-x));
|
|
aspect-ratio: 1;
|
|
background: var(--pd-ink);
|
|
border-radius: 50%;
|
|
}
|
|
|
|
.pd-item span {
|
|
width: calc(19.36 * var(--pv-x));
|
|
height: calc(1.27 * var(--pv-text) * var(--pv-x));
|
|
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 calc(50.06 * var(--pv-x)) 0 calc(16.94 * var(--pv-x));
|
|
}
|
|
|
|
.pd-heading {
|
|
width: var(--w, 100%);
|
|
height: calc(2.53 * var(--pv-text) * var(--pv-x));
|
|
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: calc(1.1 * var(--pv-text) * var(--pv-x));
|
|
background: var(--pd-ink);
|
|
border-radius: 2px;
|
|
}
|
|
|
|
.pd-note {
|
|
display: flex;
|
|
gap: calc(3.41 * var(--pv-x));
|
|
height: 14.03cqh;
|
|
margin-top: 7.37cqh;
|
|
padding: 3.52cqh calc(4.4 * var(--pv-x)) 0 calc(5.17 * var(--pv-x));
|
|
background: var(--pv-surface-2);
|
|
border-left: calc(1.27 * var(--pv-x)) solid var(--pv-primary);
|
|
border-radius: 0 2px 2px 0;
|
|
}
|
|
|
|
.pd-note-dot {
|
|
flex: none;
|
|
align-self: start;
|
|
width: calc(2.64 * var(--pv-x));
|
|
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: calc(0.83 * var(--pv-text) * var(--pv-x));
|
|
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 measured against the
|
|
padded box, in `cqh` down the page and in `--pv-x` across it. The single
|
|
column is stated so a child that asks for more width than the box has cannot
|
|
open the column and carry its siblings out through the card edge. */
|
|
.pd-phone-body {
|
|
container-type: size;
|
|
min-height: 0;
|
|
display: grid;
|
|
grid-template-columns: minmax(0, 1fr);
|
|
align-content: start;
|
|
padding: 8.84% 11.42% 9.33% 10.23%;
|
|
--pv-x: 1.857cqw;
|
|
}
|
|
|
|
.pd-crumb {
|
|
width: calc(13.11 * var(--pv-x));
|
|
height: calc(1.77 * var(--pv-x));
|
|
margin-inline: auto;
|
|
background: var(--pd-ink);
|
|
border-radius: 2px;
|
|
}
|
|
|
|
.pd-phone-body .pd-heading {
|
|
height: calc(3.14 * var(--pv-x));
|
|
margin-top: 9.62cqh;
|
|
}
|
|
|
|
.pd-phone-body .pd-para {
|
|
gap: 4.1cqh;
|
|
}
|
|
|
|
.pd-phone-body .pd-para i {
|
|
height: calc(1.37 * var(--pv-x));
|
|
}
|
|
|
|
.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: calc(3.34 * var(--pv-x));
|
|
margin-top: 6.76cqh;
|
|
padding: 4.37cqh calc(1.37 * var(--pv-x)) 0 calc(3.14 * var(--pv-x));
|
|
border-left-width: calc(1.23 * var(--pv-x));
|
|
}
|
|
|
|
.pd-phone-body .pd-note-dot {
|
|
width: calc(3.34 * var(--pv-x));
|
|
}
|
|
|
|
.pd-phone-body .pd-note-lines {
|
|
gap: 2.8cqh;
|
|
}
|
|
|
|
.pd-phone-body .pd-note-lines i {
|
|
height: calc(1.37 * var(--pv-x));
|
|
}
|
|
|
|
/* ── 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 measure 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. Vertical ones spend that count in `cqh`, which is
|
|
what makes a taller card show taller plates rather than a zoomed index, and
|
|
horizontal ones spend it in `--pv-x` against the width, as the ops variant
|
|
above sets out. The chrome around both bodies is counted on the cards
|
|
themselves. */
|
|
.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. */
|
|
/* The mark is a fixed inset from the plate's own corner, so both insets are
|
|
measured with it rather than with the page. */
|
|
.picker-preview--gallery .pv-image::after {
|
|
top: calc(3.41 * var(--pv-x));
|
|
right: calc(3.15 * var(--pv-x));
|
|
width: calc(3.37 * var(--pv-x));
|
|
background: var(--pv-primary);
|
|
}
|
|
|
|
.pg-body {
|
|
container-type: size;
|
|
min-height: 0;
|
|
display: grid;
|
|
align-content: start;
|
|
--pv-x: 0.48764cqw;
|
|
}
|
|
|
|
/* 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.
|
|
The plate is the drawn column and the caption takes what is left, so a frame
|
|
that changes shape spends it on the plate's height and never on the caption's
|
|
width. */
|
|
.pg-row {
|
|
display: grid;
|
|
grid-template-columns: calc(73.39 * var(--pv-x)) minmax(0, 1fr);
|
|
column-gap: calc(10.55 * var(--pv-x));
|
|
height: 37.31cqh;
|
|
margin-top: 2.31cqh;
|
|
padding-inline: calc(31.34 * var(--pv-x)) calc(32.13 * var(--pv-x));
|
|
}
|
|
|
|
.pg-row--flip {
|
|
grid-template-columns: minmax(0, 1fr) calc(73.39 * var(--pv-x));
|
|
column-gap: calc(11.18 * var(--pv-x));
|
|
margin-top: 7.28cqh;
|
|
padding-inline: calc(34.97 * var(--pv-x)) calc(33.07 * var(--pv-x));
|
|
}
|
|
|
|
.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: calc(1.5 * var(--pv-text) * var(--pv-x));
|
|
background: var(--pg-ink);
|
|
border-radius: 2px;
|
|
}
|
|
|
|
.pg-cap i.pg-cap-title {
|
|
height: calc(2.01 * var(--pv-text) * var(--pv-x));
|
|
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. The rail's own height is the
|
|
arrows it holds rather than a share of the page, so a taller card gives its
|
|
room to the plates above and leaves this strip alone. */
|
|
.pg-rail {
|
|
display: flex;
|
|
align-items: center;
|
|
height: calc(3.25 * var(--pv-x));
|
|
margin-top: 4.95cqh;
|
|
padding-inline: calc(7.78 * var(--pv-x)) calc(8.24 * var(--pv-x));
|
|
}
|
|
|
|
/* 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: calc(2.3 * var(--pv-x));
|
|
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: calc(3.7 * var(--pv-x));
|
|
margin-left: calc(7.26 * var(--pv-x));
|
|
margin-right: auto;
|
|
}
|
|
|
|
/* Segment widths arrive as counts for the reason the table cells above do. */
|
|
.pg-track i {
|
|
flex: none;
|
|
width: calc(var(--w) * var(--pv-x));
|
|
height: calc(1.48 * var(--pv-x));
|
|
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 measured against the padded box,
|
|
in `cqh` down the page and in `--pv-x` across it, and the single column is
|
|
stated so no child can open it wider than the phone. */
|
|
.pg-phone-body {
|
|
container-type: size;
|
|
min-height: 0;
|
|
display: grid;
|
|
grid-template-columns: minmax(0, 1fr);
|
|
align-content: start;
|
|
padding: 5.65% 8.34% 10.49%;
|
|
--pv-x: 1.770721cqw;
|
|
}
|
|
|
|
.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 measure
|
|
has to be restated against it. */
|
|
.pg-phone-body .pv-image::after {
|
|
top: calc(3.02 * var(--pv-x));
|
|
right: calc(3.26 * var(--pv-x));
|
|
width: calc(3.4 * var(--pv-x));
|
|
}
|
|
|
|
.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: calc(1.52 * var(--pv-x));
|
|
}
|
|
|
|
/* 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: calc(1.52 * var(--pv-x));
|
|
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: calc(5.12149 * var(--pv-x));
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
/* Scoped to the copy block the two were drawn for. Both classes are also the
|
|
handles the per-surface machinery reads a row's name and its description
|
|
through, and the font rail names itself in the pair's own faces, so away from
|
|
this block they are hooks and carry no design of their own. */
|
|
.picker-strategy-copy > .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-copy > .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 and the buttons under its
|
|
frame, and the two pairs sit on the same two rows: the choices beside the
|
|
frame, then the palette beside the buttons. Both columns subgrid onto them, so
|
|
the frame's bottom edge is the options panel's bottom edge by construction
|
|
rather than by two sizing formulas agreeing. The row gap is the rail's own
|
|
former 14px, which now also sets the air between the frame and the CTA.
|
|
|
|
Everything this screen shows lives inside the grid, so what the grid may
|
|
spend is the window less whatever the block spends around it: the padding,
|
|
the question, and the gap under it. --pk-chrome is that sum, measured by
|
|
fitStrategyColumn() rather than counted here, because the question is one or
|
|
two lines at a size that tracks the window's width. The literal is the value
|
|
before the script has measured,
|
|
and it is the number this rule used to carry outright; at laptop heights it
|
|
was short enough that the screen ran past the fold.
|
|
|
|
--pk-column, which the later question screens still read, is measured for a
|
|
grid with the buttons underneath it and leaves this one 90px short of what it
|
|
now owns. */
|
|
.picker-screen[data-screen="03"] .picker-strategy-grid {
|
|
max-height: calc(100svh - var(--pk-chrome, 346px));
|
|
}
|
|
|
|
.picker-screen[data-screen="03"] .picker-strategy-grid > .picker-type-rail {
|
|
height: auto;
|
|
align-self: start;
|
|
grid-template-rows: auto auto auto;
|
|
}
|
|
|
|
.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 and the column it sits in. 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 tabs sit on the frame's top right corner rather than
|
|
in a row of their own, and the buttons sit under it, so the column is the
|
|
whole answer to the question rather than a preview with the commitment parked
|
|
somewhere else on the page.
|
|
|
|
The frame takes the height of the row the choices set instead of a ratio of
|
|
its own, so it is wider than tall here and squarer on a short viewport. Three
|
|
of the four drawings key their horizontal measures to their own width and so
|
|
hold either way. Persuade is still drawn in pixels and sits loose in a frame
|
|
this size.
|
|
============================================================ */
|
|
.picker-strategy-stage {
|
|
min-width: 0;
|
|
min-height: 0;
|
|
display: grid;
|
|
grid-template-rows: auto minmax(0, 1fr);
|
|
}
|
|
|
|
.picker-strategy-stage > .picker-preview {
|
|
/* Every chosen surface is mounted and all but one are display: none, so the
|
|
row is named rather than left to auto-placement: a frame where two are
|
|
briefly shown puts them on top of each other instead of stacking the stage
|
|
into a third row and taking the column past the choices' bottom edge. */
|
|
grid-row: 2;
|
|
/* The frame runs down to the bottom of the choices beside it, so it takes the
|
|
row's height rather than the 16 / 7 it carries everywhere else. */
|
|
aspect-ratio: auto;
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
/* The tiles and the palette screen show the drawing at a fraction of this
|
|
size, and this stage is the one place a visitor reads the desktop card as a
|
|
page rather than as a thumbnail. At that size the tool, the document and the
|
|
index draw their body text as hairlines beside their own phone card, which
|
|
spends nearly four times the share of its card on the same count. The step is
|
|
on the thickness only, so the page behind the window is unchanged: same
|
|
measure, same rhythm, same everything the counts were measured for.
|
|
|
|
The landing page is left at its authored weight. Its counts are the largest
|
|
of the four, and at this size they already read as type; a headline bar taken
|
|
any heavier stops being display type and becomes a slab. The nav is left out
|
|
for the same kind of reason: it is the chrome all four cards share, and a
|
|
visitor moving between tabs should not watch it change weight. */
|
|
.picker-strategy-stage > .picker-preview:is(.picker-preview--ops, .picker-preview--docs, .picker-preview--gallery) .pv-desktop {
|
|
--pv-text: 1.35;
|
|
}
|
|
|
|
/* 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.
|
|
|
|
In the flow, on the stage's first row, right-aligned over the frame. The row
|
|
is what reserves the strip's height, so nothing downstream has to guess at it
|
|
and the question above keeps the gap every screen shares.
|
|
|
|
On the edge rather than inside the frame. Every drawing puts its nav across
|
|
the frame's top and the strip is taller than that band, so sitting inside
|
|
covers the one place the primary shows up as chrome. Outside it the strip
|
|
also keeps its own ground: raised lacquer and a hairline are read against the
|
|
page, not against a drawing repainted in whatever palette and strategy were
|
|
chosen. The 1px pull is the join, landing the strip's bottom hairline on the
|
|
frame's top one, and the z-index is what keeps that edge visible, since the
|
|
drawings raise plates of their own that would otherwise paint over it. */
|
|
.picker-surface-tabs {
|
|
position: relative;
|
|
z-index: 1;
|
|
grid-row: 1;
|
|
justify-self: end;
|
|
margin-bottom: 16px;
|
|
display: flex;
|
|
gap: 3px;
|
|
padding: 5px;
|
|
background: var(--ks-lacquer-raised);
|
|
border: 1px solid var(--ks-rule);
|
|
border-radius: 2px;
|
|
}
|
|
|
|
.picker-surface-tabs[hidden] {
|
|
display: none;
|
|
}
|
|
|
|
/* One line: the dot and the surface this tab names. The picker's chrome
|
|
control, the same 0.8rem on the same tracking as `.picker-type-custom`, since
|
|
a strip docked on a frame's corner names which drawing is showing and should
|
|
not read as the page's loudest button. */
|
|
.picker-surface-tab {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 13px;
|
|
padding: 11px 15px;
|
|
color: var(--ks-text-muted);
|
|
background: transparent;
|
|
border: 0;
|
|
border-radius: 2px;
|
|
font-family: inherit;
|
|
font-size: 1rem;
|
|
letter-spacing: 0.04em;
|
|
line-height: 1.15;
|
|
white-space: nowrap;
|
|
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 {
|
|
flex: none;
|
|
width: 10px;
|
|
height: 10px;
|
|
border: 1px solid color-mix(in oklab, var(--ks-patina) 55%, transparent);
|
|
border-radius: 50%;
|
|
}
|
|
|
|
/* The open tab's dot, checked the way a radio is. The plate behind the tab
|
|
already says which one is open, but a row of hollow dots reads as a row of
|
|
unanswered questions and the plate is too quiet to argue with it. Kept above
|
|
the fill so the answered report still outranks this on the open tab. */
|
|
.picker-surface-tab[aria-pressed="true"] .picker-surface-dot {
|
|
border-color: var(--ks-patina);
|
|
background: radial-gradient(circle at center, var(--ks-patina) 2px, transparent 2px);
|
|
}
|
|
|
|
.picker-surface-tab[data-set="yes"] .picker-surface-dot {
|
|
background: var(--ks-patina);
|
|
border-color: var(--ks-patina);
|
|
}
|
|
|
|
/* An option 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 type boards under Restrained, which is the answer no strategy block
|
|
below rewrites. Left to the artboard's own defaults these slots sit on bone
|
|
and the near-neutral surface step, so the font screen drew a page the chosen
|
|
palette had never reached while the drawing on screen 03 kept its color under
|
|
the same answer. Restrained still means mostly neutral: the mixes stay on the
|
|
quiet end and the ground is untouched. Declared ahead of the strategy section
|
|
so the three answers that do rewrite these slots still win. */
|
|
.picker-preview-type {
|
|
--pvs-ghost: color-mix(in oklab, var(--pv-primary) 14%, var(--pv-neutral));
|
|
--pvs-ghost-edge: color-mix(in oklab, var(--pv-primary) 42%, var(--pv-neutral));
|
|
--pvs-image: color-mix(in oklab, var(--pv-primary) 22%, var(--pv-neutral));
|
|
--pvs-image-a: color-mix(in oklab, var(--pv-primary) 26%, var(--pv-neutral));
|
|
--pvs-image-b: color-mix(in oklab, var(--pv-primary) 48%, var(--pv-neutral));
|
|
--pvs-image-edge: color-mix(in oklab, var(--pv-primary) 40%, var(--pv-neutral));
|
|
--pvs-accent-a: var(--pv-primary);
|
|
--pvs-accent-b: color-mix(in oklab, var(--pv-primary) 62%, var(--pv-neutral));
|
|
--pvs-accent-c: color-mix(in oklab, var(--pv-primary) 38%, var(--pv-neutral));
|
|
--pvs-accent-d: color-mix(in oklab, var(--pv-primary) 20%, var(--pv-neutral));
|
|
}
|
|
|
|
/* ============================================================
|
|
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;
|
|
}
|
|
|
|
/* The picture is the one row here whose height is a share of the frame rather
|
|
than a count of what is in it, so it is the row that gives when the frame is
|
|
shorter than the page: the bars below it are drawn at fixed pixel heights and
|
|
do not shrink with the window. Capped at its share rather than set to it, so
|
|
a tall frame is unchanged and a short one takes the difference out of the
|
|
picture instead of pushing the card row through the footer. The card row asks
|
|
for its own height first and takes the leftover after: as a bare fraction it
|
|
was handed whatever the rows above had finished with, which on a short frame
|
|
was a third of what a card is. */
|
|
.ps-phone-body {
|
|
min-height: 0;
|
|
display: grid;
|
|
grid-template-rows: minmax(0, 31%) auto auto auto 34px auto minmax(min-content, 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,
|
|
.ps-ops-rail,
|
|
.ps-ops-main,
|
|
.ps-ops-item,
|
|
.ps-ops-chart,
|
|
.ps-ops-table,
|
|
.ps-ops-row,
|
|
.ps-ops-panel,
|
|
.ps-ops-plot > i,
|
|
.ps-docs-rail,
|
|
.ps-docs-note,
|
|
.ps-index-row,
|
|
.ps-index-rail
|
|
) {
|
|
/* Width, margin, and padding ride along so the boundary answers morph rather
|
|
than snap: a hairline grows out of nothing, the proof band slides off the
|
|
page margins, and a card's padding opens instead of jumping. */
|
|
transition:
|
|
background-color 280ms var(--ks-ease),
|
|
background-image 280ms var(--ks-ease),
|
|
border-color 280ms var(--ks-ease),
|
|
border-width 280ms var(--ks-ease),
|
|
border-radius 280ms var(--ks-ease),
|
|
box-shadow 280ms var(--ks-ease),
|
|
margin 280ms var(--ks-ease),
|
|
padding 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 frame's own box on the font screen. Every chosen surface gets a board and
|
|
they are all mounted, so the row is named rather than left to auto-placement:
|
|
four boards would otherwise stack into four rows and take the column past the
|
|
rail beside it. The tab strip takes the row above them, and the stage is also
|
|
the node the chosen pair's faces are written to, which is how one write sets
|
|
all four boards. */
|
|
.picker-type-stage {
|
|
/* The chosen pair is the run's answer rather than one board's, so the two
|
|
faces are declared here and written here, and every board is set in them
|
|
by inheriting. Declared on a board instead, these would outrank the write
|
|
and the boards would all draw in the picker's own faces. */
|
|
--pt-heading: var(--ks-font-display);
|
|
--pt-body: var(--ks-font);
|
|
--pt-heading-weight: 600;
|
|
|
|
min-width: 0;
|
|
min-height: 0;
|
|
display: grid;
|
|
grid-template-columns: minmax(0, 1fr);
|
|
grid-template-rows: auto minmax(0, 1fr);
|
|
}
|
|
|
|
.picker-type-stage > .picker-artboard {
|
|
grid-row: 2;
|
|
/* Both sides are named so the declared ratio has nothing left to decide. It
|
|
still does the asking: with no height of its own the stage measures the
|
|
ratio, which is what the row across from the rail is offered, and the taller
|
|
of the two wins. Leave the height out and the ratio answers instead of the
|
|
row, and the board comes up short of the rail beside it. */
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
/* One board 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-artboard[hidden] {
|
|
display: none;
|
|
}
|
|
|
|
/* The same box for a question screen that draws a board per surface, which is
|
|
screen 06 today. The font stage above carries the pair's two faces as well,
|
|
and that is the whole of the difference between them. */
|
|
.picker-board-stage {
|
|
min-width: 0;
|
|
min-height: 0;
|
|
display: grid;
|
|
grid-template-columns: minmax(0, 1fr);
|
|
grid-template-rows: auto minmax(0, 1fr);
|
|
}
|
|
|
|
.picker-board-stage > .picker-artboard {
|
|
grid-row: 2;
|
|
width: 100%;
|
|
height: 100%;
|
|
}
|
|
|
|
/* 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.
|
|
|
|
Three columns share 1388px at the container's cap, and the two previews are
|
|
what pay for the third. The rail goes first: its widest row is the ratio and
|
|
Augmented Fourth on one line, which needs 300px of column and not the 420 it
|
|
was taking, and its description was already wrapping. The sheet gives the
|
|
rest. It can afford to because its sample string shortened to the scale's
|
|
name in the same pass, and a name is under half the width of the sentence
|
|
that was there, so the sheet renders its steps larger in 538px than it did in
|
|
918. The reading column is last and smallest: 45 characters of lorem at the
|
|
base step is a measure, and more than that is width the other two need. */
|
|
.picker-scale-grid {
|
|
max-height: calc(100svh - 420px);
|
|
display: grid;
|
|
grid-template-columns: minmax(300px, 352px) minmax(0, 1.25fr) minmax(0, 1fr);
|
|
gap: clamp(22px, 2.2vw, 34px);
|
|
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.
|
|
|
|
The gutters answer to the row rather than the viewport, because the row is
|
|
one of three columns now and a viewport-scaled gutter was spending 48px of a
|
|
538px sheet on air the numbers did not ask for. */
|
|
.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, 3%, 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;
|
|
}
|
|
|
|
/* The sheet answers what each step measures. This column answers what the steps
|
|
do to each other once a page is built out of them, which is a different
|
|
question and the one a scale is actually chosen on. Same panel, same ink, same
|
|
sizes: it reads the step values syncTypeScale computed for the sheet, so one
|
|
scale cannot render two ways on one screen.
|
|
|
|
What it deliberately has none of is color, and no chrome past the panel it
|
|
sits in. A palette here would ask screen 03's question again inside screen
|
|
05's, and a specimen that has to be read past its own decoration is not
|
|
showing a type scale. The annotation is held to the same rule: thirteen
|
|
accented marks on a colorless field were the brightest thing on the screen,
|
|
which put the loudest ink on the layer that is only there to be looked up.
|
|
Mono, faint, and out in the margin says annotation three times over. */
|
|
.picker-scale-specimen {
|
|
--pt-heading: var(--ks-font-display);
|
|
--pt-body: var(--ks-font);
|
|
--pt-heading-weight: 400;
|
|
/* One rhythm unit for the column, taken from the step the running text sets
|
|
at, so the air between components is measured in the scale being judged
|
|
rather than against it. */
|
|
--sp-rhythm: calc(var(--ts-step-0) * 1px);
|
|
/* The margin the annotation hangs in. Reserved as padding so a label can
|
|
never be measured against the line it names. Sized to the longest label
|
|
the column sets, five mono characters, plus enough slack that a fallback
|
|
mono face with wider glyphs still clears the panel edge. */
|
|
--sp-rail: 2.6rem;
|
|
--sp-rail-gap: 0.85rem;
|
|
--sp-pad: clamp(18px, 3.2%, 36px);
|
|
min-width: 0;
|
|
min-height: 0;
|
|
display: grid;
|
|
align-content: start;
|
|
/* Two intervals, not one. A page is read in sections, and a column set at a
|
|
single interval has none: the heading, its paragraph, and its list arrive
|
|
as three unrelated blocks, which is the one thing a specimen of a scale
|
|
must not say. Blocks inside a section sit close; the break before a
|
|
heading is the wide one. Both are counted in the base step, so the cadence
|
|
holds while every size in the column moves. */
|
|
gap: calc(var(--sp-rhythm) * 1.5);
|
|
padding-block: calc(clamp(14px, 2.4%, 28px) + 10px) clamp(14px, 2.4%, 28px);
|
|
padding-inline: calc(var(--sp-rail) + var(--sp-rail-gap) + 8px) 16px;
|
|
background: var(--ks-lacquer-raised);
|
|
color: var(--ks-text);
|
|
border: 1px solid var(--ks-rule);
|
|
border-radius: 2px;
|
|
overflow-y: auto;
|
|
/* Reaching the end of the specimen should not then carry the page with it. */
|
|
overscroll-behavior: contain;
|
|
/* Screen 04's list says it scrolls with a thin patina track, and the same
|
|
track says it here. What that list adds on top, arrows that page by a row,
|
|
has nothing to page through in a column of prose. */
|
|
scrollbar-width: thin;
|
|
scrollbar-color: color-mix(in oklab, var(--ks-patina-deep) 35%, transparent) transparent;
|
|
}
|
|
|
|
/* Both preview columns take the rail's shape, a panel with a line under it, so
|
|
the three panels end on one edge. What goes in that line is what the column
|
|
owes the reader: the specimen hides more than either list on this screen and
|
|
an overlay scrollbar says so only once the reader has already guessed, so it
|
|
gets the rail's arrows; the sheet hides nothing and gets the sentence that
|
|
explains why its numbers and its rendered sizes disagree, which is also the
|
|
one place that sentence has ever been next to the numbers. */
|
|
.picker-scale-column {
|
|
min-width: 0;
|
|
min-height: 0;
|
|
display: grid;
|
|
grid-template-rows: minmax(0, 1fr) auto;
|
|
gap: 14px;
|
|
}
|
|
|
|
/* A column of prose is the one item in this row whose content is taller than
|
|
any viewport, so left alone it takes the whole cap and the rail, which stops
|
|
at 570, ends 90px short of it. Taking the rail's ceiling keeps the columns
|
|
landing on one line and keeps the row the height the CTA above the fold was
|
|
calibrated against. */
|
|
@media (min-width: 1200px) {
|
|
.picker-scale-column {
|
|
max-height: 570px;
|
|
}
|
|
}
|
|
|
|
/* One sentence is shorter than the 44px buttons the other two lines are made
|
|
of, and a line that measures less takes height back from its panel, which
|
|
loses the shared edge the shape exists for. */
|
|
.picker-scale-column .picker-type-controls {
|
|
min-height: 44px;
|
|
}
|
|
|
|
/* A grid item's automatic minimum is its min-content width, so one word of the
|
|
display heading at the widest ratio can hold this column open past the panel
|
|
and hand it a horizontal scrollbar. Zeroing that floor and letting the word
|
|
break keeps every overflow on the axis the column already scrolls. */
|
|
.picker-scale-specimen > * {
|
|
min-width: 0;
|
|
overflow-wrap: break-word;
|
|
}
|
|
|
|
/* Headings take the display face and running text the body face, which is the
|
|
division screen 04 just committed to. */
|
|
.picker-scale-specimen :is(.sp-h1, .sp-h2, .sp-h3, .sp-h4) {
|
|
margin: 0;
|
|
color: var(--ks-champagne);
|
|
font-family: var(--pt-heading);
|
|
font-weight: var(--pt-heading-weight);
|
|
line-height: 1.14;
|
|
letter-spacing: -0.01em;
|
|
text-wrap: balance;
|
|
}
|
|
|
|
.sp-h1 {
|
|
font-size: calc(var(--ts-step-6) * 1px);
|
|
}
|
|
|
|
.sp-h2 {
|
|
font-size: calc(var(--ts-step-5) * 1px);
|
|
}
|
|
|
|
.sp-h3 {
|
|
font-size: calc(var(--ts-step-4) * 1px);
|
|
}
|
|
|
|
.sp-h4 {
|
|
font-size: calc(var(--ts-step-3) * 1px);
|
|
}
|
|
|
|
/* A heading belongs to what follows it, so the break in the column goes above
|
|
it and not below. The column gap alone reads as a list of unrelated blocks. */
|
|
.picker-scale-specimen :is(.sp-h2, .sp-h3, .sp-h4) {
|
|
margin-block-start: calc(var(--sp-rhythm) * 2.4);
|
|
}
|
|
|
|
/* Which component each block is, said in the annotation layer rather than in
|
|
the specimen: the mono face where the specimen is proportional, the same
|
|
faint ink the sheet's reference numbers take, and the smallest step the kit
|
|
labels anything at.
|
|
|
|
It hangs in the reserved margin instead of entering the block. A float or a
|
|
right inset both shorten the line box they share, and this column exists to
|
|
show how a ratio wraps: at the widest ratios a floated label was breaking
|
|
the display heading onto a third line and leaving one character alone on the
|
|
first, so the annotation was deciding the texture it was there to describe.
|
|
Out here it decides nothing, and thirteen marks that were ragged against
|
|
thirteen different line endings share one edge. */
|
|
.picker-scale-specimen [data-sp-label] {
|
|
position: relative;
|
|
}
|
|
|
|
.picker-scale-specimen [data-sp-label]::before {
|
|
content: attr(data-sp-label);
|
|
position: absolute;
|
|
top: 0;
|
|
padding-top: 6px;
|
|
inset-inline-end: calc(100% + var(--sp-rail-gap));
|
|
color: var(--ks-text-faint);
|
|
font-family: var(--ks-mono);
|
|
font-size: var(--ks-type-eyebrow-size);
|
|
font-weight: 500;
|
|
letter-spacing: 0.08em;
|
|
line-height: 1.5;
|
|
text-transform: uppercase;
|
|
white-space: nowrap;
|
|
/* Trimmed so the label's cap, not its leading, is what lands on the block's
|
|
top edge. The blocks above and below it are set at whatever the ratio
|
|
dealt, so any alignment measured from a line box moves with the scale. */
|
|
text-box: trim-both cap alphabetic;
|
|
}
|
|
|
|
.picker-scale-specimen :is(.sp-lede, .sp-p, .sp-list, .sp-quote) {
|
|
margin: 0;
|
|
font-family: var(--pt-body);
|
|
font-weight: 400;
|
|
}
|
|
|
|
.sp-lede {
|
|
color: var(--ks-champagne);
|
|
font-size: calc(var(--ts-step-1) * 1px);
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.sp-p {
|
|
font-size: calc(var(--ts-step-0) * 1px);
|
|
line-height: 1.62;
|
|
}
|
|
|
|
.picker-scale-specimen strong {
|
|
color: var(--ks-champagne);
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* Clear of the descenders the body face will have, whichever face it is. */
|
|
.picker-scale-specimen u {
|
|
text-decoration-thickness: from-font;
|
|
text-underline-offset: 0.16em;
|
|
}
|
|
|
|
.sp-list {
|
|
padding-inline-start: calc(var(--sp-rhythm) * 1.5);
|
|
font-size: calc(var(--ts-step-0) * 1px);
|
|
line-height: 1.58;
|
|
}
|
|
|
|
.sp-list li + li {
|
|
margin-block-start: calc(var(--sp-rhythm) * 0.5);
|
|
}
|
|
|
|
.sp-list-ordered {
|
|
font-variant-numeric: tabular-nums;
|
|
}
|
|
|
|
.sp-list::marker,
|
|
.sp-list li::marker {
|
|
color: var(--ks-text-faint);
|
|
}
|
|
|
|
/* The rule is the quote's only mark. It is a typographic convention rather
|
|
than picker chrome, which is why it survives the no-decoration rule that
|
|
removed everything else from this column. */
|
|
.sp-quote {
|
|
padding-inline-start: calc(var(--sp-rhythm) * 1.2);
|
|
border-inline-start: 1px solid var(--ks-rule);
|
|
}
|
|
|
|
.sp-quote p {
|
|
margin: 0;
|
|
color: var(--ks-champagne);
|
|
font-size: calc(var(--ts-step-2) * 1px);
|
|
line-height: 1.42;
|
|
}
|
|
|
|
.sp-cite {
|
|
display: block;
|
|
margin-block-start: calc(var(--sp-rhythm) * 0.7);
|
|
color: var(--ks-text-muted);
|
|
font-size: calc(var(--ts-step-0) * 1px);
|
|
font-style: normal;
|
|
letter-spacing: 0.02em;
|
|
}
|
|
|
|
/* The mono face is not part of the pair screen 04 deals, so code takes the
|
|
kit's own stack. Guessing a third face here would put a claim in the
|
|
specimen that the run never made. */
|
|
.sp-code,
|
|
.sp-pre {
|
|
/* Mono runs large against a proportional face set at the same size. */
|
|
font-size: calc(var(--ts-step-0) * 0.94px);
|
|
font-family: var(--ks-mono);
|
|
}
|
|
|
|
.sp-code {
|
|
color: var(--ks-champagne);
|
|
}
|
|
|
|
/* The block wraps instead of scrolling sideways, and not for taste: an
|
|
overflowing box is a scroll container, a scroll container's automatic minimum
|
|
height is zero, and this column always has more content than height. The grid
|
|
spends that overflow on the one track it is allowed to shrink, so a code block
|
|
with overflow-x lands two pixels tall and the snippet disappears. */
|
|
.sp-pre {
|
|
margin: 0;
|
|
padding: 14px 16px;
|
|
color: var(--ks-text-muted);
|
|
border: 1px solid var(--ks-rule);
|
|
border-radius: 2px;
|
|
line-height: 1.55;
|
|
white-space: pre-wrap;
|
|
}
|
|
|
|
/* ============================================================
|
|
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.
|
|
|
|
The structural boards are on it too. They set no glyphs, but the app shell,
|
|
the document, and the index are drawn out of this ladder's spacing steps, and
|
|
a board that resolved none of them would drop every rule that reaches for one.
|
|
The ink slots below stay where they are: those exist because a bar and a
|
|
printed line need different contrast, and a structural board is all bars. */
|
|
.picker-preview-type,
|
|
[data-carry] {
|
|
--pt-ratio: 1.618;
|
|
--pt-base: clamp(0.72rem, 0.86vw, 0.84rem);
|
|
}
|
|
|
|
.picker-preview-type {
|
|
/* 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,
|
|
[data-carry],
|
|
[data-carry] .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%;
|
|
grid-template-rows: 7.2% 1fr;
|
|
}
|
|
|
|
/* On a structural board the band inset is the drawn field's own margin, and it is
|
|
a constant on purpose. What the layout answer moves on a shell, a document, or
|
|
an index is the proportion of their regions, and the margin is only there so
|
|
those regions can be checked against the field. Hung off the layout answer
|
|
instead, the balanced page would take the artboard's 5.5 and 3.2, and every
|
|
region edge would land three pixels shy of the line it was drawn to meet:
|
|
read as sloppiness on a tool, where on the landing page the same lean is the
|
|
answer being shown. */
|
|
[data-carry] {
|
|
--ps-gutter: 6%;
|
|
}
|
|
|
|
[data-carry] .ps-phone {
|
|
--ps-gutter: var(--pvs-phone-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) 34%;
|
|
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) minmax(0, 1fr);
|
|
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 cards keep their column's width so the three squares are one size;
|
|
what centers is their contents, below. Shrinking the card to its label
|
|
handed each square a different width. */
|
|
justify-items: stretch;
|
|
}
|
|
|
|
.picker-preview-type .ps-desktop .ps-gallery-item {
|
|
justify-items: center;
|
|
text-align: center;
|
|
}
|
|
|
|
.picker-preview-type .ps-desktop .ps-gallery-item span {
|
|
align-items: center;
|
|
}
|
|
|
|
/* 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 > i {
|
|
margin-inline: auto;
|
|
}
|
|
|
|
/* 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.
|
|
|
|
The picture keeps a cap rather than a share, for the reason under the shared
|
|
phone body: it is the only row here that is not a count of its own contents,
|
|
so it is the row that gives first when the frame runs short. */
|
|
.picker-preview-type .ps-phone-body {
|
|
grid-template-rows: minmax(0, 18%) repeat(5, auto);
|
|
gap: var(--ps-gap-sm);
|
|
align-content: space-between;
|
|
padding: var(--ps-band-y) var(--ps-gutter);
|
|
}
|
|
|
|
.picker-preview-type[data-surface="persuade"] .ps-phone-body > .ps-image {
|
|
display: none;
|
|
}
|
|
|
|
.picker-preview-type[data-surface="persuade"] .ps-phone-body {
|
|
grid-template-rows: repeat(5, auto);
|
|
gap: 0;
|
|
align-content: start;
|
|
}
|
|
|
|
.picker-preview-type[data-surface="persuade"] .ps-phone-body .pt-headline {
|
|
font-size: 26px;
|
|
line-height: 1.15;
|
|
margin-block-end: 8px;
|
|
}
|
|
|
|
.picker-preview-type[data-surface="persuade"] .ps-phone-body .pt-body {
|
|
margin-block-end: var(--ps-gap-md);
|
|
}
|
|
|
|
.picker-preview-type[data-surface="persuade"] .ps-phone-body .ps-actions {
|
|
margin-block-end: var(--ps-gap-lg);
|
|
}
|
|
|
|
.picker-preview-type[data-surface="persuade"] .ps-phone-body .ps-proof {
|
|
grid-template-columns: auto minmax(0, 1fr) auto;
|
|
align-items: center;
|
|
column-gap: var(--ps-gap-md);
|
|
margin-block-end: var(--ps-gap-md);
|
|
}
|
|
|
|
.picker-preview-type[data-surface="persuade"] .ps-phone-body .ps-proof-divider,
|
|
.picker-preview-type[data-surface="persuade"] .ps-phone-body .ps-proof-item:nth-of-type(2) {
|
|
display: none;
|
|
}
|
|
|
|
.picker-preview-type[data-surface="persuade"] .ps-phone-body .ps-proof-item:first-of-type {
|
|
grid-column: 2;
|
|
justify-content: center;
|
|
}
|
|
|
|
.picker-preview-type[data-surface="persuade"] .ps-phone-body .ps-proof::before,
|
|
.picker-preview-type[data-surface="persuade"] .ps-phone-body .ps-proof::after {
|
|
content: "";
|
|
position: static;
|
|
width: 5px;
|
|
height: 5px;
|
|
background: none;
|
|
border-left: 1px solid color-mix(in oklab, var(--pvs-bars) 38%, transparent);
|
|
border-bottom: 1px solid color-mix(in oklab, var(--pvs-bars) 38%, transparent);
|
|
align-self: center;
|
|
}
|
|
|
|
.picker-preview-type[data-surface="persuade"] .ps-phone-body .ps-proof::before {
|
|
grid-column: 1;
|
|
transform: rotate(45deg);
|
|
}
|
|
|
|
.picker-preview-type[data-surface="persuade"] .ps-phone-body .ps-proof::after {
|
|
grid-column: 3;
|
|
transform: rotate(-135deg);
|
|
}
|
|
|
|
.picker-preview-type[data-surface="persuade"] .ps-phone-body .ps-editorial-copy {
|
|
gap: var(--ps-gap-xs);
|
|
}
|
|
|
|
.picker-preview-type[data-surface="persuade"] .ps-phone-body .ps-editorial-copy strong {
|
|
font-size: calc(var(--pt-title) * 1.12);
|
|
}
|
|
|
|
.picker-preview-type[data-surface="persuade"] .ps-phone-body .pt-section-body {
|
|
font-size: var(--pt-base);
|
|
}
|
|
|
|
/* 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 two buttons divide the row rather than each asking for the width its own
|
|
label wants. A pair of labels nobody wrote to a length was setting the widest
|
|
line on the handset, and a nowrap line is a width the column has to find:
|
|
the row came out wider than the phone, and every band under it was drawn to
|
|
the row instead of to the frame.
|
|
|
|
Two even columns rather than two flexible children, because a flex item still
|
|
reports what its label wants when its container is asked for a minimum, and
|
|
the minimum is exactly what was overflowing here. A label longer than half the
|
|
row ends in an ellipsis; the clip margin is the house allowance for a
|
|
descender under text-box trimming. */
|
|
.picker-preview-type .ps-phone-body .ps-actions {
|
|
display: grid;
|
|
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
|
}
|
|
|
|
.picker-preview-type .ps-phone-body .ps-actions i {
|
|
min-width: 0;
|
|
padding-inline: 0.8em;
|
|
overflow: clip;
|
|
overflow-clip-margin: 0.4em;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
/* 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;
|
|
}
|
|
|
|
/* Asked of the board rather than the window, for the reason the docs and ops
|
|
boards are: the ladder floors while the frame keeps shrinking, so a short
|
|
frame holds fewer blocks at the same reading size. The ops and docs boards
|
|
name their own container over this one. */
|
|
.picker-preview-type {
|
|
container-type: size;
|
|
container-name: ps-type-board;
|
|
}
|
|
|
|
/* Below this the handset is holding more sections than the frame has room for,
|
|
and the picture it takes them out of has already given everything it has. The
|
|
section pair is what goes, on the same terms as the card row above: its
|
|
heading face is the headline's and its body face is the lede's, both already
|
|
set on this page, and the desktop beside it keeps the section in full. */
|
|
@container ps-type-board (height < 500px) {
|
|
.picker-preview-type .ps-phone-body .ps-editorial-copy {
|
|
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,
|
|
[data-carry] .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, so
|
|
the proof label, which does have to truncate, clips with a margin instead.
|
|
|
|
The nowrap below is the default for a one-line label, and the card labels
|
|
opt back out of it further down. Every selector in this list is one class
|
|
and one type, so the rules that override this one 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: none;
|
|
}
|
|
|
|
.picker-preview-type .ps-phone-footer {
|
|
display: none;
|
|
}
|
|
|
|
.picker-preview-type .ps-phone .pt-headline {
|
|
font-size: var(--pt-title);
|
|
}
|
|
|
|
.picker-preview-type .ps-phone-top .ps-brand-block {
|
|
font-size: var(--pt-micro);
|
|
}
|
|
|
|
.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: center;
|
|
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.
|
|
|
|
The hero buttons are `<i>`, so the style is stated rather than inherited:
|
|
the slots that reset the UA italic in one shared list cannot take these two,
|
|
because that list also clears the background and the radius and a button is
|
|
the fill it sits on. The nav button beside them is a `div` and never had the
|
|
italic to lose, which is how the pair came to be set two ways. */
|
|
.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-style: normal;
|
|
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 chip is one line. The columns are even and the row is read across, so the
|
|
label that ran long took a second line and set the whole band to two
|
|
heights at once. Clipped rather than hidden because the box above is trimmed
|
|
to the baseline: the clip margin is the room the tail of a g or a y needs,
|
|
and the ellipsis still lands on the inline edge. */
|
|
.picker-preview-type .ps-proof-item span {
|
|
min-width: 0;
|
|
overflow: clip;
|
|
overflow-clip-margin: 0.4em;
|
|
color: var(--pvs-bars);
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* 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 {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
}
|
|
|
|
/* 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;
|
|
text-box: unset;
|
|
display: block;
|
|
}
|
|
|
|
.picker-preview-type .ps-gallery-item b:first-child,
|
|
.picker-preview-type .ps-gallery-item [data-type-gallery-title] {
|
|
color: var(--pvs-title);
|
|
line-height: 1.38;
|
|
}
|
|
|
|
.picker-preview-type .ps-gallery-item b:last-child {
|
|
line-height: 1.45;
|
|
}
|
|
|
|
/* ============================================================
|
|
The App UI board.
|
|
|
|
Same frame, same nav band, same footer as the landing board; only the two
|
|
bodies change. The wireframe on the surface tile is what it is drawn from,
|
|
column for column: a rail, a working column, a settings panel, at the
|
|
18.68 / 51.84 / rest the drawing already uses.
|
|
|
|
What it refuses to add is a display line: nothing on a dashboard is set at
|
|
35px, and a board that opened with one would be recommending a pair for a
|
|
page this surface never has. The largest step here is the section title, and
|
|
the face doing the most work is the body face between 10 and 12px.
|
|
|
|
The chart is the one element on the board that is still mostly a drawing.
|
|
Its bars carry no glyphs and prove nothing about a pair, so they are held to
|
|
a share of the plot and are there for the two pieces of text they bracket:
|
|
the heading above them and the category labels below, which is the tightest
|
|
slot on any of the boards. Both are what a chart really costs a pairing.
|
|
============================================================ */
|
|
.picker-preview-type--ops .ps-desktop {
|
|
grid-template-rows: 9.4% minmax(0, 1fr) 8.3%;
|
|
}
|
|
|
|
/* The board is asked its own height, the way the docs board is, because that is
|
|
what the working column has to fit inside and it is not the window's: the
|
|
frame shrinks with the window while the type ladder floors, so a short frame
|
|
holds the same four blocks at a larger relative size. What a short frame
|
|
spends less on hangs off the query at the end of this section. */
|
|
.picker-preview-type--ops {
|
|
container-type: size;
|
|
container-name: ps-ops-board;
|
|
}
|
|
|
|
.ps-ops {
|
|
min-width: 0;
|
|
min-height: 0;
|
|
display: grid;
|
|
grid-template-columns: 18.68% 51.84% minmax(0, 1fr);
|
|
}
|
|
|
|
.ps-ops-rail,
|
|
.ps-ops-main {
|
|
border-right: 1px solid var(--pvs-rule);
|
|
}
|
|
|
|
.ps-ops-rail,
|
|
.ps-ops-panel {
|
|
min-width: 0;
|
|
display: grid;
|
|
align-content: start;
|
|
gap: var(--ps-gap-sm);
|
|
padding: var(--ps-band-y) calc(var(--pt-base) * 0.7);
|
|
}
|
|
|
|
/* The working column runs to the bottom of the band rather than stopping partway
|
|
down it: whatever a pair leaves over is spread between the four blocks instead
|
|
of pooling under the table, which is how the docs and portfolio boards already
|
|
fill. It takes no height gate, unlike the docs column, because the inset it
|
|
would spend on a tall frame is already there on every frame (this padding is
|
|
symmetric) and because `space-between` is `start` again the moment the free
|
|
space runs out, so a short frame keeps the drawing it has.
|
|
|
|
What it does not do is gain a block on a tall frame the way the docs column
|
|
gains its list. That column was short a structural element; this one draws the
|
|
four a dashboard is made of, so a fifth would be filling for its own sake. Its
|
|
table is also filled from the run's own item slots, and a row past their count
|
|
would come up blank.
|
|
|
|
`min-height: 0` for the same reason the docs column carries it: without it the
|
|
column takes its automatic minimum height from the table and grows past the
|
|
row, which pushes the footer out of the frame. Held to the band, the
|
|
widest-setting pair runs to the band's edge instead. */
|
|
.ps-ops-main {
|
|
min-width: 0;
|
|
min-height: 0;
|
|
display: grid;
|
|
align-content: space-between;
|
|
gap: var(--ps-gap-lg);
|
|
padding: var(--ps-band-y) calc(var(--pt-base) * 1.1);
|
|
}
|
|
|
|
/* Chrome on a tool is one line at the smallest step there is, and a rail item
|
|
or a table cell that took two would be a different control. Holding them to
|
|
one means a pair that sets them too wide reports it as an overflow rather
|
|
than as a second line nobody reads as a fault. */
|
|
.picker-preview-type--ops :is(
|
|
.ps-ops-item b,
|
|
.ps-ops-row b,
|
|
.ps-ops-field b,
|
|
.ps-ops-switch b,
|
|
.ps-ops-lanes b
|
|
) {
|
|
min-width: 0;
|
|
color: var(--pvs-bars);
|
|
font-family: var(--pt-body);
|
|
font-size: var(--pt-micro);
|
|
font-weight: 500;
|
|
line-height: 1.4;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.ps-ops-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: calc(var(--pt-base) * 0.5);
|
|
padding: calc(var(--pt-base) * 0.35) calc(var(--pt-base) * 0.45);
|
|
border-radius: var(--pvs-radius-bar);
|
|
}
|
|
|
|
.ps-ops-item > i {
|
|
flex: none;
|
|
width: calc(var(--pt-base) * 0.4);
|
|
aspect-ratio: 1;
|
|
background-color: var(--pvs-accent-a);
|
|
border-radius: 50%;
|
|
}
|
|
|
|
/* The open section is a filled plate rather than a tinted row, which is the
|
|
mark that tells a reader this rail belongs to a tool and not to a document.
|
|
Its label is the one place a rail sets the body face's semibold. */
|
|
.ps-ops-item--on {
|
|
background-color: var(--pvs-cta);
|
|
}
|
|
|
|
.picker-preview-type--ops .ps-ops-item--on b {
|
|
color: var(--pvs-cta-text);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.ps-ops-item--on > i {
|
|
background-color: var(--pvs-cta-text);
|
|
}
|
|
|
|
.ps-ops-title {
|
|
color: var(--pvs-title);
|
|
font-family: var(--pt-heading);
|
|
font-size: var(--pt-title);
|
|
font-weight: var(--pt-heading-weight);
|
|
line-height: 1.2;
|
|
overflow-wrap: break-word;
|
|
}
|
|
|
|
.ps-ops-metrics {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
gap: var(--ps-gap-sm);
|
|
}
|
|
|
|
.ps-ops-metric {
|
|
min-width: 0;
|
|
display: grid;
|
|
gap: var(--ps-gap-xs);
|
|
}
|
|
|
|
/* The figure is set in the body face, because on a tool it is the body face
|
|
that has to draw numbers: a column of them lines up or it does not, and a
|
|
pair whose text figures step up and down loses a table the moment there is
|
|
more than one row of it. */
|
|
.ps-ops-metric b {
|
|
color: var(--pvs-title);
|
|
font-family: var(--pt-body);
|
|
font-size: var(--pt-title);
|
|
font-weight: 600;
|
|
font-variant-numeric: tabular-nums lining-nums;
|
|
line-height: 1.1;
|
|
}
|
|
|
|
/* Three of these sit side by side in a third of the column each, so the label
|
|
is the one micro line on the board that has to be allowed to take two. */
|
|
.picker-preview-type--ops .ps-ops-metric span {
|
|
color: var(--pvs-bars);
|
|
font-family: var(--pt-body);
|
|
font-size: var(--pt-micro);
|
|
line-height: 1.35;
|
|
}
|
|
|
|
.ps-ops-chart {
|
|
min-width: 0;
|
|
display: grid;
|
|
gap: var(--ps-gap-sm);
|
|
}
|
|
|
|
/* A chart title is a heading, so it takes the heading face, and it takes it at
|
|
the reading step rather than the title step: the section title above it is
|
|
already the largest thing on the board, and a second line at that size would
|
|
read as a second section. Two sizes of the same face is the useful thing here
|
|
anyway, because a display face that holds at 22px and falls apart at 13 is
|
|
exactly the fault this board exists to surface. */
|
|
.ps-ops-chart-title {
|
|
color: var(--pvs-title);
|
|
font-family: var(--pt-heading);
|
|
font-size: var(--pt-base);
|
|
font-weight: var(--pt-heading-weight);
|
|
line-height: 1.3;
|
|
overflow-wrap: break-word;
|
|
}
|
|
|
|
/* The bars and the labels are two grids over one column structure, so a label
|
|
is under its own bar without either row measuring the other. Implicit columns
|
|
rather than a stated count, because the handset draws three of them and the
|
|
desktop five and neither should have to restate it here.
|
|
|
|
The axis is the plot's own bottom border, so the bars stand on it at every
|
|
width. Their height is a share of the plot rather than a value, which is what
|
|
keeps this a drawing of a chart: nothing here is a reading of data. */
|
|
.ps-ops-plot,
|
|
.ps-ops-lanes {
|
|
display: grid;
|
|
grid-auto-flow: column;
|
|
grid-auto-columns: minmax(0, 1fr);
|
|
gap: calc(var(--pt-base) * 0.7);
|
|
}
|
|
|
|
.ps-ops-plot {
|
|
height: calc(var(--pt-base) * 5.5);
|
|
align-items: end;
|
|
border-bottom: 1px solid var(--pvs-rule);
|
|
}
|
|
|
|
/* The lanes that are not the leading one take the fill an unselected row takes,
|
|
which is the same slot the wireframe chart on the surface tile fills its bars
|
|
with. It is the one slot that stays a light step off the ground under every
|
|
strategy and moves only in hue, and five bars are an area where the board's
|
|
accents are points: at accent weight the chart would outrank the table it
|
|
sits above. */
|
|
.ps-ops-plot > i {
|
|
height: var(--h);
|
|
background-color: var(--pvs-ghost);
|
|
}
|
|
|
|
/* The leading lane is the only bar a schematic chart has to say anything with,
|
|
so it takes the fill the board already spends on a chosen state: the open
|
|
rail plate and the switch that is on. Under Drenched that fill is the page's
|
|
own ink, which is the one reading of primary that stays visible when the
|
|
ground is the primary. */
|
|
.ps-ops-plot > .ps-ops-bar--lead {
|
|
background-color: var(--pvs-cta);
|
|
}
|
|
|
|
/* Centered under the bar, and the one micro label on this board that is a
|
|
caption rather than a row: it has its bar's width and no more, which is why
|
|
the words it is filled with are single and short. */
|
|
.picker-preview-type--ops .ps-ops-lanes b {
|
|
text-align: center;
|
|
}
|
|
|
|
/* Three columns over a handset's width come out square at the desktop's plot
|
|
height, and a square is a tile rather than a bar. The gap is spent on both
|
|
rows or the labels walk off the bars they name. */
|
|
.picker-preview-type--ops .ps-phone-body :is(.ps-ops-plot, .ps-ops-lanes),
|
|
[data-carry][data-surface="operate"] .ps-phone-body :is(.ps-ops-plot, .ps-ops-lanes) {
|
|
gap: calc(var(--pt-base) * 2);
|
|
}
|
|
|
|
.picker-preview-type--ops .ps-phone-body .ps-ops-plot,
|
|
[data-carry][data-surface="operate"] .ps-phone-body .ps-ops-plot {
|
|
height: calc(var(--pt-base) * 7);
|
|
}
|
|
|
|
.ps-ops-table {
|
|
display: grid;
|
|
border-top: 1px solid var(--pvs-rule);
|
|
}
|
|
|
|
.ps-ops-row {
|
|
display: grid;
|
|
grid-template-columns: auto minmax(0, 1fr) auto auto;
|
|
gap: calc(var(--pt-base) * 0.7);
|
|
align-items: center;
|
|
padding: calc(var(--pt-base) * 0.55) calc(var(--pt-base) * 0.3);
|
|
}
|
|
|
|
.ps-ops-row > i {
|
|
width: calc(var(--pt-base) * 0.4);
|
|
aspect-ratio: 1;
|
|
background-color: var(--pvs-accent-c);
|
|
border-radius: 50%;
|
|
}
|
|
|
|
.picker-preview-type--ops .ps-ops-row b:first-of-type {
|
|
color: var(--pvs-title);
|
|
}
|
|
|
|
.picker-preview-type--ops .ps-ops-row b:last-of-type {
|
|
text-align: end;
|
|
font-variant-numeric: tabular-nums lining-nums;
|
|
}
|
|
|
|
/* The heading row is the semibold an Operate product leans on hardest, and it
|
|
sits directly over the cells it names, so any drift between the two weights
|
|
of the same face shows up here first. */
|
|
.picker-preview-type--ops .ps-ops-row--head b {
|
|
color: var(--pvs-copy);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.ps-ops-row--head > i {
|
|
background-color: transparent;
|
|
}
|
|
|
|
.ps-ops-row--on {
|
|
background-color: var(--pvs-ghost);
|
|
}
|
|
|
|
/* A settings panel's heading is a UI label rather than a section title, so it
|
|
takes the body face at reading size and the weight that goes with the job.
|
|
No other board sets that combination, and it is the one an app spends most
|
|
of its chrome on. */
|
|
.ps-ops-panel-title {
|
|
color: var(--pvs-title);
|
|
font-family: var(--pt-body);
|
|
font-size: var(--pt-base);
|
|
font-weight: 600;
|
|
line-height: 1.3;
|
|
}
|
|
|
|
.ps-ops-field {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: calc(var(--pt-base) * 0.4);
|
|
padding: calc(var(--pt-base) * 0.4) calc(var(--pt-base) * 0.5);
|
|
border: 1px solid var(--pvs-ghost-edge);
|
|
border-radius: var(--pvs-radius-bar);
|
|
}
|
|
|
|
.ps-ops-chev {
|
|
flex: none;
|
|
width: calc(var(--pt-base) * 0.32);
|
|
aspect-ratio: 1;
|
|
margin-left: auto;
|
|
border-right: 1px solid var(--pvs-copy);
|
|
border-bottom: 1px solid var(--pvs-copy);
|
|
transform: translateY(-25%) rotate(45deg);
|
|
}
|
|
|
|
.ps-ops-switch {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: calc(var(--pt-base) * 0.4);
|
|
padding-block: calc(var(--pt-base) * 0.3);
|
|
}
|
|
|
|
.ps-ops-toggle {
|
|
flex: none;
|
|
position: relative;
|
|
width: calc(var(--pt-base) * 1.5);
|
|
height: calc(var(--pt-base) * 0.8);
|
|
margin-left: auto;
|
|
background-color: var(--pvs-bars);
|
|
border-radius: 999px;
|
|
}
|
|
|
|
.ps-ops-toggle--on {
|
|
background-color: var(--pvs-cta);
|
|
}
|
|
|
|
.ps-ops-toggle::after {
|
|
content: "";
|
|
position: absolute;
|
|
top: 12.5%;
|
|
left: 8%;
|
|
height: 75%;
|
|
aspect-ratio: 1;
|
|
background-color: var(--pvs-ground);
|
|
border-radius: 50%;
|
|
}
|
|
|
|
.ps-ops-toggle--on::after {
|
|
right: 8%;
|
|
left: auto;
|
|
}
|
|
|
|
/* The handset takes the same screen narrowed: the title, one figure, the rows
|
|
cut to what they are and what they come to, and the two switches. The rail
|
|
and the panel are chrome a phone gives to a drawer, and a drawer is not on
|
|
screen. */
|
|
/* The structural boards are on the same selectors from here down wherever the
|
|
rule is about the shape of the body rather than the setting of its words. A
|
|
handset holding four blocks of a tool holds them the same way whether they are
|
|
printed or drawn, and writing that twice is how two boards claiming to be the
|
|
same anatomy drift apart. */
|
|
.picker-preview-type--ops .ps-phone-body,
|
|
[data-carry][data-surface="operate"] .ps-phone-body {
|
|
grid-template-rows: repeat(4, auto);
|
|
align-content: space-between;
|
|
gap: var(--ps-gap-md);
|
|
}
|
|
|
|
.picker-preview-type--ops .ps-phone-body .ps-ops-row,
|
|
[data-carry][data-surface="operate"] .ps-phone-body .ps-ops-row {
|
|
grid-template-columns: auto minmax(0, 1fr) auto;
|
|
}
|
|
|
|
.picker-preview-type--ops .ps-phone-body .ps-ops-panel,
|
|
[data-carry][data-surface="operate"] .ps-phone-body .ps-ops-panel {
|
|
gap: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
/* A frame with less room than the drawing needs. The plot is the one block here
|
|
whose height is a count rather than its contents, so it is the block that
|
|
gives: the chart keeps its title, its bars, and its labels, and loses the air
|
|
between them, which is the reduction it already makes on the handset. The
|
|
column closes to the middle step at the same time.
|
|
|
|
Shortening rather than dropping, because a dashboard without its chart is the
|
|
one thing this board exists to show a pair against, and 1280 by 800 is a
|
|
laptop rather than an edge case. */
|
|
@container ps-ops-board (height < 500px) {
|
|
.picker-preview-type--ops .ps-ops-main {
|
|
gap: var(--ps-gap-md);
|
|
}
|
|
|
|
.picker-preview-type--ops .ps-ops-plot {
|
|
height: calc(var(--pt-base) * 2.8);
|
|
}
|
|
|
|
.picker-preview-type--ops .ps-phone-body,
|
|
[data-carry][data-surface="operate"] .ps-phone-body {
|
|
gap: var(--ps-gap-sm);
|
|
}
|
|
|
|
.picker-preview-type--ops .ps-phone-body .ps-ops-plot,
|
|
[data-carry][data-surface="operate"] .ps-phone-body .ps-ops-plot {
|
|
height: calc(var(--pt-base) * 4.4);
|
|
}
|
|
}
|
|
|
|
/* The bottom of the range, where the ladder has floored and the frame is still
|
|
shrinking. The handset is the frame that runs out first, and the plot is the
|
|
block that gives on it for the same reason it gives above: four blocks at
|
|
their own heights and one whose height is a number. */
|
|
@container ps-ops-board (height < 400px) {
|
|
.picker-preview-type--ops .ps-phone-body .ps-ops-plot,
|
|
[data-carry][data-surface="operate"] .ps-phone-body .ps-ops-plot {
|
|
height: calc(var(--pt-base) * 3.2);
|
|
}
|
|
}
|
|
|
|
/* ============================================================
|
|
The Docs board.
|
|
|
|
Drawn from the wireframe on the surface tile: a section rail at the 23.75%
|
|
the drawing sets, and a document beside it. Where it departs is the count
|
|
of headings. The drawing carries one; this board sets a page title and a
|
|
subsection heading together, because the relationship between two heading
|
|
levels is the thing a documentation pair is actually chosen on, and a
|
|
specimen showing one level can never be judged on it.
|
|
|
|
The other departure is the measure. The drawing stops its prose at roughly
|
|
46 characters, which is a picture of a document rather than a document. A
|
|
body face is judged on the texture of a block at a reading measure, so the
|
|
prose is capped in characters instead: the cap is set in the chosen face,
|
|
so a wide-set family takes the column and a narrow one leaves the drawing's
|
|
whitespace of its own accord.
|
|
============================================================ */
|
|
.picker-preview-type--read .ps-desktop {
|
|
grid-template-rows: 9.4% minmax(0, 1fr) 8.3%;
|
|
}
|
|
|
|
/* The board is asked its own height because that is what the document has to
|
|
fill, and it is not the window's: the frame shrinks with the window while
|
|
the type ladder floors, so a short frame holds fewer blocks at the same
|
|
reading size. Everything the fuller page needs hangs off the query below. */
|
|
.picker-preview-type--read {
|
|
container-type: size;
|
|
container-name: ps-docs-board;
|
|
}
|
|
|
|
.ps-docs {
|
|
min-width: 0;
|
|
min-height: 0;
|
|
display: grid;
|
|
grid-template-columns: 23.75% minmax(0, 1fr);
|
|
}
|
|
|
|
.ps-docs-rail {
|
|
min-width: 0;
|
|
display: grid;
|
|
align-content: start;
|
|
gap: var(--ps-gap-sm);
|
|
padding: var(--ps-band-y) calc(var(--pt-base) * 0.6) 0 var(--ps-gutter);
|
|
border-right: 1px solid var(--pvs-rule);
|
|
}
|
|
|
|
.ps-docs-item {
|
|
min-width: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: calc(var(--pt-base) * 0.5);
|
|
}
|
|
|
|
.ps-docs-item > i {
|
|
flex: none;
|
|
width: calc(var(--pt-base) * 0.34);
|
|
aspect-ratio: 1;
|
|
background-color: var(--pvs-bars);
|
|
border-radius: 50%;
|
|
}
|
|
|
|
.picker-preview-type--read .ps-docs-item b {
|
|
min-width: 0;
|
|
color: var(--pvs-bars);
|
|
font-family: var(--pt-body);
|
|
font-size: var(--pt-micro);
|
|
font-weight: 500;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
/* The open section is a recolor, not a plate. A reader is somewhere in a
|
|
document rather than in one of its modes, and a filled plate would say the
|
|
rail is a set of switches. */
|
|
.ps-docs-item--on > i {
|
|
background-color: var(--pvs-accent-c);
|
|
}
|
|
|
|
.picker-preview-type--read .ps-docs-item--on b {
|
|
color: var(--pvs-accent-text);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.ps-docs-main {
|
|
min-width: 0;
|
|
/* Without it the column takes its automatic minimum height from the copy and
|
|
grows past the row, which pushes the footer out of the frame. Held to the
|
|
band, the widest-setting pair runs to the band's edge instead. */
|
|
min-height: 0;
|
|
display: grid;
|
|
align-content: start;
|
|
padding: var(--ps-band-y) 6% 0 calc(var(--pt-base) * 1.6);
|
|
}
|
|
|
|
.picker-preview-type--read .ps-docs-main .pt-headline {
|
|
max-width: 22ch;
|
|
}
|
|
|
|
.picker-preview-type--read :is(.ps-docs-lede, .ps-docs-para) {
|
|
max-width: 70ch;
|
|
margin: 0;
|
|
color: var(--pvs-copy);
|
|
font-family: var(--pt-body);
|
|
font-size: var(--pt-base);
|
|
line-height: 1.65;
|
|
}
|
|
|
|
/* The lede is the one paragraph a documentation page sets apart, and it is set
|
|
apart by weight rather than by size so the block below it keeps the leading
|
|
the family drew for it. */
|
|
.picker-preview-type--read .ps-docs-lede {
|
|
margin-top: calc(var(--pt-base) * 0.9);
|
|
color: var(--pvs-title);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.picker-preview-type--read .ps-docs-para {
|
|
margin-top: calc(var(--pt-base) * 0.7);
|
|
}
|
|
|
|
/* A heading belongs to the passage under it, so that one step stays shorter
|
|
than the step between two passages. Left at the paragraph interval the
|
|
subsection floated between the block above it and the block below. */
|
|
.picker-preview-type--read .ps-docs-sub + .ps-docs-para {
|
|
margin-top: calc(var(--pt-base) * 0.55);
|
|
}
|
|
|
|
.ps-docs-sub {
|
|
margin-top: calc(var(--pt-base) * 1.5);
|
|
color: var(--pvs-title);
|
|
font-family: var(--pt-heading);
|
|
font-size: var(--pt-title);
|
|
font-weight: var(--pt-heading-weight);
|
|
line-height: 1.25;
|
|
overflow-wrap: break-word;
|
|
}
|
|
|
|
/* The marker is the list's own, so it takes the ink the rail's markers take
|
|
rather than a drawn element that would have to be positioned against a line
|
|
whose height changes with the pair. Off until the frame is tall enough to
|
|
hold it, which is what the container query below decides. */
|
|
.picker-preview-type--read .ps-docs-list {
|
|
display: none;
|
|
margin: calc(var(--pt-base) * 1.2) 0 0;
|
|
padding-left: calc(var(--pt-base) * 1.2);
|
|
color: var(--pvs-copy);
|
|
font-family: var(--pt-body);
|
|
font-size: var(--pt-base);
|
|
line-height: 1.55;
|
|
}
|
|
|
|
.picker-preview-type--read .ps-docs-list li {
|
|
max-width: 62ch;
|
|
}
|
|
|
|
.picker-preview-type--read .ps-docs-list li + li {
|
|
margin-top: calc(var(--pt-base) * 0.45);
|
|
}
|
|
|
|
.picker-preview-type--read .ps-docs-list li::marker {
|
|
color: var(--pvs-bars);
|
|
}
|
|
|
|
/* A callout is the one place a documentation page prints body copy on a ground
|
|
that is not the page, which is where a light body weight stops holding.
|
|
|
|
The accent is the box's own border. It was a border plus a round marker sized
|
|
off the type scale, and the marker was a flex item with an auto height, so
|
|
the box it drew was the flex line rather than the circle it was declared as:
|
|
a stretched ellipse whose length changed with the callout, detached from the
|
|
edge by the padding. A border cannot leave its box or fall short of it. */
|
|
.picker-preview-type--read .ps-docs-note {
|
|
margin-top: calc(var(--pt-base) * 1.3);
|
|
padding: calc(var(--pt-base) * 0.7) calc(var(--pt-base) * 0.9);
|
|
background-color: var(--pvs-ghost);
|
|
border-left: calc(var(--pt-base) * 0.25) solid var(--pvs-accent-c);
|
|
}
|
|
|
|
.ps-docs-note-copy {
|
|
min-width: 0;
|
|
display: grid;
|
|
gap: calc(var(--pt-base) * 0.2);
|
|
}
|
|
|
|
.picker-preview-type--read .ps-docs-note-copy b {
|
|
color: var(--pvs-accent-text);
|
|
font-family: var(--pt-body);
|
|
font-size: var(--pt-micro);
|
|
font-weight: 600;
|
|
line-height: 1.4;
|
|
}
|
|
|
|
.picker-preview-type--read .ps-docs-note-copy > span {
|
|
color: var(--pvs-copy);
|
|
font-family: var(--pt-body);
|
|
font-size: var(--pt-micro);
|
|
line-height: 1.5;
|
|
}
|
|
|
|
/* The handset is the same document with the rail folded into a crumb, which is
|
|
how the drawing puts it too. One passage rather than two: the list is what
|
|
the width is spent on instead, and past that the note leaves the card. The
|
|
blocks carry the rhythm, so the gap stays at zero. */
|
|
.picker-preview-type--read .ps-phone-body,
|
|
[data-carry][data-surface="read"] .ps-phone-body {
|
|
grid-template-rows: repeat(6, auto);
|
|
align-content: start;
|
|
gap: 0;
|
|
}
|
|
|
|
.ps-docs-crumb {
|
|
color: var(--pvs-bars);
|
|
font-family: var(--pt-body);
|
|
font-size: var(--pt-micro);
|
|
font-weight: 500;
|
|
text-align: center;
|
|
}
|
|
|
|
.picker-preview-type--read .ps-phone-body .pt-headline {
|
|
margin-top: calc(var(--pt-base) * 1);
|
|
font-size: var(--pt-title);
|
|
}
|
|
|
|
/* The phone re-bases the type ladder smaller, so the step that separates a
|
|
passage from the heading over it has to be restated against the new base or
|
|
the two run together. */
|
|
.picker-preview-type--read .ps-phone-body .ps-docs-sub {
|
|
margin-top: calc(var(--pt-base) * 2.1);
|
|
}
|
|
|
|
/* A frame with room for the whole page. The document runs to the bottom of the
|
|
band instead of stopping partway down it and leaving the footer standing on
|
|
white: the list arrives, the column takes an inset under it, and whatever a
|
|
pair leaves over is spread between the blocks rather than pooled under the
|
|
last one, which is how the landing and portfolio handsets already fill.
|
|
|
|
Below this the band and the reading size have pulled apart far enough that
|
|
the original five blocks are the page: the type ladder floors on a small
|
|
window while the frame keeps shrinking, so there is no slack to spread and
|
|
the sixth block would run into the footer. */
|
|
@container ps-docs-board (height >= 545px) {
|
|
.picker-preview-type--read .ps-docs-main {
|
|
align-content: space-between;
|
|
padding-bottom: var(--ps-band-y);
|
|
}
|
|
|
|
.picker-preview-type--read .ps-docs-list {
|
|
display: block;
|
|
}
|
|
|
|
.picker-preview-type--read .ps-phone-body {
|
|
align-content: space-between;
|
|
}
|
|
}
|
|
|
|
/* Two steps further down, where the handset's five remaining blocks are already
|
|
more than the frame holds. The callout is the block that goes: it is an aside
|
|
to the passage above it rather than part of the document's own ladder, and
|
|
everything above it is either a heading level or the running text the pair is
|
|
judged on. */
|
|
@container ps-docs-board (height < 440px) {
|
|
.picker-preview-type--read .ps-phone-body .ps-docs-note,
|
|
[data-carry][data-surface="read"] .ps-phone-body .ps-docs-note {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
/* ============================================================
|
|
The Portfolio board.
|
|
|
|
The sparsest of the four drawings, so it is the one that changes most. The
|
|
drawing staggers two plates and a carousel rail under them, and it keeps both
|
|
plates: this is also the one surface whose heading face is chosen to be looked
|
|
at rather than read past, so the page title is added above them rather than
|
|
paid for out of the second entry.
|
|
|
|
Both entry rows are an equal share of the band and the plate takes its row,
|
|
which is the construction the handset below already uses. That is what fills
|
|
the height: a plate held to its own ratio left the leftover pooled around one
|
|
centred row, and a band that has to hold whatever a pair's caption comes to
|
|
cannot be filled by a shape sized off its width.
|
|
|
|
The tracked caption meta is deliberate and it is the only tracked label in
|
|
the picker. An `experience` body face is asked to hold up letter-spaced and
|
|
uppercased, and an instruction a specimen cannot test is decoration.
|
|
============================================================ */
|
|
.picker-preview-type--experience .ps-desktop {
|
|
grid-template-rows: 9.4% minmax(0, 1fr) 8.3%;
|
|
}
|
|
|
|
.ps-index {
|
|
min-width: 0;
|
|
min-height: 0;
|
|
display: grid;
|
|
grid-template-rows: auto minmax(0, 1fr) minmax(0, 1fr) auto;
|
|
gap: var(--ps-gap-md);
|
|
padding: var(--ps-band-y) var(--ps-gutter);
|
|
}
|
|
|
|
.picker-preview-type--experience .ps-index > .pt-headline {
|
|
max-width: 20ch;
|
|
}
|
|
|
|
.ps-index-row {
|
|
min-height: 0;
|
|
display: grid;
|
|
grid-template-columns: 42% minmax(0, 1fr);
|
|
column-gap: 5.14%;
|
|
}
|
|
|
|
/* The plate changes sides on the second entry by placement, so both rows stay
|
|
one element order and the caption keeps reading after the work it names. */
|
|
.ps-index-row--flip {
|
|
grid-template-columns: minmax(0, 1fr) 42%;
|
|
}
|
|
|
|
.ps-index-row--flip > .ps-image {
|
|
grid-area: 1 / 2;
|
|
}
|
|
|
|
.ps-index-row--flip > .ps-index-cap {
|
|
grid-area: 1 / 1;
|
|
}
|
|
|
|
.ps-index-cap {
|
|
min-width: 0;
|
|
display: grid;
|
|
align-content: center;
|
|
gap: calc(var(--pt-base) * 0.35);
|
|
}
|
|
|
|
.ps-index-title {
|
|
color: var(--pvs-title);
|
|
font-family: var(--pt-heading);
|
|
font-size: var(--pt-title);
|
|
font-weight: var(--pt-heading-weight);
|
|
line-height: 1.2;
|
|
overflow-wrap: break-word;
|
|
}
|
|
|
|
.picker-preview-type--experience .ps-index-meta {
|
|
color: var(--pvs-bars);
|
|
font-family: var(--pt-body);
|
|
font-size: var(--pt-micro);
|
|
font-weight: 500;
|
|
line-height: 1.4;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.08em;
|
|
}
|
|
|
|
.picker-preview-type--experience .ps-index-note {
|
|
max-width: 46ch;
|
|
margin-top: calc(var(--pt-base) * 0.3);
|
|
color: var(--pvs-copy);
|
|
font-family: var(--pt-body);
|
|
font-size: var(--pt-micro);
|
|
line-height: 1.55;
|
|
}
|
|
|
|
.ps-index-rail {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: calc(var(--pt-base) * 0.9);
|
|
}
|
|
|
|
/* A square stood on its corner, the same construction the drawing uses, so the
|
|
board takes no inline SVG for two marks. */
|
|
.ps-index-arrow {
|
|
flex: none;
|
|
width: calc(var(--pt-base) * 0.42);
|
|
aspect-ratio: 1;
|
|
border-left: 1px solid var(--pvs-bars);
|
|
border-bottom: 1px solid var(--pvs-bars);
|
|
transform: rotate(45deg);
|
|
}
|
|
|
|
.ps-index-arrow--next {
|
|
margin-left: auto;
|
|
border-right: 1px solid var(--pvs-bars);
|
|
border-top: 1px solid var(--pvs-bars);
|
|
border-bottom: 0;
|
|
border-left: 0;
|
|
}
|
|
|
|
.ps-index-track {
|
|
display: flex;
|
|
align-items: baseline;
|
|
gap: calc(var(--pt-base) * 1.1);
|
|
}
|
|
|
|
.picker-preview-type--experience .ps-index-track b {
|
|
color: var(--pvs-bars);
|
|
font-family: var(--pt-body);
|
|
font-size: var(--pt-micro);
|
|
font-weight: 500;
|
|
line-height: 1.4;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.picker-preview-type--experience .ps-index-stop--on {
|
|
color: var(--pvs-title);
|
|
}
|
|
|
|
/* The handset drops the title and the rail and shows the work: two plates,
|
|
each with the caption the desktop row carries. */
|
|
.picker-preview-type--experience .ps-phone-body,
|
|
[data-carry][data-surface="experience"] .ps-phone-body {
|
|
grid-template-rows: minmax(0, 1fr) auto minmax(0, 1fr) auto;
|
|
gap: var(--ps-gap-sm);
|
|
}
|
|
|
|
.picker-preview-type--experience .ps-phone-body .ps-index-cap,
|
|
[data-carry][data-surface="experience"] .ps-phone-body .ps-index-cap {
|
|
padding-top: 0;
|
|
}
|
|
|
|
/* ============================================================
|
|
The structural boards: the four bodies drawn as wireframes.
|
|
|
|
Screens 07 to 10 ask what a page is made of rather than what it is set in, and
|
|
they ask it of each surface the run chose. The three bodies above this were
|
|
built for the font screen, where every slot holds a printed line, and the font
|
|
screen is the only place a slot gets filled. So the same anatomy is drawn a
|
|
second way here: the tree, the class names, and the geometry are the ones
|
|
above, and what changes is that a slot is a bar.
|
|
|
|
Everything below is scoped to [data-carry], the attribute that marks a board
|
|
as one accumulating the structural answers. That is exactly screens 07 to 10,
|
|
so nothing here can reach the color screen's tiles, the font screen's
|
|
specimens, or the motion screen's scenes, all of which draw the same bodies
|
|
under their own terms.
|
|
============================================================ */
|
|
|
|
/* A single body between the chrome, so three bands rather than the landing
|
|
page's five. Declared on the row property rather than through --pvs-rows
|
|
because the layout answer owns that variable for the page it describes, and
|
|
the answer this board is showing must not be able to leave it with two empty
|
|
rows. The same construction the font screen's boards use. */
|
|
[data-carry]:is([data-surface="operate"], [data-surface="read"], [data-surface="experience"]) .ps-desktop {
|
|
grid-template-rows: 9.4% minmax(0, 1fr) 8.3%;
|
|
}
|
|
|
|
/* ── A word, drawn ───────────────────────────────────────────
|
|
One declaration for every slot that holds a line of type upstairs. The height
|
|
ladder underneath is the type ladder read as thicknesses: a figure and a
|
|
heading are the two things on a tool you are meant to see from across a room,
|
|
a row cell and a rail label are the smallest marks the board makes, and a
|
|
running line sits between them. Widths are stated per slot, because a column
|
|
of bars all the same length is a table and not prose. */
|
|
[data-carry] :is(
|
|
.ps-ops-title,
|
|
.ps-ops-item b,
|
|
.ps-ops-metric b,
|
|
.ps-ops-metric span,
|
|
.ps-ops-chart-title,
|
|
.ps-ops-lanes b,
|
|
.ps-ops-row b,
|
|
.ps-ops-panel-title,
|
|
.ps-ops-field b,
|
|
.ps-ops-switch b,
|
|
.ps-docs-item b,
|
|
.ps-docs-sub,
|
|
.ps-docs-crumb,
|
|
.ps-docs-list li,
|
|
.ps-docs-note-copy b,
|
|
.ps-docs-main .pt-headline i,
|
|
.ps-phone-body .pt-headline i,
|
|
.ps-docs-lede i,
|
|
.ps-docs-para i,
|
|
.ps-docs-note-copy > span i
|
|
) {
|
|
display: block;
|
|
background-color: var(--pvs-bars);
|
|
border-radius: var(--pvs-radius-bar);
|
|
}
|
|
|
|
/* The two headings, at the two weights the ladder gives them. */
|
|
[data-carry] :is(.ps-ops-title, .ps-docs-main .pt-headline i, .ps-phone-body .pt-headline i) {
|
|
height: calc(var(--pt-base) * 0.72);
|
|
background-color: var(--pvs-headline);
|
|
}
|
|
|
|
[data-carry] .ps-ops-title {
|
|
width: 38%;
|
|
}
|
|
|
|
[data-carry] :is(.ps-docs-sub, .ps-ops-chart-title, .ps-ops-panel-title) {
|
|
height: calc(var(--pt-base) * 0.5);
|
|
background-color: var(--pvs-title);
|
|
}
|
|
|
|
[data-carry] .ps-docs-sub {
|
|
width: 44%;
|
|
}
|
|
|
|
[data-carry] .ps-ops-chart-title {
|
|
width: 46%;
|
|
}
|
|
|
|
[data-carry] .ps-ops-panel-title {
|
|
width: 62%;
|
|
}
|
|
|
|
/* Running copy: a stack of lines with a short last one, so a passage reads as a
|
|
passage. The headline takes the same treatment at its own weight.
|
|
|
|
Each gap is the line-height the printed version sets, less the thickness of
|
|
the bar, so a stack of n bars stands exactly as tall as the n lines it replaces
|
|
and the block leaves the same room behind it. Guessing the gap instead is what
|
|
made these boards float apart in a band sized for text: a bar is a quarter the
|
|
height of the line it stands in for, and four blocks each short by that much
|
|
is a page of holes. */
|
|
[data-carry] :is(.ps-docs-main, .ps-phone-body) .pt-headline {
|
|
display: grid;
|
|
gap: calc(var(--pt-base) * 1.3);
|
|
margin: 0;
|
|
}
|
|
|
|
[data-carry] :is(.ps-docs-main, .ps-phone-body) .pt-headline i:last-child {
|
|
width: 58%;
|
|
}
|
|
|
|
/* The steps between blocks are the printed board's own, restated because the
|
|
bars replace the elements those margins were declared on. Kept identical so a
|
|
document reads with the same rhythm whether it is set or drawn. */
|
|
[data-carry] :is(.ps-docs-lede, .ps-docs-para) {
|
|
display: grid;
|
|
gap: calc(var(--pt-base) * 1.25);
|
|
}
|
|
|
|
[data-carry] .ps-docs-lede {
|
|
margin: calc(var(--pt-base) * 0.9) 0 0;
|
|
}
|
|
|
|
[data-carry] .ps-docs-para {
|
|
margin: calc(var(--pt-base) * 0.7) 0 0;
|
|
}
|
|
|
|
[data-carry] .ps-docs-sub + .ps-docs-para {
|
|
margin-top: calc(var(--pt-base) * 0.55);
|
|
}
|
|
|
|
[data-carry] .ps-docs-note-copy > span {
|
|
display: grid;
|
|
gap: calc(var(--pt-base) * 0.67);
|
|
margin: 0;
|
|
}
|
|
|
|
[data-carry] :is(.ps-docs-lede, .ps-docs-para) i {
|
|
height: calc(var(--pt-base) * 0.3);
|
|
background-color: var(--pvs-copy);
|
|
}
|
|
|
|
[data-carry] .ps-docs-lede i:last-child,
|
|
[data-carry] .ps-docs-para i:last-child {
|
|
width: 62%;
|
|
}
|
|
|
|
[data-carry] .ps-docs-note-copy > span i {
|
|
height: calc(var(--pt-base) * 0.26);
|
|
}
|
|
|
|
[data-carry] .ps-docs-note-copy > span i:last-child {
|
|
width: 54%;
|
|
}
|
|
|
|
[data-carry] .ps-docs-note-copy b {
|
|
width: 42%;
|
|
height: calc(var(--pt-base) * 0.3);
|
|
background-color: var(--pvs-accent-c);
|
|
}
|
|
|
|
[data-carry] .ps-docs-crumb {
|
|
width: 46%;
|
|
height: calc(var(--pt-base) * 0.26);
|
|
margin-inline: auto;
|
|
}
|
|
|
|
/* A list item is one ragged line against its marker, and the marker is the
|
|
rail's own dot rather than a ::marker, which has no box a bar can stand on. */
|
|
[data-carry] .ps-docs-list {
|
|
display: grid;
|
|
gap: calc(var(--pt-base) * 1.7);
|
|
margin: calc(var(--pt-base) * 1.2) 0 0;
|
|
padding: 0;
|
|
list-style: none;
|
|
}
|
|
|
|
[data-carry] .ps-docs-note {
|
|
margin-top: calc(var(--pt-base) * 1.3);
|
|
}
|
|
|
|
[data-carry] .ps-docs-list li {
|
|
position: relative;
|
|
height: calc(var(--pt-base) * 0.3);
|
|
margin-left: calc(var(--pt-base) * 1.1);
|
|
background-color: var(--pvs-copy);
|
|
}
|
|
|
|
[data-carry] .ps-docs-list li:nth-child(2) {
|
|
width: 82%;
|
|
}
|
|
|
|
[data-carry] .ps-docs-list li::before {
|
|
content: "";
|
|
position: absolute;
|
|
top: 0;
|
|
left: calc(var(--pt-base) * -1.1);
|
|
width: calc(var(--pt-base) * 0.3);
|
|
aspect-ratio: 1;
|
|
background-color: var(--pvs-accent-c);
|
|
border-radius: var(--pvs-radius-dot);
|
|
}
|
|
|
|
/* The tool's own labels. A rail item and a table cell are the smallest marks on
|
|
the board, and their lengths are staggered so neither column reads as a
|
|
ruled form. */
|
|
[data-carry] :is(.ps-ops-item b, .ps-docs-item b, .ps-ops-lanes b, .ps-ops-row b, .ps-ops-field b, .ps-ops-switch b, .ps-ops-metric span) {
|
|
height: calc(var(--pt-base) * 0.26);
|
|
}
|
|
|
|
[data-carry] :is(.ps-ops-item, .ps-docs-item) b {
|
|
flex: 1 1 auto;
|
|
min-width: 0;
|
|
}
|
|
|
|
[data-carry] .ps-ops-item b {
|
|
width: 74%;
|
|
}
|
|
|
|
/* The open plate is a fill, so the label on it takes the ink that fill was paired
|
|
with rather than the page's bar tone, which would be reading a light mark
|
|
against a dark ground and hoping. */
|
|
[data-carry] .ps-ops-item--on b {
|
|
background-color: var(--pvs-cta-ink);
|
|
}
|
|
|
|
[data-carry] .ps-docs-item b {
|
|
width: 72%;
|
|
}
|
|
|
|
[data-carry] .ps-docs-item:nth-of-type(2) b {
|
|
width: 88%;
|
|
}
|
|
|
|
[data-carry] .ps-docs-item:nth-of-type(3) b {
|
|
width: 60%;
|
|
}
|
|
|
|
[data-carry] .ps-docs-item:nth-of-type(4) b {
|
|
width: 80%;
|
|
}
|
|
|
|
[data-carry] .ps-docs-item--on b {
|
|
background-color: var(--pvs-accent-c);
|
|
}
|
|
|
|
[data-carry] .ps-ops-item:nth-of-type(2) b {
|
|
width: 58%;
|
|
}
|
|
|
|
[data-carry] .ps-ops-item:nth-of-type(3) b {
|
|
width: 82%;
|
|
}
|
|
|
|
[data-carry] .ps-ops-metric b {
|
|
width: 56%;
|
|
height: calc(var(--pt-base) * 0.62);
|
|
background-color: var(--pvs-title);
|
|
}
|
|
|
|
[data-carry] .ps-ops-metric span {
|
|
width: 88%;
|
|
}
|
|
|
|
[data-carry] .ps-ops-lanes b {
|
|
width: 68%;
|
|
margin-inline: auto;
|
|
}
|
|
|
|
/* The row's outer tracks are sized by their contents, so a cell that carries no
|
|
words has to state the width the words would have taken. */
|
|
[data-carry] .ps-ops-row b:first-of-type {
|
|
width: calc(var(--pt-base) * 4.4);
|
|
background-color: var(--pvs-title);
|
|
}
|
|
|
|
[data-carry] .ps-ops-row b:nth-of-type(2) {
|
|
width: calc(var(--pt-base) * 3);
|
|
}
|
|
|
|
[data-carry] .ps-ops-row b:last-of-type {
|
|
width: calc(var(--pt-base) * 2.2);
|
|
margin-left: auto;
|
|
}
|
|
|
|
[data-carry] .ps-ops-row--head b {
|
|
background-color: var(--pvs-copy);
|
|
}
|
|
|
|
[data-carry] .ps-ops-field b {
|
|
width: 64%;
|
|
}
|
|
|
|
[data-carry] .ps-ops-switch b {
|
|
width: 56%;
|
|
}
|
|
|
|
[data-carry] .ps-ops-switch:nth-of-type(3) b {
|
|
width: 70%;
|
|
}
|
|
|
|
/* ── The index, drawn ────────────────────────────────────────
|
|
The caption stand-ins the wireframe index already declares. They are styled
|
|
here rather than borrowed from the motion screen, whose copies are scoped to
|
|
its own scenes and carry the fixed pixel sizes those scenes animate. */
|
|
[data-carry] :is(.ps-index-head, .ps-index-name, .ps-index-tag, .ps-index-lines b) {
|
|
display: block;
|
|
background-color: var(--pvs-bars);
|
|
border-radius: var(--pvs-radius-bar);
|
|
}
|
|
|
|
[data-carry] .ps-index-head {
|
|
width: 28%;
|
|
height: calc(var(--pt-base) * 0.72);
|
|
background-color: var(--pvs-headline);
|
|
}
|
|
|
|
[data-carry] .ps-index-name {
|
|
width: 68%;
|
|
height: calc(var(--pt-base) * 0.52);
|
|
background-color: var(--pvs-title);
|
|
}
|
|
|
|
[data-carry] .ps-index-tag {
|
|
width: 34%;
|
|
height: calc(var(--pt-base) * 0.24);
|
|
}
|
|
|
|
[data-carry] .ps-index-lines {
|
|
display: grid;
|
|
gap: calc(var(--pt-base) * 0.7);
|
|
margin-top: calc(var(--pt-base) * 0.3);
|
|
}
|
|
|
|
[data-carry] .ps-index-lines b {
|
|
width: 78%;
|
|
height: calc(var(--pt-base) * 0.26);
|
|
background-color: var(--pvs-copy);
|
|
}
|
|
|
|
[data-carry] .ps-index-lines b:last-child {
|
|
width: 52%;
|
|
}
|
|
|
|
[data-carry] .ps-index-track b {
|
|
width: calc(var(--pt-base) * 1.5);
|
|
height: calc(var(--pt-base) * 0.24);
|
|
background-color: var(--pvs-bars);
|
|
border-radius: var(--pvs-radius-bar);
|
|
}
|
|
|
|
[data-carry] .ps-index-stop--on {
|
|
background-color: var(--pvs-cta);
|
|
}
|
|
|
|
/* ── Shape and lift on the three bodies ──────────────────────
|
|
The corner and depth answers reach the landing page through the slots the
|
|
drawing has always read. These are the same slots on the parts the other
|
|
three bodies are made of, so one answer lands on every board.
|
|
|
|
Which slot a part takes is the part's own kind, not its size: a rail plate
|
|
and a switch are controls, a card and a callout are surfaces, and a bar
|
|
standing in for a line of text is never either, because at control radius it
|
|
stops reading as text and starts reading as a tag. */
|
|
[data-carry] :is(.ps-ops-item, .ps-ops-field, .ps-ops-row) {
|
|
border-radius: var(--pvs-radius-control);
|
|
}
|
|
|
|
[data-carry] :is(.ps-ops-chart, .ps-ops-table, .ps-ops-panel, .ps-ops-rail, .ps-docs-note, .ps-docs-rail, .ps-index-cap) {
|
|
border-radius: var(--pvs-radius-surface);
|
|
}
|
|
|
|
/* A switch is the most control-shaped thing a tool has, so it is where this
|
|
answer reads hardest: fully round under Pill, and squared under Sharp, where a
|
|
drafted page should have no round control left on it. The knob follows the
|
|
track, because a square switch holding a circle is neither answer. Both were
|
|
fixed at 999px, which is the pill answer already given away. */
|
|
[data-carry] :is(.ps-ops-toggle, .ps-ops-toggle::after) {
|
|
border-radius: var(--pvs-radius-control);
|
|
}
|
|
|
|
/* A bar stands on the axis, so only the two corners off it can take a radius.
|
|
Read off the surface slot rather than the control slot: a fully round bar at
|
|
Pill would be a lozenge floating over the baseline it is measured from. */
|
|
[data-carry] .ps-ops-plot > i {
|
|
border-radius: var(--pvs-radius-surface) var(--pvs-radius-surface) 0 0;
|
|
}
|
|
|
|
/* Which of these actually carries a ground is the boundary answer's business, so
|
|
the depth screen withholds the lift where the answer left the box transparent.
|
|
The slots are wired here; the gating is in depth.css beside the answer that
|
|
knows it. */
|
|
[data-carry] :is(.ps-ops-chart, .ps-ops-table, .ps-docs-note) {
|
|
box-shadow: var(--pvs-shadow-card);
|
|
}
|
|
|
|
[data-carry] :is(.ps-ops-item--on, .ps-ops-field, .ps-ops-toggle) {
|
|
box-shadow: var(--pvs-shadow-control);
|
|
}
|
|
|
|
[data-carry] :is(.ps-ops-rail, .ps-ops-panel, .ps-docs-rail) {
|
|
box-shadow: var(--pvs-shadow-surface);
|
|
}
|
|
|
|
/* ── The bands the answer divides ────────────────────────────
|
|
The layout answer's column tracks, per body. Every track is a whole number of
|
|
the field's columns, and the halves added on are the halves of a gutter that
|
|
put a region's dividing line down the middle of a gutter rather than against
|
|
one edge of it: a shell's regions abut and are told apart by a rule, where a
|
|
page's blocks are told apart by air. Defaults are the shape the drawing had
|
|
before the answer was asked, so a board is never left without one. */
|
|
[data-carry] .ps-ops {
|
|
grid-template-columns: var(--pvs-shell-cols, calc(var(--pvs-w2, 15.3333%) + var(--pvs-gut, 1.6%) / 2) calc(var(--pvs-w7, 57.6667%) + var(--pvs-gut, 1.6%)) minmax(0, 1fr));
|
|
column-gap: 0;
|
|
padding-inline: var(--ps-gutter);
|
|
}
|
|
|
|
[data-carry] .ps-docs {
|
|
grid-template-columns: var(--pvs-measure-cols, calc(var(--pvs-w3, 23.8%) + var(--pvs-gut, 1.6%) / 2) minmax(0, 1fr));
|
|
padding-inline: var(--ps-gutter);
|
|
}
|
|
|
|
/* The metrics row is the shell's second reading of the answer: three equal
|
|
thirds under a strict grid, and the lead figure taking more than its
|
|
neighbours once the answer allows an emphasis. */
|
|
[data-carry] .ps-ops-metrics {
|
|
grid-template-columns: var(--pvs-metric-cols, repeat(3, minmax(0, 1fr)));
|
|
}
|
|
|
|
[data-carry] .ps-index-row {
|
|
grid-template-columns: var(--pvs-entry-cols, var(--pvs-w6, 49.2%) minmax(0, 1fr));
|
|
column-gap: var(--pvs-gut, 1.6%);
|
|
}
|
|
|
|
[data-carry] .ps-index-row--flip {
|
|
grid-template-columns: var(--pvs-entry-flip-cols, minmax(0, 1fr) var(--pvs-w6, 49.2%));
|
|
}
|
|
|
|
/* The rail and the panel start on the page's margin like the nav's mark above
|
|
them, so their own inset is spent on the inside edge only and the dividing
|
|
line lands on the column line rather than a text inset past it. */
|
|
[data-carry] .ps-ops-rail {
|
|
padding-left: 0;
|
|
}
|
|
|
|
[data-carry] .ps-ops-panel {
|
|
padding-right: 0;
|
|
}
|
|
|
|
[data-carry] .ps-docs-rail {
|
|
padding-left: 0;
|
|
}
|
|
|
|
/* The measure starts on the next column's own left edge, which is half a gutter
|
|
past the line the rail's rule stands on. A text inset chosen for looks would
|
|
put every line of the document a few points off the grid it is being checked
|
|
against, and this is the one screen where that is the fault on show. */
|
|
[data-carry] .ps-docs-main {
|
|
padding-inline: calc(var(--pvs-gut, 1.6%) / 2) 0;
|
|
}
|
|
|
|
/* ── Filling the band ────────────────────────────────────────
|
|
The trap the docs board hit: a column holding less than its band pools all the
|
|
slack under the last block and leaves the footer standing on white. Bars are
|
|
shorter than the lines they stand in for, so these boards have more slack than
|
|
the specimens do, and every one of them spreads it between the blocks instead.
|
|
|
|
Under the query rather than always, and asked of the board's own height rather
|
|
than the window's, because the ladder floors on a small window while the frame
|
|
keeps shrinking: below this there is no slack to spread and forcing the spread
|
|
would push the last block through the footer. */
|
|
[data-carry][data-surface="read"] {
|
|
container-type: size;
|
|
container-name: ps-docs-board;
|
|
}
|
|
|
|
[data-carry][data-surface="operate"] {
|
|
container-type: size;
|
|
container-name: ps-ops-board;
|
|
}
|
|
|
|
@container ps-docs-board (height >= 300px) {
|
|
[data-carry] .ps-docs-main {
|
|
align-content: space-between;
|
|
padding-bottom: var(--ps-band-y);
|
|
}
|
|
|
|
[data-carry] .ps-phone-body:has(.ps-docs-list) {
|
|
align-content: space-between;
|
|
}
|
|
}
|
|
|
|
/* The tool fills its pane differently from the document, and the difference is
|
|
real rather than a taste: a chart and a table are the two blocks on a
|
|
dashboard that are *given* their height rather than taking it from what is in
|
|
them, so the slack goes into them instead of into the gaps between blocks.
|
|
Spreading the gaps here left a shell whose four blocks floated apart with
|
|
nothing between them, which is the same emptiness the docs fix was chasing. */
|
|
@container ps-ops-board (height >= 300px) {
|
|
[data-carry] .ps-ops-main {
|
|
grid-template-rows: auto auto minmax(0, 1fr) minmax(0, 1fr);
|
|
align-content: stretch;
|
|
padding-bottom: var(--ps-band-y);
|
|
}
|
|
|
|
[data-carry] .ps-ops-chart {
|
|
grid-template-rows: auto minmax(0, 1fr) auto;
|
|
}
|
|
|
|
[data-carry] .ps-ops-plot {
|
|
height: auto;
|
|
min-height: 0;
|
|
}
|
|
|
|
[data-carry] .ps-ops-table {
|
|
align-content: space-between;
|
|
}
|
|
|
|
/* Same reading on the handset: the plot is the block whose height is a count
|
|
rather than its contents, so it is the one that takes up the slack. */
|
|
[data-carry][data-surface="operate"] .ps-phone-body {
|
|
grid-template-rows: auto auto minmax(0, 1fr) auto auto;
|
|
align-content: stretch;
|
|
}
|
|
|
|
[data-carry][data-surface="operate"] .ps-phone-body .ps-ops-plot {
|
|
height: auto;
|
|
min-height: 0;
|
|
}
|
|
}
|
|
|
|
.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-palette-hint.is-warning {
|
|
color: var(--ks-vermilion);
|
|
}
|
|
|
|
.picker-select {
|
|
width: auto;
|
|
}
|
|
|
|
.picker-placeholder {
|
|
display: grid;
|
|
justify-items: start;
|
|
gap: 28px;
|
|
color: var(--ks-text-muted);
|
|
}
|
|
|
|
/* Finish screen: the review copy plus the loader that covers the save and
|
|
the handoff into the design context document. */
|
|
.picker-finish {
|
|
max-width: 560px;
|
|
gap: 18px;
|
|
}
|
|
|
|
.picker-finish-lede {
|
|
margin: 0;
|
|
color: var(--ks-text-muted);
|
|
font-size: 1.05rem;
|
|
line-height: 1.6;
|
|
text-wrap: pretty;
|
|
}
|
|
|
|
.picker-finish-loader {
|
|
width: min(360px, 100%);
|
|
height: 2px;
|
|
margin-top: 10px;
|
|
background: color-mix(in oklab, var(--ks-text) 14%, transparent);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.picker-finish-loader i {
|
|
display: block;
|
|
height: 100%;
|
|
width: 38%;
|
|
background: var(--ks-kinpaku);
|
|
animation: picker-finish-sweep 1.4s var(--ks-ease) infinite;
|
|
}
|
|
|
|
.picker-finish-loader[data-stalled] i {
|
|
animation-play-state: paused;
|
|
opacity: 0.35;
|
|
}
|
|
|
|
@keyframes picker-finish-sweep {
|
|
from { translate: -100% 0; }
|
|
to { translate: 380% 0; }
|
|
}
|
|
|
|
.picker-finish-status {
|
|
margin: 0;
|
|
min-height: 1.6em;
|
|
color: var(--ks-text-faint);
|
|
font-size: 0.84rem;
|
|
letter-spacing: 0.04em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.picker-finish-error {
|
|
margin: 0;
|
|
color: var(--ks-text-muted);
|
|
font-size: 0.94rem;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
/* The stack's own display beats the hidden attribute's UA rule, so the error
|
|
state needs the attribute spelled out. */
|
|
.picker-finish [data-doc-error][hidden] {
|
|
display: none;
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.picker-finish-loader i { animation: none; translate: 0 0; width: 100%; opacity: 0.5; }
|
|
}
|
|
|
|
@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,
|
|
.ps-ops-rail,
|
|
.ps-ops-main,
|
|
.ps-ops-item,
|
|
.ps-ops-chart,
|
|
.ps-ops-table,
|
|
.ps-ops-row,
|
|
.ps-ops-panel,
|
|
.ps-ops-plot > i,
|
|
.ps-docs-rail,
|
|
.ps-docs-note,
|
|
.ps-index-row,
|
|
.ps-index-rail
|
|
),
|
|
.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;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* ============================================================
|
|
Screen 04b: the configure hub. Three packed columns of cards;
|
|
the tall per-surface cards (motion, layout) anchor the outer
|
|
tracks. Sharp corners and hairline rules, the same vocabulary
|
|
as the option rows. Summaries are written by renderHub().
|
|
============================================================ */
|
|
.picker-hub {
|
|
display: grid;
|
|
gap: clamp(28px, 4.5svh, 44px);
|
|
justify-items: center;
|
|
padding-block: clamp(32px, 5svh, 48px) clamp(24px, 3.5svh, 48px);
|
|
}
|
|
|
|
.picker-hub-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
gap: 14px;
|
|
align-items: start;
|
|
width: min(100%, 1080px);
|
|
}
|
|
|
|
.picker-hub-col {
|
|
display: grid;
|
|
gap: 14px;
|
|
align-content: start;
|
|
}
|
|
|
|
.picker-hub-card {
|
|
display: grid;
|
|
gap: 10px;
|
|
width: 100%;
|
|
padding: 20px 24px;
|
|
text-align: left;
|
|
background: var(--ks-lacquer-raised);
|
|
border: 1px solid var(--ks-rule);
|
|
border-radius: 0;
|
|
cursor: pointer;
|
|
transition:
|
|
background-color 180ms var(--ks-ease),
|
|
border-color 180ms var(--ks-ease);
|
|
}
|
|
|
|
.picker-hub-card:hover {
|
|
background: var(--ks-graphite);
|
|
border-color: var(--ks-kinpaku-deep);
|
|
}
|
|
|
|
.picker-hub-card:focus-visible {
|
|
outline: 1px solid var(--ks-kinpaku);
|
|
outline-offset: 3px;
|
|
}
|
|
|
|
.picker-hub-eyebrow {
|
|
color: var(--ks-text-faint);
|
|
font-size: var(--ks-type-eyebrow-size);
|
|
font-weight: 500;
|
|
letter-spacing: var(--ks-type-eyebrow-track);
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.picker-hub-title {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
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-hub-chevron {
|
|
width: 16px;
|
|
height: 16px;
|
|
margin-left: auto;
|
|
flex: 0 0 auto;
|
|
fill: none;
|
|
stroke: currentColor;
|
|
stroke-width: 1.5;
|
|
color: var(--ks-text-faint);
|
|
transition: color 180ms var(--ks-ease), transform 180ms var(--ks-ease);
|
|
}
|
|
|
|
.picker-hub-card:hover .picker-hub-chevron {
|
|
color: var(--ks-kinpaku);
|
|
transform: translateX(2px);
|
|
}
|
|
|
|
.picker-hub-mark {
|
|
padding: 2px 8px;
|
|
border: 1px solid var(--ks-kinpaku-deep);
|
|
color: var(--ks-kinpaku);
|
|
font-size: var(--ks-type-eyebrow-size);
|
|
font-weight: 500;
|
|
letter-spacing: var(--ks-type-eyebrow-track);
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.picker-hub-summary {
|
|
display: grid;
|
|
gap: 6px;
|
|
color: var(--ks-text-muted);
|
|
font-size: 0.86rem;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.picker-hub-line {
|
|
display: grid;
|
|
gap: 2px;
|
|
}
|
|
|
|
.picker-hub-line b {
|
|
color: var(--ks-text-faint);
|
|
font-weight: 500;
|
|
}
|
|
|
|
/* A question no chosen surface takes: present so the menu keeps its
|
|
shape, muted so nobody is invited into a screen the run skips. */
|
|
.picker-hub-card.is-skipped,
|
|
.picker-hub-card.is-skipped:hover {
|
|
background: transparent;
|
|
border-color: var(--ks-rule);
|
|
cursor: not-allowed;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.picker-hub-card.is-skipped .picker-hub-title,
|
|
.picker-hub-card.is-skipped .picker-hub-eyebrow,
|
|
.picker-hub-card.is-skipped .picker-hub-summary,
|
|
.picker-hub-card.is-skipped .picker-hub-chevron {
|
|
color: var(--ks-text-mute-deep);
|
|
}
|
|
|
|
/* The app's one narrow breakpoint. Today the width gate overlays the
|
|
form below 1200px, so this is the collapse the surface owes if that
|
|
gate ever loosens; it costs nothing while it holds. */
|
|
@media (max-width: 1199px) {
|
|
.picker-hub-grid {
|
|
grid-template-columns: minmax(0, 1fr);
|
|
}
|
|
}
|