Rework light theme, add format wheel and motion, fix Firefox paths

Theme: light is a dimmer brand-tinted stone with deeper shade, so it reads
as the same material as dark; every neutral now carries --nv-brand in both
appearances. Native controls (accent-color, caret-color) take the brand.

Time picker: 24-hour by default; data-nv-format-toggle adds a third wheel
of 24H / AM / PM. Swiping it rebuilds the hour wheel for the new format.
The selection lens is a raised glass bar, not a flat band.

Motion: drum effect on the wheels as a scroll-driven animation, with a
script fallback for engines without it (Firefox); rubber-band at both ends
of a wheel that springs back; calendar months slide locally instead of via
a page-wide transition; a chosen day springs into place.

Firefox: wheel notches arrive in lines, not pixels, which made the
rubber-band invisible; deltaMode is now normalised. The selected-day pop
no longer plays on page load.

Also: hover only inside @media (hover: hover) on the slider, the Glaze
inner glow is a token, and the focus note in GUIDELINES.md is corrected
(popovers do not trap focus). SPDX identifier added to niveus.js.

check.py, build.py and build.py --check pass.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Nexoniarz 2026-10-28 09:34:08 +01:00
commit 5d435c4f49
12 changed files with 410 additions and 157 deletions

View file

@ -134,6 +134,14 @@ textarea {
border-radius: var(--nv-radius-xs);
}
/* Whatever the browser draws itself — a bare checkbox, a progress bar, the
text caret, a date field's glyphs — takes the brand, so no native colour
survives next to the system's own. */
:root {
accent-color: var(--nv-color-accent);
caret-color: var(--nv-color-accent);
}
::selection {
background-color: var(--nv-color-selection);
color: var(--nv-color-text-primary);

View file

@ -93,8 +93,12 @@
}
@media (hover: hover) {
/* Hover presses the day slightly into the surface; it is never a flat wash
of colour, because the chosen day is a raised key and the two must read as
the same material. */
.nv-calendar__day:hover:not(:disabled, [aria-selected="true"]) {
background-color: var(--nv-color-surface-hover);
box-shadow: var(--nv-elevation-inset-sm), var(--nv-ledge);
}
}
@ -105,7 +109,7 @@
/* Today is ringed, not filled: filled means chosen. */
.nv-calendar__day[aria-current="date"] {
color: var(--nv-color-accent-text);
box-shadow: inset 0 0 0 var(--nv-border-width-strong) var(--nv-color-accent);
box-shadow: inset 0 0 0 var(--nv-border-width-strong) var(--nv-color-accent), var(--nv-elevation-inset-sm);
}
/* The chosen day is a small raised key, lit like the filled buttons. */
@ -129,3 +133,14 @@
color: var(--nv-color-text-disabled);
cursor: not-allowed;
}
/* A day pressed into place springs up to full size. Only when it was just
chosen: a selection that is merely *there* on load stays still. */
.nv-calendar__day[aria-selected="true"][data-nv-pop] {
animation: nv-day-pop var(--nv-duration-quick) var(--nv-ease-spring);
}
@keyframes nv-day-pop {
from { scale: 0.72; }
to { scale: 1; }
}

View file

