mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-12 06:06:37 +03:00
update
This commit is contained in:
+105
-5
@@ -7,6 +7,23 @@ const roles = [
|
||||
['tertiary', 'Tertiary'],
|
||||
['neutral', 'Neutral'],
|
||||
];
|
||||
|
||||
// Musical intervals, smallest step first. The ratio is the multiplier between
|
||||
// one size and the next; the description says what that much contrast is for,
|
||||
// since the number alone tells nobody which to pick.
|
||||
const scales = [
|
||||
['minor-second', 1.067, 'Minor Second', 'Almost no contrast. For dense tools where weight and color carry hierarchy instead of size.'],
|
||||
['major-second', 1.125, 'Major Second', 'A quiet step. Long documents and data screens that need many levels close together.'],
|
||||
['minor-third', 1.2, 'Minor Third', 'The workhorse. Clear order without shouting, and it survives on small screens.'],
|
||||
['major-third', 1.25, 'Major Third', 'Confident separation. A safe default when the page has both marketing and interface on it.'],
|
||||
['perfect-fourth', 1.333, 'Perfect Fourth', 'Real jumps between levels. Landing pages where the headline has to lead.'],
|
||||
['augmented-fourth', 1.414, 'Augmented Fourth', 'Sharp contrast. Editorial layouts built from few sizes used decisively.'],
|
||||
['perfect-fifth', 1.5, 'Perfect Fifth', 'Display type against small text, with little in between. Poster thinking.'],
|
||||
['golden-ratio', 1.618, 'Golden Ratio', 'The widest range here. Beautiful with one big statement, awkward with six levels.'],
|
||||
];
|
||||
|
||||
// Exponent above the 16px base: P sits at the base, H1 six steps up.
|
||||
const scaleRows = [['H1', 6], ['H2', 5], ['H3', 4], ['H4', 3], ['H5', 2], ['H6', 1], ['P', 0]];
|
||||
---
|
||||
|
||||
<Picker>
|
||||
@@ -348,10 +365,10 @@ const roles = [
|
||||
|
||||
<div class="picker-type-controls">
|
||||
<div class="picker-type-scroll">
|
||||
<button class="picker-icon-button" type="button" data-font-scroll="-1" aria-label="Scroll font pairs up">
|
||||
<button class="picker-icon-button" type="button" data-list-scroll="-1" aria-label="Scroll font pairs up">
|
||||
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="m4.5 12.5 5.5-5 5.5 5"></path></svg>
|
||||
</button>
|
||||
<button class="picker-icon-button" type="button" data-font-scroll="1" aria-label="Scroll font pairs down">
|
||||
<button class="picker-icon-button" type="button" data-list-scroll="1" aria-label="Scroll font pairs down">
|
||||
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="m4.5 7.5 5.5 5 5.5-5"></path></svg>
|
||||
</button>
|
||||
</div>
|
||||
@@ -465,7 +482,90 @@ const roles = [
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="picker-screen" data-screen="05" data-step="Review" aria-hidden="true" aria-labelledby="picker-finish-copy">
|
||||
<section class="picker-screen" data-screen="05" data-step="Type scale" aria-hidden="true" aria-labelledby="picker-scale-title">
|
||||
<div class="picker-container">
|
||||
<div class="picker-scale">
|
||||
<div class="picker-question-head">
|
||||
<h2 id="picker-scale-title" class="picker-question-title">Choose a type scale</h2>
|
||||
<p class="picker-question-lede">
|
||||
A type scale is the short list of sizes every heading and paragraph is allowed to use, each one a
|
||||
fixed ratio above the last. Setting it here means every screen inherits the same steps, so hierarchy
|
||||
is decided once instead of re-guessed in each component.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="picker-scale-grid">
|
||||
<div class="picker-type-rail">
|
||||
<fieldset class="picker-strategy-options picker-type-options picker-scale-options" data-scale-options>
|
||||
<legend>Type scale</legend>
|
||||
|
||||
{scales.map(([slug, ratio, name, description]) => (
|
||||
<label class="picker-strategy-option">
|
||||
<input
|
||||
type="radio"
|
||||
name="type-scale"
|
||||
value={slug}
|
||||
data-ratio={ratio}
|
||||
data-scale-name={name}
|
||||
checked={slug === 'minor-third'}
|
||||
/>
|
||||
<span class="picker-strategy-copy">
|
||||
<span class="picker-strategy-title">
|
||||
<b class="picker-scale-ratio">{ratio.toFixed(3)}</b>
|
||||
{name}
|
||||
</span>
|
||||
<span class="picker-strategy-desc">{description}</span>
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</fieldset>
|
||||
|
||||
<div class="picker-type-controls">
|
||||
<div class="picker-type-scroll">
|
||||
<button class="picker-icon-button" type="button" data-list-scroll="-1" aria-label="Scroll type scales up">
|
||||
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="m4.5 12.5 5.5-5 5.5 5"></path></svg>
|
||||
</button>
|
||||
<button class="picker-icon-button" type="button" data-list-scroll="1" aria-label="Scroll type scales down">
|
||||
<svg viewBox="0 0 20 20" aria-hidden="true"><path d="m4.5 7.5 5.5 5 5.5-5"></path></svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p class="picker-scale-note">Values at a 16px base, specimen scaled to fit</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="picker-scale-sheet" data-scale-sheet aria-hidden="true">
|
||||
{scaleRows.map(([tag, step]) => (
|
||||
<div class="ts-row" data-scale-row={step}>
|
||||
<p class="ts-sample" data-scale-sample></p>
|
||||
<span class="ts-tag">{tag}</span>
|
||||
<span class="ts-values">
|
||||
<b data-scale-px></b>
|
||||
<i data-scale-rem></i>
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="type-scale-ratio" />
|
||||
|
||||
<div class="picker-actions-stack">
|
||||
<button class="ks-button ks-button-primary" type="button" data-advance="next">
|
||||
Select this scale
|
||||
<span class="ks-button-arrow" aria-hidden="true">
|
||||
<svg viewBox="0 0 16 8" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="square">
|
||||
<path d="M0 4h14M10 0l4 4-4 4"></path>
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
<button class="picker-back" type="button" data-advance="prev">Back</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="picker-screen" data-screen="06" data-step="Review" aria-hidden="true" aria-labelledby="picker-finish-copy">
|
||||
<div class="picker-container">
|
||||
<div class="picker-placeholder">
|
||||
<p id="picker-finish-copy">Your visual direction is ready.</p>
|
||||
@@ -486,10 +586,10 @@ const roles = [
|
||||
|
||||
<div class="picker-progress" aria-hidden="true">
|
||||
<div class="picker-progress-track">
|
||||
<i></i><i></i><i></i><i></i>
|
||||
<i></i><i></i><i></i><i></i><i></i>
|
||||
</div>
|
||||
<p class="picker-progress-label">
|
||||
<span class="picker-progress-index"><b data-progress-index>1</b> / 4</span>
|
||||
<span class="picker-progress-index"><b data-progress-index>1</b> / 5</span>
|
||||
<span class="picker-progress-name" data-progress-name></span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -17,6 +17,8 @@ const strategyPreview = document.querySelector('[data-strategy-preview]');
|
||||
const typePreview = document.querySelector('[data-type-preview]');
|
||||
const fontOptions = document.querySelector('[data-font-options]');
|
||||
const pairTemplate = document.querySelector('[data-pair-card]');
|
||||
const scaleOptions = document.querySelector('[data-scale-options]');
|
||||
const scaleSheet = document.querySelector('[data-scale-sheet]');
|
||||
const states = new Map();
|
||||
const canvases = new WeakMap();
|
||||
let cards = [];
|
||||
@@ -289,9 +291,12 @@ function syncFontPair(pair) {
|
||||
const desktop = typePreview.querySelector('.ps-desktop');
|
||||
const phoneBody = typePreview.querySelector('.ps-phone-body');
|
||||
const phoneFooter = typePreview.querySelector('.ps-phone-footer');
|
||||
typePreview.style.setProperty('--pt-heading', fontStack(pair.heading.family));
|
||||
typePreview.style.setProperty('--pt-body', fontStack(pair.body.family));
|
||||
typePreview.style.setProperty('--pt-heading-weight', pair.heading.weight);
|
||||
// The scale sheet is set in the pair chosen here, so it travels with it.
|
||||
for (const target of [typePreview, scaleSheet]) {
|
||||
target.style.setProperty('--pt-heading', fontStack(pair.heading.family));
|
||||
target.style.setProperty('--pt-body', fontStack(pair.body.family));
|
||||
target.style.setProperty('--pt-heading-weight', pair.heading.weight);
|
||||
}
|
||||
for (const node of document.querySelectorAll('[data-type-brand]')) node.textContent = preview.brand;
|
||||
fillIndexed(desktop, '[data-type-nav]', preview.nav);
|
||||
for (const node of document.querySelectorAll('[data-type-nav-action]')) node.textContent = preview.navAction;
|
||||
@@ -359,26 +364,93 @@ fontOptions.onchange = ({ target }) => {
|
||||
if (pair) syncFontPair(pair);
|
||||
};
|
||||
|
||||
// Scroll by whole cards so a row never ends up half in frame.
|
||||
const scrollButtons = [...document.querySelectorAll('[data-font-scroll]')];
|
||||
for (const button of scrollButtons) {
|
||||
button.onclick = () => {
|
||||
const step = fontOptions.querySelector('.picker-type-option')?.offsetHeight || 100;
|
||||
fontOptions.scrollBy({ top: step * Number(button.dataset.fontScroll), behavior: 'smooth' });
|
||||
/* Scroll by whole rows so an option never ends up half in frame, and disable
|
||||
an arrow at the end it points to, since a live arrow that does nothing is
|
||||
the reason the list looked unscrollable in the first place. */
|
||||
function wireListScroll(list) {
|
||||
const buttons = [...list.closest('.picker-type-rail').querySelectorAll('[data-list-scroll]')];
|
||||
const sync = () => {
|
||||
const room = list.scrollHeight - list.clientHeight;
|
||||
for (const button of buttons) {
|
||||
const down = button.dataset.listScroll === '1';
|
||||
const spent = down ? list.scrollTop >= room - 1 : list.scrollTop <= 1;
|
||||
button.disabled = room < 2 || spent;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function syncScrollButtons() {
|
||||
const room = fontOptions.scrollHeight - fontOptions.clientHeight;
|
||||
for (const button of scrollButtons) {
|
||||
const down = button.dataset.fontScroll === '1';
|
||||
const spent = down ? fontOptions.scrollTop >= room - 1 : fontOptions.scrollTop <= 1;
|
||||
button.disabled = room < 2 || spent;
|
||||
for (const button of buttons) {
|
||||
button.onclick = () => {
|
||||
const step = list.querySelector('.picker-strategy-option')?.offsetHeight || 100;
|
||||
list.scrollBy({ top: step * Number(button.dataset.listScroll), behavior: 'smooth' });
|
||||
};
|
||||
}
|
||||
list.addEventListener('scroll', sync, { passive: true });
|
||||
new ResizeObserver(sync).observe(list);
|
||||
return sync;
|
||||
}
|
||||
|
||||
fontOptions.addEventListener('scroll', syncScrollButtons, { passive: true });
|
||||
new ResizeObserver(syncScrollButtons).observe(fontOptions);
|
||||
const syncScrollButtons = wireListScroll(fontOptions);
|
||||
wireListScroll(scaleOptions);
|
||||
|
||||
/* Type scale.
|
||||
|
||||
The numbers are the real ones: step n is 16px * ratio^n, and a Golden Ratio
|
||||
H1 really is 287px. The specimen cannot be, because 287px of "This is the
|
||||
Golden Ratio scale" is eight times the width of the sheet, and a sheet fitted
|
||||
to that H1 would set its paragraph at 2px.
|
||||
|
||||
So the rendering compresses the exponent by half, which keeps every scale in
|
||||
its own character (a Minor Second sheet still reads as nearly flat, a Golden
|
||||
Ratio one as dramatic) while bringing the range from 18x down to about 4x.
|
||||
The measured fit below then scales the whole sheet if even that overflows.
|
||||
The quoted px and rem stay untouched, which is the point of showing them. */
|
||||
const SCALE_BASE = 16;
|
||||
const SCALE_BODY_PX = 13;
|
||||
const SCALE_COMPRESSION = 0.5;
|
||||
const scaleRows = [...scaleSheet.querySelectorAll('[data-scale-row]')];
|
||||
const scaleRatioInput = document.querySelector('[name="type-scale-ratio"]');
|
||||
|
||||
const trimZeros = (value) => value.replace(/\.0+$/, '').replace(/(\.\d*[1-9])0+$/, '$1');
|
||||
|
||||
/* One pass is exact: every size is linear in the fit factor, and the row boxes
|
||||
the text has to fit inside do not move when it changes. */
|
||||
function fitScaleSheet() {
|
||||
scaleSheet.style.setProperty('--ts-fit', '1');
|
||||
if (!scaleSheet.clientHeight) return;
|
||||
let over = 1;
|
||||
for (const row of scaleRows) {
|
||||
const sample = row.querySelector('[data-scale-sample]');
|
||||
over = Math.max(
|
||||
over,
|
||||
sample.scrollWidth / Math.max(sample.clientWidth, 1),
|
||||
sample.scrollHeight / Math.max(row.clientHeight, 1),
|
||||
);
|
||||
}
|
||||
if (over > 1.001) scaleSheet.style.setProperty('--ts-fit', (1 / over).toFixed(4));
|
||||
}
|
||||
|
||||
function syncTypeScale(input) {
|
||||
const ratio = Number(input.dataset.ratio);
|
||||
const name = input.dataset.scaleName;
|
||||
for (const row of scaleRows) {
|
||||
const step = Number(row.dataset.scaleRow);
|
||||
const px = SCALE_BASE * ratio ** step;
|
||||
row.style.setProperty('--ts-size', (SCALE_BODY_PX * ratio ** (step * SCALE_COMPRESSION)).toFixed(3));
|
||||
row.querySelector('[data-scale-sample]').textContent = `This is the ${name} scale`;
|
||||
row.querySelector('[data-scale-px]').textContent = `${Math.round(px)}px`;
|
||||
row.querySelector('[data-scale-rem]').textContent = `${trimZeros((px / SCALE_BASE).toFixed(2))}rem`;
|
||||
}
|
||||
scaleRatioInput.value = ratio.toFixed(3);
|
||||
fitScaleSheet();
|
||||
}
|
||||
|
||||
scaleOptions.onchange = ({ target }) => {
|
||||
if (target.matches('input[name="type-scale"]')) syncTypeScale(target);
|
||||
};
|
||||
|
||||
new ResizeObserver(fitScaleSheet).observe(scaleSheet);
|
||||
// Every face swap changes the width of the same string, fit included.
|
||||
document.fonts?.addEventListener('loadingdone', fitScaleSheet);
|
||||
syncTypeScale(scaleOptions.querySelector('input:checked'));
|
||||
|
||||
/* Custom fonts. A URL is carried through as-is; an uploaded face is handed to
|
||||
the server, which stores the bytes and returns the path the answers record.
|
||||
@@ -791,8 +863,17 @@ scroller.addEventListener('scroll', () => {
|
||||
}, { passive: true });
|
||||
document.addEventListener('picker:screenchange', (event) => {
|
||||
activate(event.detail.screen === '02');
|
||||
const target = { '03': strategyPreview, '04': typePreview }[event.detail.screen];
|
||||
const target = { '03': strategyPreview, '04': typePreview, '05': scaleSheet }[event.detail.screen];
|
||||
if (target) syncCommittedPalette(target);
|
||||
// A hidden sheet measures zero, so the fit can only be resolved on arrival.
|
||||
// The scroll waits a frame: the screen change focuses the first control after
|
||||
// this event, and that scrolls the list back to the top.
|
||||
if (event.detail.screen === '05') {
|
||||
fitScaleSheet();
|
||||
requestAnimationFrame(() => {
|
||||
scaleOptions.querySelector('input:checked')?.parentElement.scrollIntoView({ block: 'center' });
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
|
||||
+163
-3
@@ -251,7 +251,8 @@ body.picker-page {
|
||||
|
||||
.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="04"]) .picker-hero-art,
|
||||
.picker-shell:has(#picker-form[data-current="05"]) .picker-hero-art {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@@ -269,7 +270,7 @@ body.picker-page {
|
||||
|
||||
.picker-progress-track {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 34px);
|
||||
grid-template-columns: repeat(5, 34px);
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
@@ -282,7 +283,8 @@ body.picker-page {
|
||||
.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 {
|
||||
.picker-progress[data-step="4"] .picker-progress-track i:nth-child(-n + 4),
|
||||
.picker-progress[data-step="5"] .picker-progress-track i {
|
||||
background: var(--ks-kinpaku);
|
||||
}
|
||||
|
||||
@@ -2164,6 +2166,164 @@ body.picker-page {
|
||||
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);
|
||||
}
|
||||
|
||||
.picker-question-lede {
|
||||
max-width: 62ch;
|
||||
color: var(--ks-text-muted);
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.65;
|
||||
text-align: center;
|
||||
text-wrap: pretty;
|
||||
}
|
||||
|
||||
/* Eight intervals and a lede leave less room than screen 04 has, so the sheet
|
||||
is capped harder. The subtraction covers the title, the lede, and the CTA
|
||||
stack, which is what keeps Select this scale above the fold. */
|
||||
.picker-scale-grid {
|
||||
max-height: calc(100svh - 420px);
|
||||
display: grid;
|
||||
grid-template-columns: minmax(340px, 420px) minmax(0, 1fr);
|
||||
gap: 50px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
/* Eight rows scroll rather than share the height four strategy rows can split,
|
||||
so this list takes screen 04's rail and screen 03's copy scale, not screen
|
||||
03's fixed four-row grid. */
|
||||
.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);
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
/* Paper, set in the palette the user committed and the pair they just chose,
|
||||
so the scale is judged in the design it will actually live in. */
|
||||
.picker-scale-sheet {
|
||||
--ts-fit: 1;
|
||||
--pv-neutral: var(--ks-lacquer-raised);
|
||||
--pv-n-ink: var(--ks-champagne);
|
||||
--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: repeat(7, minmax(0, 1fr));
|
||||
padding: clamp(14px, 2.4%, 28px) clamp(18px, 3.2%, 36px);
|
||||
background: var(--pv-neutral);
|
||||
color: var(--pv-n-ink);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ts-row {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 2.6em auto;
|
||||
align-items: center;
|
||||
gap: clamp(12px, 2vw, 24px);
|
||||
border-bottom: 1px dotted color-mix(in oklab, var(--pv-n-ink) 26%, transparent);
|
||||
}
|
||||
|
||||
.ts-row:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.ts-sample {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
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;
|
||||
}
|
||||
|
||||
.ts-row[data-scale-row="0"] .ts-sample {
|
||||
font-family: var(--pt-body);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* The tag and the numbers are chrome, not specimen: they hold one size while
|
||||
everything to their left moves, which is what makes the movement legible. */
|
||||
.ts-tag {
|
||||
color: color-mix(in oklab, var(--pv-n-ink) 62%, transparent);
|
||||
font-family: var(--ks-font);
|
||||
font-size: 0.82rem;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.ts-values {
|
||||
display: grid;
|
||||
justify-items: end;
|
||||
gap: 2px;
|
||||
font-family: var(--ks-mono);
|
||||
font-size: 0.68rem;
|
||||
font-variant-numeric: tabular-nums;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.ts-values b {
|
||||
color: color-mix(in oklab, var(--pv-n-ink) 88%, transparent);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.ts-values i {
|
||||
color: color-mix(in oklab, var(--pv-n-ink) 52%, transparent);
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
.picker-type-scroll {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
Reference in New Issue
Block a user