@ -75,7 +75,7 @@
box-shadow: var(--nv-outline), var(--nv-elevation-2);
appearance: none;
-webkit-appearance: none;
transition: scale var(--nv-duration-fast) var(--nv-ease-standard);
transition: scale var(--nv-duration-quick) var(--nv-ease-spring);
}
.nv-slider::-moz-range-thumb {
@ -87,12 +87,17 @@
border: 0;
border-radius: var(--nv-radius-full);
box-shadow: var(--nv-outline), var(--nv-elevation-2);
transition: scale var(--nv-duration-fast) var(--nv-ease-standard);
transition: scale var(--nv-duration-quick) var(--nv-ease-spring);
}
/* Hover only where there is a pointer to hover with; touch would leave the
thumb enlarged after a tap. */
@media (hover: hover) {
.nv-slider:hover::-webkit-slider-thumb { scale: 1.06; }
.nv-slider:hover::-moz-range-thumb { scale: 1.06; }
}
.nv-slider:hover::-webkit-slider-thumb { scale: 1.06; }
.nv-slider:active::-webkit-slider-thumb { scale: 0.96; }
.nv-slider:hover::-moz-range-thumb { scale: 1.06; }
.nv-slider:active::-moz-range-thumb { scale: 0.96; }
.nv-slider:focus-visible::-webkit-slider-thumb {

View file

@ -99,7 +99,7 @@
border: var(--nv-border-width) solid transparent;
box-shadow:
var(--nv-highlight-top-strong),
inset 0 0 1.5rem -0.5rem var(--nv-color-glaze-specular),
var(--nv-glaze-glow),
var(--nv-outline),
var(--nv-elevation-4);
backdrop-filter:

View file

@ -15,9 +15,9 @@
Glaze with something moving behind it.
The format is 24-hour unless told otherwise. With data-nv-format-toggle a
24H / AM/PM switch sits under the wheels and adds an AM/PM wheel when chosen;
the value stays 24-hour either way, so a form never has to know which the
person used.
third wheel reads 24H / AM / PM: swipe it to AM or PM and the hour wheel
becomes 1–12; swipe back to 24H and it returns to 0–23. The value stays
24-hour either way, so a form never has to know which the person used.
Without niveus.js the element is empty. For a no-script fallback, use a
native <input type="time" class="nv-input">.
@ -31,7 +31,7 @@
flex-direction: column;
align-items: center;
gap: var(--nv-space-2);
padding: var(--nv-space-2) var(--nv-space-3) var(--nv-space-3);
padding: var(--nv-space-2) var(--nv-space-3);
border-radius: var(--nv-radius-xl);
color: var(--nv-color-text-primary);
}
@ -42,8 +42,10 @@
gap: var(--nv-space-1);
}
/* The lens. It belongs to the wheels, not the picker, so the switch beneath
them never moves the centre. */
/* The lens: a bar of tinted glass lying across the centre row. It is raised
like the keys elsewhere in the system — lit on top, shaded below, with a
contact line — not a flat band of colour, so it reads as a thing the wheels
pass under. */
.nv-timepicker__wheels::before {
content: "";
position: absolute;
@ -52,8 +54,9 @@
block-size: var(--_row);
translate: 0 -50%;
background-color: var(--nv-color-selection);
background-image: var(--nv-sheen-raised);
border-radius: var(--nv-radius-md);
box-shadow: var(--nv-highlight-top);
box-shadow: var(--nv-bevel), var(--nv-outline), var(--nv-elevation-1);
pointer-events: none;
}
@ -74,6 +77,9 @@
currentcolor 72%,
transparent
);
/* Depth for the drum effect below: rows turn away from the viewer as they
leave the centre. */
perspective: 14rem;
}
.nv-timepicker__column::-webkit-scrollbar {
@ -96,28 +102,43 @@
font-size: var(--nv-font-size-lg);
font-variant-numeric: tabular-nums;
cursor: pointer;
/* --nv-pull is how far the wheel has been dragged past its end. Letting go
removes it, and the spring easing carries the rows back with a small
overshoot — the way a phone's list gives at its limit and settles. */
translate: 0 var(--nv-pull, 0);
transition:
var(--nv-transition-colors),
translate var(--nv-duration-slower) var(--nv-ease-spring);
}
.nv-timepicker__column[data-nv-pulling] .nv-timepicker__option {
transition: var(--nv-transition-colors);
}
/* A drum: each row shrinks, fades and tilts away in proportion to how far it
is from the lens. Driven by the scroll position itself, so it needs no
script and tracks a finger exactly. Where scroll-driven animation is missing
the rows are simply flat, which is still correct. */
@supports (animation-timeline: view()) {
@media (prefers-reduced-motion: no-preference) {
.nv-timepicker__option {
animation: nv-wheel-drum linear both;
animation-timeline: view(block);
}
}
}
@keyframes nv-wheel-drum {
0% { scale: 0.78; opacity: 0.25; rotate: x 52deg; }
50% { scale: 1; opacity: 1; rotate: x 0deg; }
100% { scale: 0.78; opacity: 0.25; rotate: x -52deg; }
}
.nv-timepicker__option[aria-selected="true"] {
color: var(--nv-color-text-primary);
font-weight: var(--nv-font-weight-semibold);
}
/* 24H / AM/PM. A button group under the wheels; the pressed one is the format
in use. 24-hour is the default, so it starts pressed. */
.nv-timepicker__format {
align-self: center;
}
/* Pressed reads as pushed in: the format in use is a key held down. */
.nv-timepicker__format .nv-button[aria-pressed="true"] {
background-color: var(--nv-color-accent-subtle);
background-image: none;
color: var(--nv-color-accent-text);
box-shadow: var(--nv-elevation-inset-sm), var(--nv-ledge);
}
.nv-timepicker__separator {
display: grid;
place-items: center;

View file

@ -1,3 +1,4 @@
/* SPDX-License-Identifier: Apache-2.0 */
/* ==========================================================================
Niveus — continuity helpers
--------------------------------------------------------------------------
@ -492,6 +493,8 @@
grid.setAttribute("aria-labelledby", title.id);
}
var pop = false;
function render() {
if (title) title.textContent = view.toLocaleDateString(locale, { month: "long", year: "numeric" });
grid.textContent = "";
@ -525,6 +528,7 @@
}
var iso = isoDate(cursor);
var day = document.createElement("button");
if (pop && iso === selectedIso) day.dataset.nvPop = "";
day.type = "button";
day.className = "nv-calendar__day";
day.setAttribute("role", "gridcell");
@ -558,10 +562,36 @@
var update = function () {
view = new Date(date.getFullYear(), date.getMonth(), 1);
render();
pop = false;
if (focus) focusActive();
};
if (monthChanged) transition(update, { direction: direction });
else update();
update();
if (monthChanged) slide(direction);
}
// The month slides in from the side it lies on, with the easing and
// duration the rest of the system uses, so it collapses with reduced
// motion like everything else. Only the grid moves; the page does not.
function slide(direction) {
if (prefersReducedMotion() || !grid.animate) return;
var root = getComputedStyle(document.documentElement);
var duration = parseFloat(root.getPropertyValue("--nv-duration-expand"));
if (!duration) return;
var easing = root.getPropertyValue("--nv-ease-emphasized").trim() || "ease-out";
var shift = (direction === "forward" ? 1 : -1) * 28;
grid.animate(
[
{ opacity: 0, translate: shift + "px 0", filter: "blur(3px)" },
{ opacity: 1, translate: "0 0", filter: "blur(0)" },
],
{ duration: duration, easing: easing }
);
if (title) {
title.animate(
[{ opacity: 0, translate: shift / 2 + "px 0" }, { opacity: 1, translate: "0 0" }],
{ duration: duration, easing: easing }
);
}
}
function stepMonth(count) {
@ -574,14 +604,18 @@
function choose(button) {
selected = parseDate(button.dataset.date);
active = selected;
pop = true;
calendar.dataset.nvValue = button.dataset.date;
if (button.dataset.outside !== undefined) {
show(selected, selected > view ? "forward" : "backward", true);
} else {
grid.querySelectorAll('[aria-selected="true"]').forEach(function (other) {
other.removeAttribute("aria-selected");
delete other.dataset.nvPop;
});
button.setAttribute("aria-selected", "true");
button.dataset.nvPop = "";
pop = false;
}
calendar.dispatchEvent(new CustomEvent("nv:date", {
bubbles: true,
@ -632,7 +666,8 @@
* Builds and drives one .nv-timepicker from data-*: nv-value (HH:MM, 24h),
* nv-hours ("24", the default, or "12" for an AM/PM wheel), nv-step (minute
* interval), nv-name (adds a hidden input for forms) and nv-format-toggle
* (adds a 24H / AM/PM switch under the wheels). The value is always 24-hour,
* (adds a third wheel of 24H / AM / PM: swiping it changes the format). The
* value is always 24-hour,
* whatever the wheels show. Fires `nv:time` with { value } as the wheels
* settle on a new row.
*/
@ -652,9 +687,10 @@
var wheels = document.createElement("div");
wheels.className = "nv-timepicker__wheels";
var hourColumn, minuteColumn, periodColumn;
var hourColumn, minuteColumn, periodColumn, formatColumn;
var field = null;
var formatButtons = [];
var modeTimer = 0;
var withFormat = picker.hasAttribute("data-nv-format-toggle");
var placed = false;
var last = "";
var frame = 0;
@ -680,10 +716,22 @@
var hi = indexOf(hourColumn);
var mi = indexOf(minuteColumn);
if (hi < 0 || mi < 0) return;
var pm = periodColumn ? indexOf(periodColumn) === 1 : false;
var fi = formatColumn ? indexOf(formatColumn) : -1;
if (formatColumn && fi < 0) return;
// The format wheel disagrees with the hour wheel: a new format was
// swiped to. Wait for it to settle, then rebuild the hours for it.
if (formatColumn && (fi > 0) !== twelve) {
global.clearTimeout(modeTimer);
modeTimer = global.setTimeout(switchFormat, 160);
return;
}
var pm = formatColumn ? fi === 2 : periodColumn ? indexOf(periodColumn) === 1 : false;
mark(hourColumn, hi);
mark(minuteColumn, mi);
if (periodColumn) mark(periodColumn, pm ? 1 : 0);
if (formatColumn) mark(formatColumn, fi);
else if (periodColumn) mark(periodColumn, pm ? 1 : 0);
var hour = twelve ? ((hi + 1) % 12) + (pm ? 12 : 0) : hi;
var value = pad(hour) + ":" + minuteItems[mi];
@ -708,10 +756,24 @@
var mi = Math.round(minute / step) % minuteItems.length;
scrollTo(hourColumn, hi, false);
scrollTo(minuteColumn, mi, false);
if (periodColumn) scrollTo(periodColumn, h24 >= 12 ? 1 : 0, false);
if (formatColumn) scrollTo(formatColumn, twelve ? (h24 >= 12 ? 2 : 1) : 0, false);
else if (periodColumn) scrollTo(periodColumn, h24 >= 12 ? 1 : 0, false);
read();
}
// Going to 12-hour takes the half of the day that was swiped to; going back
// to 24-hour keeps the moment, which the hour wheel has been tracking.
function switchFormat() {
var fi = indexOf(formatColumn);
if (fi < 0) return;
var wants = fi > 0;
if (wants === twelve) { read(); return; }
if (wants) h24 = (h24 % 12) + (fi === 2 ? 12 : 0);
twelve = wants;
picker.dataset.nvHours = twelve ? "12" : "24";
build();
}
function buildColumn(label, items) {
var list = document.createElement("ul");
list.className = "nv-timepicker__column";
@ -726,9 +788,51 @@
list.appendChild(option);
});
// Past either end the wheel gives, as a phone's list does, then springs
// back. Touch is left to the platform (iOS bounces, Android stretches);
// overscroll-behavior keeps that inside the wheel. Mouse wheels and
// trackpads, which have no such effect on a nested scroller, are handled
// here. The distance has resistance: the further it is pulled, the less
// each notch moves it.
var pull = 0;
var settle = 0;
var limit = 36;
function setPull(value) {
pull = value;
list.style.setProperty("--nv-pull", value.toFixed(1) + "px");
list.dataset.nvPulling = "";
global.clearTimeout(settle);
settle = global.setTimeout(function () {
pull = 0;
delete list.dataset.nvPulling;
list.style.removeProperty("--nv-pull");
}, 90);
}
list.addEventListener("wheel", function (event) {
if (prefersReducedMotion()) return;
var atTop = list.scrollTop <= 0;
var atBottom = list.scrollTop >= list.scrollHeight - list.clientHeight - 1;
// Chromium reports a wheel notch in pixels (~100); Firefox reports it
// in lines (~3). Both are brought to pixels, or the pull would be
// invisible in one of them.
var delta = event.deltaY * (event.deltaMode === 1 ? 16 : event.deltaMode === 2 ? list.clientHeight : 1);
var pushing = (atTop && delta < 0) || (atBottom && delta > 0);
if (!pushing && !pull) return;
event.preventDefault();
var next = pull - delta * 0.12 * (1 - Math.abs(pull) / limit);
setPull(Math.max(-limit, Math.min(limit, next)));
}, { passive: false });
// A key pressed at the end nudges the wheel instead of doing nothing.
list.nvNudge = function (direction) {
if (!prefersReducedMotion()) setPull(direction * 12);
};
list.addEventListener("scroll", function () {
if (frame) return;
frame = global.requestAnimationFrame(function () { frame = 0; read(); });
frame = global.requestAnimationFrame(function () { frame = 0; read(); drum(list); });
}, { passive: true });
list.addEventListener("click", function (event) {
@ -750,11 +854,41 @@
default: return;
}
event.preventDefault();
scrollTo(list, Math.max(0, Math.min(end, to)), true);
var clamped = Math.max(0, Math.min(end, to));
if (clamped === index) list.nvNudge(to < index ? 1 : -1);
scrollTo(list, clamped, true);
});
return list;
}
// The drum effect is a CSS scroll-driven animation (see timepicker.css).
// Where that does not exist — Firefox — the same curve is applied from the
// scroll position here, so the wheels look the same in both. Only the few
// rows near the lens are touched.
var scrollDriven = global.CSS && global.CSS.supports && global.CSS.supports("animation-timeline", "view()");
function drum(list) {
if (scrollDriven || prefersReducedMotion()) return;
var first = list.firstElementChild;
var row = first ? first.offsetHeight : 0;
if (!row) return;
var centre = list.scrollTop / row;
var reach = list.clientHeight + row;
for (var i = 0; i < list.children.length; i++) {
var away = i - centre;
var option = list.children[i];
if (Math.abs(away) > 3) {
option.style.opacity = "0.25";
continue;
}
// 1 at the lens, 0 at the edge of the wheel; the CSS keyframes, spelt out.
var near = 1 - Math.min(1, Math.abs(away) * row / reach * 2);
option.style.scale = String(0.78 + 0.22 * near);
option.style.opacity = String(0.25 + 0.75 * near);
option.style.rotate = "x " + ((away < 0 ? 1 : -1) * 52 * (1 - near)).toFixed(1) + "deg";
}
}
function colon() {
var mark = document.createElement("span");
mark.className = "nv-timepicker__separator";
@ -773,17 +907,19 @@
wheels.textContent = "";
hourColumn = buildColumn("Hours", hourItems);
minuteColumn = buildColumn("Minutes", minuteItems);
periodColumn = twelve ? buildColumn("AM or PM", ["AM", "PM"]) : null;
formatColumn = withFormat ? buildColumn("Time format", ["24H", "AM", "PM"]) : null;
periodColumn = twelve && !withFormat ? buildColumn("AM or PM", ["AM", "PM"]) : null;
wheels.appendChild(hourColumn);
wheels.appendChild(colon());
wheels.appendChild(minuteColumn);
if (periodColumn) wheels.appendChild(periodColumn);
if (formatColumn) wheels.appendChild(formatColumn);
else if (periodColumn) wheels.appendChild(periodColumn);
formatButtons.forEach(function (button) {
button.setAttribute("aria-pressed", String((button.dataset.hours === "12") === twelve));
});
placed = false;
place();
[hourColumn, minuteColumn, formatColumn || periodColumn].forEach(function (list) {
if (list) drum(list);
});
}
picker.textContent = "";
@ -793,30 +929,6 @@
}
picker.appendChild(wheels);
if (picker.hasAttribute("data-nv-format-toggle")) {
var format = document.createElement("div");
format.className = "nv-timepicker__format nv-button-group";
format.setAttribute("role", "group");
format.setAttribute("aria-label", "Time format");
[["24", "24H"], ["12", "AM/PM"]].forEach(function (option) {
var button = document.createElement("button");
button.type = "button";
button.className = "nv-button nv-button--sm";
button.dataset.hours = option[0];
button.textContent = option[1];
button.addEventListener("click", function () {
var wants = option[0] === "12";
if (wants === twelve) return;
twelve = wants;
picker.dataset.nvHours = option[0];
build();
});
formatButtons.push(button);
format.appendChild(button);
});
picker.appendChild(format);
}
if (picker.dataset.nvName) {
field = document.createElement("input");
field.type = "hidden";

View file

@ -32,16 +32,18 @@
:root,
[data-nv-brand] {
/* --- Canvas and surfaces -----------------------------------------------
Paper stays paper: the two surfaces that hold content are left untinted so
text keeps its contrast. Everything the surfaces sit *in* picks up a trace
of the brand, which is what stops an accent from looking stuck on. */
--nv-light-background: color-mix(in oklab, #e8e5de, var(--nv-brand) var(--nv-brand-tint));
--nv-light-background-subtle: color-mix(in oklab, #eeece6, var(--nv-brand) var(--nv-brand-tint));
--nv-light-surface: #fbfbfa;
--nv-light-surface-elevated: #f8f8f6;
--nv-light-surface-inset: color-mix(in oklab, #eeede8, var(--nv-brand) var(--nv-brand-tint));
--nv-light-surface-hover: color-mix(in oklab, #f1f0ec, var(--nv-brand) var(--nv-brand-tint));
--nv-light-surface-active: color-mix(in oklab, #e6e5e0, var(--nv-brand) var(--nv-brand-tint));
Every neutral carries a trace of the brand, content surfaces included, the
way dark does: that is what stops an accent from looking stuck on, and what
makes the two appearances read as one material. The paper is a soft
grey-stone, not white — a bright field glares, and flattens the bevels that
are meant to shape it. Text keeps its contrast: the primary is neutral. */
--nv-light-background: color-mix(in oklab, #d8d5ce, var(--nv-brand) calc(var(--nv-brand-tint) * 2));
--nv-light-background-subtle: color-mix(in oklab, #dedbd4, var(--nv-brand) calc(var(--nv-brand-tint) * 2));
--nv-light-surface: color-mix(in oklab, #eeede9, var(--nv-brand) var(--nv-brand-tint));
--nv-light-surface-elevated: color-mix(in oklab, #f2f1ed, var(--nv-brand) var(--nv-brand-tint));
--nv-light-surface-inset: color-mix(in oklab, #dddbd4, var(--nv-brand) calc(var(--nv-brand-tint) * 2));
--nv-light-surface-hover: color-mix(in oklab, #e6e4de, var(--nv-brand) var(--nv-brand-tint));
--nv-light-surface-active: color-mix(in oklab, #dad8d1, var(--nv-brand) var(--nv-brand-tint));
--nv-light-scrim: rgb(28 24 18 / 0.34);
/* --- Text ---------------------------------------------------------------
@ -50,7 +52,7 @@
loss of legibility. */
--nv-light-text-primary: var(--nv-palette-neutral-900);
--nv-light-text-secondary: color-mix(in oklab, var(--nv-palette-neutral-600), var(--nv-brand) 8%);
--nv-light-text-tertiary: color-mix(in oklab, var(--nv-palette-neutral-500), var(--nv-brand) 8%);
--nv-light-text-tertiary: color-mix(in oklab, color-mix(in oklab, var(--nv-palette-neutral-500), var(--nv-palette-neutral-600) 40%), var(--nv-brand) 8%);
--nv-light-text-disabled: var(--nv-palette-neutral-400);
--nv-light-text-inverted: var(--nv-palette-neutral-0);
@ -135,20 +137,20 @@
/* --- Depth and material -------------------------------------------------
Three shadow colors build every elevation level: an ambient contact
shadow, a soft penumbra, and a deeper umbra for high elevations. */
--nv-light-shadow-ambient: rgb(40 32 20 / 0.08);
--nv-light-shadow-penumbra: rgb(40 32 20 / 0.1);
--nv-light-shadow-umbra: rgb(40 32 20 / 0.16);
--nv-light-shadow-inset: rgb(40 32 20 / 0.14);
--nv-light-highlight: rgb(255 255 255 / 0.9);
--nv-light-highlight-strong: rgb(255 255 255 / 1);
--nv-light-shadow-ambient: rgb(32 28 22 / 0.12);
--nv-light-shadow-penumbra: rgb(32 28 22 / 0.15);
--nv-light-shadow-umbra: rgb(32 28 22 / 0.24);
--nv-light-shadow-inset: rgb(32 28 22 / 0.2);
--nv-light-highlight: rgb(255 255 255 / 0.7);
--nv-light-highlight-strong: rgb(255 255 255 / 0.85);
/* Paper already has a border doing the contact work, so the outline only
softens it; the bottom shade is faint for the same reason. */
--nv-light-lowlight: rgb(40 32 20 / 0.09);
--nv-light-outline: rgb(40 32 20 / 0.05);
--nv-light-lowlight: rgb(32 28 22 / 0.14);
--nv-light-outline: rgb(32 28 22 / 0.08);
--nv-light-sheen-top: rgb(255 255 255 / 0.9);
--nv-light-sheen-bottom: rgb(40 32 20 / 0.07);
--nv-light-sheen-top: rgb(255 255 255 / 0.55);
--nv-light-sheen-bottom: rgb(32 28 22 / 0.11);
--nv-light-sheen-strong-top: rgb(255 255 255 / 0.32);
--nv-light-sheen-strong-bottom: rgb(20 16 10 / 0.16);
--nv-light-glow: oklch(from var(--nv-light-accent) l c h / 0.3);
@ -164,16 +166,16 @@
--nv-light-elevation-tint-4: transparent;
--nv-light-elevation-tint-5: transparent;
--nv-light-material-frosted: rgb(252 252 250 / 0.56);
--nv-light-material-glass: rgb(252 252 250 / 0.4);
--nv-light-material-border: rgb(255 255 255 / 0.72);
--nv-light-material-frosted: color-mix(in oklab, rgb(228 227 222 / 0.6), var(--nv-brand) var(--nv-brand-tint));
--nv-light-material-glass: color-mix(in oklab, rgb(228 227 222 / 0.44), var(--nv-brand) var(--nv-brand-tint));
--nv-light-material-border: rgb(255 255 255 / 0.5);
/* Glaze: the tint it is made of, the rim that catches light on the side
facing the pointer, the rim on the far side, and the specular bloom. */
--nv-light-material-glaze: rgb(255 255 255 / 1);
--nv-light-glaze-rim-lit: rgb(255 255 255 / 1);
--nv-light-glaze-rim-far: rgb(255 255 255 / 0.5);
--nv-light-glaze-specular: rgb(255 255 255 / 0.6);
--nv-light-material-glaze: color-mix(in oklab, rgb(232 231 226 / 1), var(--nv-brand) calc(var(--nv-brand-tint) * 2));
--nv-light-glaze-rim-lit: rgb(255 255 255 / 0.9);
--nv-light-glaze-rim-far: rgb(255 255 255 / 0.4);
--nv-light-glaze-specular: rgb(255 255 255 / 0.4);
/* --- Window controls --------------------------------------------------------- */
--nv-light-window-close: var(--nv-palette-window-close);

View file

@ -54,6 +54,9 @@
--nv-glaze-tint-min: 4%; /* tint opacity at frost 0 */
--nv-glaze-tint-max: 78%; /* tint opacity at frost 1 */
--nv-glaze-rim: 1.5px;
/* The light a pane holds just inside its edge, so it reads as a thick lens
rather than a flat film. */
--nv-glaze-glow: inset 0 0 1.5rem -0.5rem var(--nv-color-glaze-specular);
/* --- Sheen ------------------------------------------------------------------
raised neutral controls: buttons, the selected tab, a titlebar