/* ── Neumorphism UI kit tokens (uiCookies) — identical in every set ──────────────
Every component is moulded from --neu-bg and lit from the top left. Change a token and
the whole kit follows; the shadow recipes below are built from these values. */
:root{
--neu-bg:#e0e5ec; /* the one surface colour */
--neu-light:#ffffff; /* highlight, top left */
--neu-dark:#a3b1c6; /* shade, bottom right */
--neu-distance:6px; /* how far a shape rises */
--neu-blur:14px; /* how soft its shadow is */
--neu-radius:18px; /* cards; buttons use .75 of it, fields .6 */
--neu-accent:#4f46e5; /* focus ring and on / selected / current states, 4.9:1 on --neu-bg */
--neu-on-accent:#ffffff; /* text and marks on accent fills, 6.3:1 */
--neu-ink:#1f2937; /* text, 11.6:1 */
--neu-muted:#4a5568; /* secondary text, 5.9:1 */
--neu-edge:#74819a; /* control boundary (fields, tracks, boxes, buttons), 3.1:1 */
--neu-line:rgba(255,255,255,.6); /* decorative edge on cards and panels */
--neu-bw:1px; /* control border width */
--neu-success:#126b35;--neu-warning:#8a5300;--neu-danger:#b42318;
}
/* Dark neumorphism: data-neu="dark" on any wrapper, or a checked .neu-theme-input inside a [data-neu] element.
:is() is a forgiving list, so browsers without :has() still apply data-neu="dark" */
[data-neu]:is([data-neu="dark"],:has(.neu-theme-input:checked)){
--neu-bg:#262b33;--neu-light:#323944;--neu-dark:#14171c;
--neu-accent:#8f88ff;--neu-on-accent:#14161b;--neu-ink:#e7ebf2;--neu-muted:#aab4c3;--neu-edge:#717d90;
--neu-line:rgba(255,255,255,.06);--neu-success:#6fdc9a;--neu-warning:#f3c164;--neu-danger:#ff8f84;color-scheme:dark}
/* More contrast requested: the soft shadows go, every control and panel gets a solid outline */
@media (prefers-contrast:more){:root,[data-neu]:is([data-neu],:has(.neu-theme-input:checked)){
--neu-light:transparent;--neu-dark:transparent;--neu-edge:var(--neu-ink);--neu-line:var(--neu-ink);--neu-muted:var(--neu-ink);--neu-bw:2px}}
:root,[data-neu]{
--neu-raised:var(--neu-distance) var(--neu-distance) var(--neu-blur) var(--neu-dark),calc(var(--neu-distance)*-1) calc(var(--neu-distance)*-1) var(--neu-blur) var(--neu-light);
--neu-raised-sm:calc(var(--neu-distance)*.5) calc(var(--neu-distance)*.5) calc(var(--neu-blur)*.5) var(--neu-dark),calc(var(--neu-distance)*-.5) calc(var(--neu-distance)*-.5) calc(var(--neu-blur)*.5) var(--neu-light);
--neu-inset:inset calc(var(--neu-distance)*.6) calc(var(--neu-distance)*.6) calc(var(--neu-blur)*.6) var(--neu-dark),inset calc(var(--neu-distance)*-.6) calc(var(--neu-distance)*-.6) calc(var(--neu-blur)*.6) var(--neu-light);
--neu-inset-sm:inset calc(var(--neu-distance)*.35) calc(var(--neu-distance)*.35) calc(var(--neu-blur)*.35) var(--neu-dark),inset calc(var(--neu-distance)*-.35) calc(var(--neu-distance)*-.35) calc(var(--neu-blur)*.35) var(--neu-light);
}
[data-neu]{background:var(--neu-bg);color:var(--neu-ink)}
/* Components */
:where([class*="neu-"]),:where([class*="neu-"])::before,:where([class*="neu-"])::after{box-sizing:border-box}
.neu-sr{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
.neu-ico{width:20px;height:20px;flex:none}
/* Buttons: raised by default, pressed (inset) while held, accent ring on keyboard focus */
.neu-btn{display:inline-flex;align-items:center;justify-content:center;gap:.55rem;min-height:46px;padding:.65rem 1.35rem;
border:var(--neu-bw) solid var(--neu-edge);border-radius:calc(var(--neu-radius)*.75);background:var(--neu-bg);color:var(--neu-ink);
font-family:inherit;font-size:.95rem;font-weight:600;line-height:1.2;text-decoration:none;cursor:pointer;
box-shadow:var(--neu-raised-sm);transition:box-shadow .2s,color .2s,background-color .2s}
.neu-btn:hover{box-shadow:var(--neu-raised)}
.neu-btn:active{box-shadow:var(--neu-inset-sm)}
.neu-btn:focus-visible{outline:3px solid var(--neu-accent);outline-offset:3px}
.neu-btn--accent{background:var(--neu-accent);border-color:var(--neu-accent);color:var(--neu-on-accent)}
.neu-btn--sm{min-height:38px;padding:.45rem 1rem;font-size:.85rem}
.neu-btn--block{width:100%}
.neu-btn--icon{width:50px;height:50px;min-height:0;padding:0;border-radius:50%}
.neu-btn--square{border-radius:calc(var(--neu-radius)*.75)}
.neu-btn--icon-sm{width:42px;height:42px}
.neu-btn--icon-lg{width:64px;height:64px}
.neu-btn--icon .neu-ico{width:22px;height:22px}
/* Card: a raised panel with a faint highlight edge (a solid outline under prefers-contrast: more) */
.neu-card{padding:1.5rem;border:var(--neu-bw) solid var(--neu-line);border-radius:var(--neu-radius);background:var(--neu-bg);color:var(--neu-ink);box-shadow:var(--neu-raised)}
.neu-card h2,.neu-card h3,.neu-card p{margin:0}
/* Seek bar: a range input in a pressed groove; the script keeps the times and aria-valuetext in step */
.neu-range{--val:50%;width:100%;height:30px;margin:0;background:transparent;-webkit-appearance:none;appearance:none;cursor:pointer}
.neu-range::-webkit-slider-runnable-track{box-sizing:border-box;height:12px;border:var(--neu-bw) solid var(--neu-edge);border-radius:999px;box-shadow:var(--neu-inset-sm);
background:linear-gradient(var(--neu-accent),var(--neu-accent)) 0 0/var(--val) 100% no-repeat,var(--neu-bg)}
.neu-range::-webkit-slider-thumb{box-sizing:border-box;width:24px;height:24px;margin-top:-7px;border:var(--neu-bw) solid var(--neu-edge);border-radius:50%;-webkit-appearance:none;appearance:none;
background:radial-gradient(circle,var(--neu-accent) 0 3.5px,var(--neu-bg) 4px);box-shadow:var(--neu-raised-sm)}
.neu-range::-moz-range-track{box-sizing:border-box;height:12px;border:var(--neu-bw) solid var(--neu-edge);border-radius:999px;background:var(--neu-bg);box-shadow:var(--neu-inset-sm)}
.neu-range::-moz-range-progress{height:10px;border-radius:999px;background:var(--neu-accent)}
.neu-range::-moz-range-thumb{box-sizing:border-box;width:24px;height:24px;border:var(--neu-bw) solid var(--neu-edge);border-radius:50%;
background:radial-gradient(circle,var(--neu-accent) 0 3.5px,var(--neu-bg) 4px);box-shadow:var(--neu-raised-sm)}
.neu-range:focus-visible{outline:3px solid var(--neu-accent);outline-offset:4px;border-radius:999px}
/* Music player: a record in a pressed well, seek bar, and controls. Shuffle and repeat are checkbox toggles:
on = pressed in, accent, and a dot under the icon. Play is a checkbox whose icon turns into pause */
.neu-player{display:grid;justify-items:center;gap:.35rem;padding:1.6rem 1.5rem 1.5rem;text-align:center}
.neu-player__art{display:grid;place-items:center;width:150px;aspect-ratio:1;margin-bottom:1rem;border-radius:50%;box-shadow:var(--neu-raised)}
.neu-player__art span{width:126px;aspect-ratio:1;border-radius:50%;box-shadow:var(--neu-inset-sm);
background:radial-gradient(circle,var(--neu-bg) 0 9%,#f59e0b 9.5% 27%,#1e1b4b 27.5% 29%,#312e81 29.5% 100%);}
.neu-player__title{margin:0;font-size:1.15rem;font-weight:700}
.neu-player__artist{margin:0 0 .6rem;font-size:.88rem;color:var(--neu-muted)}
.neu-player__seek{width:100%}
.neu-player__times{display:flex;justify-content:space-between;width:100%;font-size:.78rem;font-variant-numeric:tabular-nums;color:var(--neu-muted)}
.neu-player__ctrls{display:flex;align-items:center;justify-content:center;gap:14px;margin-top:.6rem}
.neu-mtoggle{position:relative;display:inline-flex}
.neu-mtoggle__face{position:relative;display:grid;place-items:center;width:42px;height:42px;border:var(--neu-bw) solid var(--neu-edge);border-radius:50%;background:var(--neu-bg);
color:var(--neu-muted);box-shadow:var(--neu-raised-sm);cursor:pointer;transition:box-shadow .2s,color .2s,border-color .2s}
.neu-mtoggle__face .neu-ico{width:19px;height:19px}
.neu-mtoggle input:checked+.neu-mtoggle__face{border-color:var(--neu-accent);color:var(--neu-accent);box-shadow:var(--neu-inset-sm)}
.neu-mtoggle input:checked+.neu-mtoggle__face::after{content:"";position:absolute;bottom:5px;width:4px;height:4px;border-radius:50%;background:currentColor}
.neu-mtoggle input:focus-visible+.neu-mtoggle__face{outline:3px solid var(--neu-accent);outline-offset:3px}
.neu-play{position:relative;flex:none;display:inline-flex}
.neu-play__face{display:grid;place-items:center;width:64px;height:64px;border:var(--neu-bw) solid var(--neu-accent);border-radius:50%;
background:var(--neu-accent);color:var(--neu-on-accent);box-shadow:var(--neu-raised);cursor:pointer;transition:box-shadow .2s}
.neu-play__face .neu-ico{width:28px;height:28px}
.neu-play__face .neu-ico+.neu-ico,.neu-play input:checked+.neu-play__face .neu-ico:first-child{display:none}
.neu-play input:checked+.neu-play__face .neu-ico+.neu-ico{display:block}
.neu-play input:checked+.neu-play__face{box-shadow:var(--neu-inset-sm)}
.neu-play input:focus-visible+.neu-play__face{outline:3px solid var(--neu-accent);outline-offset:3px}
/* Thermostat: a 270° conic-gradient arc in a pressed groove, a raised core with the target,
and minus / plus buttons. A few lines of script change the target and the arc (--p, 0–100). */
.neu-dial{display:grid;justify-items:center;gap:1.1rem}
.neu-dial__face{--p:50;position:relative;display:grid;place-items:center;width:216px;aspect-ratio:1;border:var(--neu-bw) solid var(--neu-line);border-radius:50%;background:var(--neu-bg);box-shadow:var(--neu-raised)}
.neu-dial__arc{position:absolute;inset:12px;border-radius:50%;box-shadow:var(--neu-inset-sm);
background:conic-gradient(from 225deg,var(--neu-accent) calc(var(--p)*.75%),transparent 0);
-webkit-mask:radial-gradient(farthest-side,transparent calc(100% - 14px),#000 calc(100% - 13.5px));mask:radial-gradient(farthest-side,transparent calc(100% - 14px),#000 calc(100% - 13.5px));transition:background .3s}
.neu-dial__core{position:relative;display:grid;place-items:center;align-content:center;width:142px;aspect-ratio:1;border-radius:50%;box-shadow:var(--neu-raised-sm);text-align:center}
.neu-dial__mode{font-size:.75rem;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--neu-accent)}
.neu-dial__target{margin:0;font-size:2.6rem;font-weight:700;line-height:1.05;letter-spacing:-.03em;font-variant-numeric:tabular-nums}
.neu-dial__now{font-size:.8rem;color:var(--neu-muted)}
.neu-dial__ctrl{display:flex;align-items:center;gap:1.4rem}
.neu-dial__name{margin:0;font-weight:600}
/* Toggle tiles: a visually hidden checkbox and a face. On = pressed in (inset), accent border,
accent icon, a filled dot and the word On — never colour alone. */
.neu-toggle{position:relative;display:inline-flex;cursor:pointer}
.neu-toggle__face{display:flex;flex-direction:column;align-items:flex-start;gap:.4rem;width:100%;min-width:132px;padding:1rem 1.1rem;
border:var(--neu-bw) solid var(--neu-edge);border-radius:var(--neu-radius);background:var(--neu-bg);color:var(--neu-ink);
box-shadow:var(--neu-raised-sm);transition:box-shadow .25s,border-color .25s}
.neu-toggle__icon{display:grid;place-items:center;width:42px;height:42px;margin-bottom:.35rem;border-radius:50%;color:var(--neu-muted);box-shadow:var(--neu-inset-sm);transition:color .25s,background-color .25s}
.neu-toggle__icon .neu-ico{width:22px;height:22px}
.neu-toggle__name{font-weight:600;line-height:1.25}
.neu-toggle__state{display:flex;align-items:center;gap:.4rem;font-size:.8rem;font-weight:600;color:var(--neu-muted)}
.neu-toggle__state::before{content:"";width:9px;height:9px;border:2px solid currentColor;border-radius:50%}
.neu-toggle__state::after{content:"Off"}
.neu-toggle input:checked+.neu-toggle__face{box-shadow:var(--neu-inset);border-color:var(--neu-accent)}
.neu-toggle input:checked+.neu-toggle__face .neu-toggle__icon{color:var(--neu-on-accent);background:var(--neu-accent)}
.neu-toggle input:checked+.neu-toggle__face .neu-toggle__state{color:var(--neu-accent)}
.neu-toggle input:checked+.neu-toggle__face .neu-toggle__state::before{background:currentColor}
.neu-toggle input:checked+.neu-toggle__face .neu-toggle__state::after{content:"On"}
.neu-toggle input:focus-visible+.neu-toggle__face{outline:3px solid var(--neu-accent);outline-offset:3px}
.neu-toggle__sub{margin-top:-.3rem;font-size:.8rem;color:var(--neu-muted)}
/* Progress ring: role="progressbar"; a conic-gradient arc in a pressed groove around a raised core */
.neu-ring{--p:0;--size:128px;position:relative;display:grid;place-items:center;width:var(--size);aspect-ratio:1;border:var(--neu-bw) solid var(--neu-line);border-radius:50%;background:var(--neu-bg);box-shadow:var(--neu-raised)}
.neu-ring__arc{position:absolute;inset:10px;border-radius:50%;box-shadow:var(--neu-inset-sm);
background:conic-gradient(var(--neu-accent) calc(var(--p)*1%),transparent 0);
-webkit-mask:radial-gradient(farthest-side,transparent calc(100% - 12px),#000 calc(100% - 11.5px));mask:radial-gradient(farthest-side,transparent calc(100% - 12px),#000 calc(100% - 11.5px))}
.neu-ring__core{position:relative;display:grid;place-items:center;width:calc(var(--size) - 58px);aspect-ratio:1;border-radius:50%;box-shadow:var(--neu-raised-sm);
font-size:1.15rem;font-weight:700;font-variant-numeric:tabular-nums}
.neu-ring-cap{margin:.9rem 0 0;font-size:.88rem;font-weight:600;text-align:center}
.neu-ring-cap small{display:block;font-size:.8rem;font-weight:400;color:var(--neu-muted)}
.neu-ring-fig{display:grid;justify-items:center;margin:0}
/* Clock: a raised face, twelve ticks cut from a repeating conic gradient, hands rotated by --h, --m and --s.
The time is also written out as text; the face itself is hidden from screen readers */
.neu-clock{display:grid;justify-items:center;gap:.2rem;text-align:center}
.neu-clock__face{--h:304deg;--m:48deg;--s:0deg;position:relative;width:200px;aspect-ratio:1;margin-bottom:1.1rem;border:var(--neu-bw) solid var(--neu-line);border-radius:50%;background:var(--neu-bg);box-shadow:var(--neu-raised)}
.neu-clock__dial{position:absolute;inset:14px;border-radius:50%;box-shadow:var(--neu-inset)}
.neu-clock__ticks{position:absolute;inset:22px;border-radius:50%;background:repeating-conic-gradient(from -1.5deg,var(--neu-muted) 0 3deg,transparent 3deg 30deg);
-webkit-mask:radial-gradient(farthest-side,transparent calc(100% - 9px),#000 calc(100% - 8.5px));mask:radial-gradient(farthest-side,transparent calc(100% - 9px),#000 calc(100% - 8.5px))}
.neu-clock__hand{position:absolute;left:50%;bottom:50%;border-radius:4px;background:var(--neu-ink);transform-origin:50% 100%}
.neu-clock__hand--h{width:6px;height:25%;margin-left:-3px;transform:rotate(var(--h))}
.neu-clock__hand--m{width:4px;height:34%;margin-left:-2px;transform:rotate(var(--m))}
.neu-clock__hand--s{width:2px;height:38%;margin-left:-1px;background:var(--neu-accent);transform:rotate(var(--s))}
.neu-clock__face::after{content:"";position:absolute;left:50%;top:50%;width:14px;height:14px;margin:-7px 0 0 -7px;border-radius:50%;background:var(--neu-accent);box-shadow:var(--neu-raised-sm)}
.neu-clock__time{margin:0;font-size:1.6rem;font-weight:700;letter-spacing:-.02em;font-variant-numeric:tabular-nums}
.neu-clock__date{margin:0;font-size:.88rem;color:var(--neu-muted)}
.neu-lights{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:18px}
.neu-lights .neu-toggle__face{min-width:0}
.neu-rings{display:flex;flex-wrap:wrap;justify-content:space-around;gap:28px}
@media (prefers-reduced-motion:reduce){.neu-btn,.neu-mtoggle__face,.neu-play__face,.neu-toggle__face,.neu-toggle__icon,.neu-dial__arc{transition:none}}
JavaScript
// Thermostat: minus / plus move the target in 0.5° steps and redraw the arc (--p is 0–100 of the range)
document.querySelectorAll('[data-neu-dial]').forEach(function (dial) {
var min = +dial.dataset.min, max = +dial.dataset.max;
var face = dial.querySelector('[data-neu-face]'), out = dial.querySelector('[data-neu-value]');
dial.querySelectorAll('[data-step]').forEach(function (b) {
b.addEventListener('click', function () {
var t = Math.min(max, Math.max(min, parseFloat(out.textContent) + parseFloat(b.dataset.step)));
out.textContent = t.toFixed(1);
face.style.setProperty('--p', ((t - min) / (max - min) * 100).toFixed(1));
});
});
});
// Seek bar: paint the WebKit fill and keep the visible times and aria-valuetext in step
document.querySelectorAll('[data-neu-seek]').forEach(function (r) {
var now = document.querySelector('[data-neu-elapsed="' + r.id + '"]');
function mmss(s) { return Math.floor(s / 60) + ':' + String(Math.floor(s % 60)).padStart(2, '0'); }
function paint() {
r.style.setProperty('--val', r.value / r.max * 100 + '%');
r.setAttribute('aria-valuetext', mmss(r.value) + ' of ' + mmss(r.max));
if (now) now.textContent = mmss(r.value);
}
r.addEventListener('input', paint); paint();
});
// Clock: set the hands and the written time once a second
document.querySelectorAll('[data-neu-clock]').forEach(function (c) {
var face = c.querySelector('[data-neu-clock-face]'), time = c.querySelector('time'), date = c.querySelector('[data-neu-date]');
function tick() {
var n = new Date(), s = n.getSeconds(), m = n.getMinutes() + s / 60, h = n.getHours() % 12 + m / 60;
face.style.setProperty('--s', s * 6 + 'deg');
face.style.setProperty('--m', m * 6 + 'deg');
face.style.setProperty('--h', h * 30 + 'deg');
time.textContent = n.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
time.dateTime = n.toISOString();
date.textContent = n.toLocaleDateString([], { weekday: 'long', day: 'numeric', month: 'long' });
}
tick(); setInterval(tick, 1000);
});
/* ── Neumorphism UI kit tokens (uiCookies) — identical in every set ──────────────
Every component is moulded from --neu-bg and lit from the top left. Change a token and
the whole kit follows; the shadow recipes below are built from these values. */
:root{
--neu-bg:#e0e5ec; /* the one surface colour */
--neu-light:#ffffff; /* highlight, top left */
--neu-dark:#a3b1c6; /* shade, bottom right */
--neu-distance:6px; /* how far a shape rises */
--neu-blur:14px; /* how soft its shadow is */
--neu-radius:18px; /* cards; buttons use .75 of it, fields .6 */
--neu-accent:#4f46e5; /* focus ring and on / selected / current states, 4.9:1 on --neu-bg */
--neu-on-accent:#ffffff; /* text and marks on accent fills, 6.3:1 */
--neu-ink:#1f2937; /* text, 11.6:1 */
--neu-muted:#4a5568; /* secondary text, 5.9:1 */
--neu-edge:#74819a; /* control boundary (fields, tracks, boxes, buttons), 3.1:1 */
--neu-line:rgba(255,255,255,.6); /* decorative edge on cards and panels */
--neu-bw:1px; /* control border width */
--neu-success:#126b35;--neu-warning:#8a5300;--neu-danger:#b42318;
}
/* Dark neumorphism: data-neu="dark" on any wrapper, or a checked .neu-theme-input inside a [data-neu] element.
:is() is a forgiving list, so browsers without :has() still apply data-neu="dark" */
[data-neu]:is([data-neu="dark"],:has(.neu-theme-input:checked)){
--neu-bg:#262b33;--neu-light:#323944;--neu-dark:#14171c;
--neu-accent:#8f88ff;--neu-on-accent:#14161b;--neu-ink:#e7ebf2;--neu-muted:#aab4c3;--neu-edge:#717d90;
--neu-line:rgba(255,255,255,.06);--neu-success:#6fdc9a;--neu-warning:#f3c164;--neu-danger:#ff8f84;color-scheme:dark}
/* More contrast requested: the soft shadows go, every control and panel gets a solid outline */
@media (prefers-contrast:more){:root,[data-neu]:is([data-neu],:has(.neu-theme-input:checked)){
--neu-light:transparent;--neu-dark:transparent;--neu-edge:var(--neu-ink);--neu-line:var(--neu-ink);--neu-muted:var(--neu-ink);--neu-bw:2px}}
:root,[data-neu]{
--neu-raised:var(--neu-distance) var(--neu-distance) var(--neu-blur) var(--neu-dark),calc(var(--neu-distance)*-1) calc(var(--neu-distance)*-1) var(--neu-blur) var(--neu-light);
--neu-raised-sm:calc(var(--neu-distance)*.5) calc(var(--neu-distance)*.5) calc(var(--neu-blur)*.5) var(--neu-dark),calc(var(--neu-distance)*-.5) calc(var(--neu-distance)*-.5) calc(var(--neu-blur)*.5) var(--neu-light);
--neu-inset:inset calc(var(--neu-distance)*.6) calc(var(--neu-distance)*.6) calc(var(--neu-blur)*.6) var(--neu-dark),inset calc(var(--neu-distance)*-.6) calc(var(--neu-distance)*-.6) calc(var(--neu-blur)*.6) var(--neu-light);
--neu-inset-sm:inset calc(var(--neu-distance)*.35) calc(var(--neu-distance)*.35) calc(var(--neu-blur)*.35) var(--neu-dark),inset calc(var(--neu-distance)*-.35) calc(var(--neu-distance)*-.35) calc(var(--neu-blur)*.35) var(--neu-light);
}
[data-neu]{background:var(--neu-bg);color:var(--neu-ink)}
/* Bootstrap's own variables mapped onto the kit tokens */
:root,[data-neu]{--bs-body-bg:var(--neu-bg);--bs-body-color:var(--neu-ink);--bs-emphasis-color:var(--neu-ink);
--bs-heading-color:var(--neu-ink);--bs-secondary-color:var(--neu-muted);--bs-tertiary-color:var(--neu-muted);
--bs-border-color:var(--neu-edge);--bs-border-width:var(--neu-bw);--bs-border-radius:calc(var(--neu-radius)*.6);
--bs-border-radius-lg:var(--neu-radius);--bs-focus-ring-color:var(--neu-accent)}
/* Components */
.neu-ico{width:20px;height:20px;flex:none}
/* Buttons: Bootstrap .btn with a .btn-neu variant set through --bs-btn-* variables */
.btn-neu{--bs-btn-padding-x:1.35rem;--bs-btn-padding-y:.65rem;--bs-btn-font-size:.95rem;--bs-btn-font-weight:600;--bs-btn-line-height:1.2;
--bs-btn-border-width:var(--neu-bw);--bs-btn-border-radius:calc(var(--neu-radius)*.75);
--bs-btn-bg:var(--neu-bg);--bs-btn-color:var(--neu-ink);--bs-btn-border-color:var(--neu-edge);
--bs-btn-hover-bg:var(--neu-bg);--bs-btn-hover-color:var(--neu-ink);--bs-btn-hover-border-color:var(--neu-edge);
--bs-btn-active-bg:var(--neu-bg);--bs-btn-active-color:var(--neu-ink);--bs-btn-active-border-color:var(--neu-edge);
--neu-btn-shadow:var(--neu-raised-sm);
display:inline-flex;align-items:center;justify-content:center;gap:.55rem;min-height:46px;
box-shadow:var(--neu-btn-shadow);transition:box-shadow .2s,color .2s,background-color .2s}
.btn-neu:hover{--neu-btn-shadow:var(--neu-raised)}
.btn-neu:active{--neu-btn-shadow:var(--neu-inset-sm)}
.btn-neu:focus-visible,.btn-check:focus-visible+.btn-neu{box-shadow:var(--neu-btn-shadow);outline:3px solid var(--neu-accent);outline-offset:3px}
.btn-neu-accent{--bs-btn-bg:var(--neu-accent);--bs-btn-color:var(--neu-on-accent);--bs-btn-border-color:var(--neu-accent);
--bs-btn-hover-bg:var(--neu-accent);--bs-btn-hover-color:var(--neu-on-accent);--bs-btn-hover-border-color:var(--neu-accent);
--bs-btn-active-bg:var(--neu-accent);--bs-btn-active-color:var(--neu-on-accent);--bs-btn-active-border-color:var(--neu-accent)}
.btn-neu.btn-sm{--bs-btn-padding-x:1rem;--bs-btn-padding-y:.45rem;--bs-btn-font-size:.85rem;min-height:38px}
.btn-neu-icon{--bs-btn-padding-x:0;--bs-btn-padding-y:0;width:50px;height:50px;min-height:0;border-radius:50%}
.btn-neu-icon.btn-neu-square{border-radius:calc(var(--neu-radius)*.75)}
.btn-neu-icon-sm{width:42px;height:42px}
.btn-neu-icon-lg{width:64px;height:64px}
.btn-neu-icon .neu-ico{width:22px;height:22px}
/* Card: Bootstrap .card restyled through --bs-card-* */
.card.neu-card{--bs-card-bg:var(--neu-bg);--bs-card-color:var(--neu-ink);--bs-card-border-color:var(--neu-line);--bs-card-border-width:var(--neu-bw);
--bs-card-border-radius:var(--neu-radius);--bs-card-inner-border-radius:calc(var(--neu-radius) - var(--neu-bw));--bs-card-spacer-x:1.5rem;--bs-card-spacer-y:1.5rem;box-shadow:var(--neu-raised)}
.neu-card h2,.neu-card h3,.neu-card p{margin:0}
/* Seek bar: a range input in a pressed groove; the script keeps the times and aria-valuetext in step */
.form-range.neu-range{--val:50%;height:30px;cursor:pointer}
.form-range.neu-range::-webkit-slider-runnable-track{box-sizing:border-box;height:12px;border:var(--neu-bw) solid var(--neu-edge);border-radius:999px;box-shadow:var(--neu-inset-sm);
background:linear-gradient(var(--neu-accent),var(--neu-accent)) 0 0/var(--val) 100% no-repeat,var(--neu-bg)}
.form-range.neu-range::-webkit-slider-thumb{box-sizing:border-box;width:24px;height:24px;margin-top:-7px;border:var(--neu-bw) solid var(--neu-edge);border-radius:50%;-webkit-appearance:none;appearance:none;
background:radial-gradient(circle,var(--neu-accent) 0 3.5px,var(--neu-bg) 4px);box-shadow:var(--neu-raised-sm)}
.form-range.neu-range::-moz-range-track{box-sizing:border-box;height:12px;border:var(--neu-bw) solid var(--neu-edge);border-radius:999px;background:var(--neu-bg);box-shadow:var(--neu-inset-sm)}
.form-range.neu-range::-moz-range-progress{height:10px;border-radius:999px;background:var(--neu-accent)}
.form-range.neu-range::-moz-range-thumb{box-sizing:border-box;width:24px;height:24px;border:var(--neu-bw) solid var(--neu-edge);border-radius:50%;
background:radial-gradient(circle,var(--neu-accent) 0 3.5px,var(--neu-bg) 4px);box-shadow:var(--neu-raised-sm)}
.form-range.neu-range:focus-visible{outline:3px solid var(--neu-accent);outline-offset:4px;border-radius:999px}
.form-range.neu-range:focus::-webkit-slider-thumb{box-shadow:var(--neu-raised-sm)}
/* Music player: a record in a pressed well, seek bar, and controls. Shuffle and repeat are checkbox toggles:
on = pressed in, accent, and a dot under the icon. Play is a checkbox whose icon turns into pause */
.neu-player .card-body{display:grid;justify-items:center;gap:.35rem;padding:1.6rem 1.5rem 1.5rem;text-align:center}
.neu-player__art{display:grid;place-items:center;width:150px;aspect-ratio:1;margin-bottom:1rem;border-radius:50%;box-shadow:var(--neu-raised)}
.neu-player__art span{width:126px;aspect-ratio:1;border-radius:50%;box-shadow:var(--neu-inset-sm);
background:radial-gradient(circle,var(--neu-bg) 0 9%,#f59e0b 9.5% 27%,#1e1b4b 27.5% 29%,#312e81 29.5% 100%);}
.neu-player__title{margin:0;font-size:1.15rem;font-weight:700}
.neu-player__artist{margin:0 0 .6rem;font-size:.88rem;color:var(--neu-muted)}
.neu-player__seek{width:100%}
.neu-player__times{display:flex;justify-content:space-between;width:100%;font-size:.78rem;font-variant-numeric:tabular-nums;color:var(--neu-muted)}
.neu-player__ctrls{display:flex;align-items:center;justify-content:center;gap:14px;margin-top:.6rem}
.neu-mtoggle{position:relative;display:inline-flex}
.neu-mtoggle__face{position:relative;display:grid;place-items:center;width:42px;height:42px;border:var(--neu-bw) solid var(--neu-edge);border-radius:50%;background:var(--neu-bg);
color:var(--neu-muted);box-shadow:var(--neu-raised-sm);cursor:pointer;transition:box-shadow .2s,color .2s,border-color .2s}
.neu-mtoggle__face .neu-ico{width:19px;height:19px}
.neu-mtoggle input:checked+.neu-mtoggle__face{border-color:var(--neu-accent);color:var(--neu-accent);box-shadow:var(--neu-inset-sm)}
.neu-mtoggle input:checked+.neu-mtoggle__face::after{content:"";position:absolute;bottom:5px;width:4px;height:4px;border-radius:50%;background:currentColor}
.neu-mtoggle input:focus-visible+.neu-mtoggle__face{outline:3px solid var(--neu-accent);outline-offset:3px}
.neu-play{position:relative;flex:none;display:inline-flex}
.neu-play__face{display:grid;place-items:center;width:64px;height:64px;border:var(--neu-bw) solid var(--neu-accent);border-radius:50%;
background:var(--neu-accent);color:var(--neu-on-accent);box-shadow:var(--neu-raised);cursor:pointer;transition:box-shadow .2s}
.neu-play__face .neu-ico{width:28px;height:28px}
.neu-play__face .neu-ico+.neu-ico,.neu-play input:checked+.neu-play__face .neu-ico:first-child{display:none}
.neu-play input:checked+.neu-play__face .neu-ico+.neu-ico{display:block}
.neu-play input:checked+.neu-play__face{box-shadow:var(--neu-inset-sm)}
.neu-play input:focus-visible+.neu-play__face{outline:3px solid var(--neu-accent);outline-offset:3px}
/* Thermostat: a 270° conic-gradient arc in a pressed groove, a raised core with the target,
and minus / plus buttons. A few lines of script change the target and the arc (--p, 0–100). */
.neu-dial{display:grid;justify-items:center;gap:1.1rem}
.neu-dial__face{--p:50;position:relative;display:grid;place-items:center;width:216px;aspect-ratio:1;border:var(--neu-bw) solid var(--neu-line);border-radius:50%;background:var(--neu-bg);box-shadow:var(--neu-raised)}
.neu-dial__arc{position:absolute;inset:12px;border-radius:50%;box-shadow:var(--neu-inset-sm);
background:conic-gradient(from 225deg,var(--neu-accent) calc(var(--p)*.75%),transparent 0);
-webkit-mask:radial-gradient(farthest-side,transparent calc(100% - 14px),#000 calc(100% - 13.5px));mask:radial-gradient(farthest-side,transparent calc(100% - 14px),#000 calc(100% - 13.5px));transition:background .3s}
.neu-dial__core{position:relative;display:grid;place-items:center;align-content:center;width:142px;aspect-ratio:1;border-radius:50%;box-shadow:var(--neu-raised-sm);text-align:center}
.neu-dial__mode{font-size:.75rem;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--neu-accent)}
.neu-dial__target{margin:0;font-size:2.6rem;font-weight:700;line-height:1.05;letter-spacing:-.03em;font-variant-numeric:tabular-nums}
.neu-dial__now{font-size:.8rem;color:var(--neu-muted)}
.neu-dial__ctrl{display:flex;align-items:center;gap:1.4rem}
.neu-dial__name{margin:0;font-weight:600}
/* Toggle tiles: Bootstrap .btn-check + .btn. On = pressed in, accent border and icon, a filled dot and the word On */
.btn-neu-tile{--bs-btn-padding-x:1.1rem;--bs-btn-padding-y:1rem;--bs-btn-border-radius:var(--neu-radius);--bs-btn-font-weight:400;--bs-btn-line-height:1.5;--bs-btn-font-size:1rem;
--bs-btn-active-color:var(--neu-ink);--bs-btn-active-border-color:var(--neu-accent);
flex-direction:column;align-items:flex-start;gap:.4rem;width:100%;min-width:132px;text-align:start;transition:box-shadow .25s,border-color .25s}
.btn-neu-tile:hover{--neu-btn-shadow:var(--neu-raised-sm)}
.btn-check:checked+.btn-neu-tile{--neu-btn-shadow:var(--neu-inset)}
.btn-neu-tile .neu-tile-icon{display:grid;place-items:center;width:42px;height:42px;margin-bottom:.35rem;border-radius:50%;color:var(--neu-muted);box-shadow:var(--neu-inset-sm);transition:color .25s,background-color .25s}
.btn-neu-tile .neu-tile-icon .neu-ico{width:22px;height:22px}
.btn-neu-tile .neu-tile-name{font-weight:600;line-height:1.25}
.btn-neu-tile .neu-tile-state{display:flex;align-items:center;gap:.4rem;font-size:.8rem;font-weight:600;color:var(--neu-muted)}
.btn-neu-tile .neu-tile-state::before{content:"";width:9px;height:9px;border:2px solid currentColor;border-radius:50%}
.btn-neu-tile .neu-tile-state::after{content:"Off"}
.btn-check:checked+.btn-neu-tile .neu-tile-icon{color:var(--neu-on-accent);background:var(--neu-accent)}
.btn-check:checked+.btn-neu-tile .neu-tile-state{color:var(--neu-accent)}
.btn-check:checked+.btn-neu-tile .neu-tile-state::before{background:currentColor}
.btn-check:checked+.btn-neu-tile .neu-tile-state::after{content:"On"}
.btn-neu-tile .neu-tile-sub{margin-top:-.3rem;font-size:.8rem;color:var(--neu-muted)}
/* Progress ring: role="progressbar"; a conic-gradient arc in a pressed groove around a raised core */
.neu-ring{--p:0;--size:128px;position:relative;display:grid;place-items:center;width:var(--size);aspect-ratio:1;border:var(--neu-bw) solid var(--neu-line);border-radius:50%;background:var(--neu-bg);box-shadow:var(--neu-raised)}
.neu-ring__arc{position:absolute;inset:10px;border-radius:50%;box-shadow:var(--neu-inset-sm);
background:conic-gradient(var(--neu-accent) calc(var(--p)*1%),transparent 0);
-webkit-mask:radial-gradient(farthest-side,transparent calc(100% - 12px),#000 calc(100% - 11.5px));mask:radial-gradient(farthest-side,transparent calc(100% - 12px),#000 calc(100% - 11.5px))}
.neu-ring__core{position:relative;display:grid;place-items:center;width:calc(var(--size) - 58px);aspect-ratio:1;border-radius:50%;box-shadow:var(--neu-raised-sm);
font-size:1.15rem;font-weight:700;font-variant-numeric:tabular-nums}
.neu-ring-cap{margin:.9rem 0 0;font-size:.88rem;font-weight:600;text-align:center}
.neu-ring-cap small{display:block;font-size:.8rem;font-weight:400;color:var(--neu-muted)}
.neu-ring-fig{display:grid;justify-items:center;margin:0}
/* Clock: a raised face, twelve ticks cut from a repeating conic gradient, hands rotated by --h, --m and --s.
The time is also written out as text; the face itself is hidden from screen readers */
.neu-clock{display:grid;justify-items:center;gap:.2rem;text-align:center}
.neu-clock__face{--h:304deg;--m:48deg;--s:0deg;position:relative;width:200px;aspect-ratio:1;margin-bottom:1.1rem;border:var(--neu-bw) solid var(--neu-line);border-radius:50%;background:var(--neu-bg);box-shadow:var(--neu-raised)}
.neu-clock__dial{position:absolute;inset:14px;border-radius:50%;box-shadow:var(--neu-inset)}
.neu-clock__ticks{position:absolute;inset:22px;border-radius:50%;background:repeating-conic-gradient(from -1.5deg,var(--neu-muted) 0 3deg,transparent 3deg 30deg);
-webkit-mask:radial-gradient(farthest-side,transparent calc(100% - 9px),#000 calc(100% - 8.5px));mask:radial-gradient(farthest-side,transparent calc(100% - 9px),#000 calc(100% - 8.5px))}
.neu-clock__hand{position:absolute;left:50%;bottom:50%;border-radius:4px;background:var(--neu-ink);transform-origin:50% 100%}
.neu-clock__hand--h{width:6px;height:25%;margin-left:-3px;transform:rotate(var(--h))}
.neu-clock__hand--m{width:4px;height:34%;margin-left:-2px;transform:rotate(var(--m))}
.neu-clock__hand--s{width:2px;height:38%;margin-left:-1px;background:var(--neu-accent);transform:rotate(var(--s))}
.neu-clock__face::after{content:"";position:absolute;left:50%;top:50%;width:14px;height:14px;margin:-7px 0 0 -7px;border-radius:50%;background:var(--neu-accent);box-shadow:var(--neu-raised-sm)}
.neu-clock__time{margin:0;font-size:1.6rem;font-weight:700;letter-spacing:-.02em;font-variant-numeric:tabular-nums}
.neu-clock__date{margin:0;font-size:.88rem;color:var(--neu-muted)}
.neu-lights{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:18px}
.neu-lights .btn-neu-tile{min-width:0}
.neu-rings{display:flex;flex-wrap:wrap;justify-content:space-around;gap:28px}
@media (prefers-reduced-motion:reduce){.btn-neu,.neu-mtoggle__face,.neu-play__face,.btn-neu-tile .neu-tile-icon,.neu-dial__arc{transition:none}}
JavaScript
// Thermostat: minus / plus move the target in 0.5° steps and redraw the arc (--p is 0–100 of the range)
document.querySelectorAll('[data-neu-dial]').forEach(function (dial) {
var min = +dial.dataset.min, max = +dial.dataset.max;
var face = dial.querySelector('[data-neu-face]'), out = dial.querySelector('[data-neu-value]');
dial.querySelectorAll('[data-step]').forEach(function (b) {
b.addEventListener('click', function () {
var t = Math.min(max, Math.max(min, parseFloat(out.textContent) + parseFloat(b.dataset.step)));
out.textContent = t.toFixed(1);
face.style.setProperty('--p', ((t - min) / (max - min) * 100).toFixed(1));
});
});
});
// Seek bar: paint the WebKit fill and keep the visible times and aria-valuetext in step
document.querySelectorAll('[data-neu-seek]').forEach(function (r) {
var now = document.querySelector('[data-neu-elapsed="' + r.id + '"]');
function mmss(s) { return Math.floor(s / 60) + ':' + String(Math.floor(s % 60)).padStart(2, '0'); }
function paint() {
r.style.setProperty('--val', r.value / r.max * 100 + '%');
r.setAttribute('aria-valuetext', mmss(r.value) + ' of ' + mmss(r.max));
if (now) now.textContent = mmss(r.value);
}
r.addEventListener('input', paint); paint();
});
// Clock: set the hands and the written time once a second
document.querySelectorAll('[data-neu-clock]').forEach(function (c) {
var face = c.querySelector('[data-neu-clock-face]'), time = c.querySelector('time'), date = c.querySelector('[data-neu-date]');
function tick() {
var n = new Date(), s = n.getSeconds(), m = n.getMinutes() + s / 60, h = n.getHours() % 12 + m / 60;
face.style.setProperty('--s', s * 6 + 'deg');
face.style.setProperty('--m', m * 6 + 'deg');
face.style.setProperty('--h', h * 30 + 'deg');
time.textContent = n.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
time.dateTime = n.toISOString();
date.textContent = n.toLocaleDateString([], { weekday: 'long', day: 'numeric', month: 'long' });
}
tick(); setInterval(tick, 1000);
});
/* ── Neumorphism UI kit tokens (uiCookies) — identical in every set ──────────────
Every component is moulded from --neu-bg and lit from the top left. Change a token and
the whole kit follows; the shadow recipes below are built from these values. */
:root{
--neu-bg:#e0e5ec; /* the one surface colour */
--neu-light:#ffffff; /* highlight, top left */
--neu-dark:#a3b1c6; /* shade, bottom right */
--neu-distance:6px; /* how far a shape rises */
--neu-blur:14px; /* how soft its shadow is */
--neu-radius:18px; /* cards; buttons use .75 of it, fields .6 */
--neu-accent:#4f46e5; /* focus ring and on / selected / current states, 4.9:1 on --neu-bg */
--neu-on-accent:#ffffff; /* text and marks on accent fills, 6.3:1 */
--neu-ink:#1f2937; /* text, 11.6:1 */
--neu-muted:#4a5568; /* secondary text, 5.9:1 */
--neu-edge:#74819a; /* control boundary (fields, tracks, boxes, buttons), 3.1:1 */
--neu-line:rgba(255,255,255,.6); /* decorative edge on cards and panels */
--neu-bw:1px; /* control border width */
--neu-success:#126b35;--neu-warning:#8a5300;--neu-danger:#b42318;
}
/* Dark neumorphism: data-neu="dark" on any wrapper, or a checked .neu-theme-input inside a [data-neu] element.
:is() is a forgiving list, so browsers without :has() still apply data-neu="dark" */
[data-neu]:is([data-neu="dark"],:has(.neu-theme-input:checked)){
--neu-bg:#262b33;--neu-light:#323944;--neu-dark:#14171c;
--neu-accent:#8f88ff;--neu-on-accent:#14161b;--neu-ink:#e7ebf2;--neu-muted:#aab4c3;--neu-edge:#717d90;
--neu-line:rgba(255,255,255,.06);--neu-success:#6fdc9a;--neu-warning:#f3c164;--neu-danger:#ff8f84;color-scheme:dark}
/* More contrast requested: the soft shadows go, every control and panel gets a solid outline */
@media (prefers-contrast:more){:root,[data-neu]:is([data-neu],:has(.neu-theme-input:checked)){
--neu-light:transparent;--neu-dark:transparent;--neu-edge:var(--neu-ink);--neu-line:var(--neu-ink);--neu-muted:var(--neu-ink);--neu-bw:2px}}
:root,[data-neu]{
--neu-raised:var(--neu-distance) var(--neu-distance) var(--neu-blur) var(--neu-dark),calc(var(--neu-distance)*-1) calc(var(--neu-distance)*-1) var(--neu-blur) var(--neu-light);
--neu-raised-sm:calc(var(--neu-distance)*.5) calc(var(--neu-distance)*.5) calc(var(--neu-blur)*.5) var(--neu-dark),calc(var(--neu-distance)*-.5) calc(var(--neu-distance)*-.5) calc(var(--neu-blur)*.5) var(--neu-light);
--neu-inset:inset calc(var(--neu-distance)*.6) calc(var(--neu-distance)*.6) calc(var(--neu-blur)*.6) var(--neu-dark),inset calc(var(--neu-distance)*-.6) calc(var(--neu-distance)*-.6) calc(var(--neu-blur)*.6) var(--neu-light);
--neu-inset-sm:inset calc(var(--neu-distance)*.35) calc(var(--neu-distance)*.35) calc(var(--neu-blur)*.35) var(--neu-dark),inset calc(var(--neu-distance)*-.35) calc(var(--neu-distance)*-.35) calc(var(--neu-blur)*.35) var(--neu-light);
}
[data-neu]{background:var(--neu-bg);color:var(--neu-ink)}
/* The tokens above, exposed as Tailwind utilities: bg-neu-bg, text-neu-muted, border-neu-edge,
shadow-neu, shadow-neu-sm, shadow-neu-in, shadow-neu-in-sm, rounded-neu, rounded-neu-btn, rounded-neu-field… */
@theme inline {
--color-neu-bg: var(--neu-bg); --color-neu-ink: var(--neu-ink); --color-neu-muted: var(--neu-muted);
--color-neu-edge: var(--neu-edge); --color-neu-line: var(--neu-line);
--color-neu-accent: var(--neu-accent); --color-neu-on-accent: var(--neu-on-accent);
--color-neu-success: var(--neu-success); --color-neu-warning: var(--neu-warning); --color-neu-danger: var(--neu-danger);
--shadow-neu: var(--neu-raised); --shadow-neu-sm: var(--neu-raised-sm);
--shadow-neu-in: var(--neu-inset); --shadow-neu-in-sm: var(--neu-inset-sm);
--radius-neu: var(--neu-radius); --radius-neu-btn: calc(var(--neu-radius) * .75); --radius-neu-field: calc(var(--neu-radius) * .6);
}
/* Seek bar thumb and track: Tailwind has no utilities for these pseudo-elements */
.neu-range{--val:50%;width:100%;height:30px;margin:0;background:transparent;-webkit-appearance:none;appearance:none;cursor:pointer}
.neu-range::-webkit-slider-runnable-track{box-sizing:border-box;height:12px;border:var(--neu-bw) solid var(--neu-edge);border-radius:999px;box-shadow:var(--neu-inset-sm);
background:linear-gradient(var(--neu-accent),var(--neu-accent)) 0 0/var(--val) 100% no-repeat,var(--neu-bg)}
.neu-range::-webkit-slider-thumb{box-sizing:border-box;width:24px;height:24px;margin-top:-7px;border:var(--neu-bw) solid var(--neu-edge);border-radius:50%;-webkit-appearance:none;appearance:none;
background:radial-gradient(circle,var(--neu-accent) 0 3.5px,var(--neu-bg) 4px);box-shadow:var(--neu-raised-sm)}
.neu-range::-moz-range-track{box-sizing:border-box;height:12px;border:var(--neu-bw) solid var(--neu-edge);border-radius:999px;background:var(--neu-bg);box-shadow:var(--neu-inset-sm)}
.neu-range::-moz-range-progress{height:10px;border-radius:999px;background:var(--neu-accent)}
.neu-range::-moz-range-thumb{box-sizing:border-box;width:24px;height:24px;border:var(--neu-bw) solid var(--neu-edge);border-radius:50%;
background:radial-gradient(circle,var(--neu-accent) 0 3.5px,var(--neu-bg) 4px);box-shadow:var(--neu-raised-sm)}
.neu-range:focus-visible{outline:3px solid var(--neu-accent);outline-offset:4px;border-radius:999px}
JavaScript
// Thermostat: minus / plus move the target in 0.5° steps and redraw the arc (--p is 0–100 of the range)
document.querySelectorAll('[data-neu-dial]').forEach(function (dial) {
var min = +dial.dataset.min, max = +dial.dataset.max;
var face = dial.querySelector('[data-neu-face]'), out = dial.querySelector('[data-neu-value]');
dial.querySelectorAll('[data-step]').forEach(function (b) {
b.addEventListener('click', function () {
var t = Math.min(max, Math.max(min, parseFloat(out.textContent) + parseFloat(b.dataset.step)));
out.textContent = t.toFixed(1);
face.style.setProperty('--p', ((t - min) / (max - min) * 100).toFixed(1));
});
});
});
// Seek bar: paint the WebKit fill and keep the visible times and aria-valuetext in step
document.querySelectorAll('[data-neu-seek]').forEach(function (r) {
var now = document.querySelector('[data-neu-elapsed="' + r.id + '"]');
function mmss(s) { return Math.floor(s / 60) + ':' + String(Math.floor(s % 60)).padStart(2, '0'); }
function paint() {
r.style.setProperty('--val', r.value / r.max * 100 + '%');
r.setAttribute('aria-valuetext', mmss(r.value) + ' of ' + mmss(r.max));
if (now) now.textContent = mmss(r.value);
}
r.addEventListener('input', paint); paint();
});
// Clock: set the hands and the written time once a second
document.querySelectorAll('[data-neu-clock]').forEach(function (c) {
var face = c.querySelector('[data-neu-clock-face]'), time = c.querySelector('time'), date = c.querySelector('[data-neu-date]');
function tick() {
var n = new Date(), s = n.getSeconds(), m = n.getMinutes() + s / 60, h = n.getHours() % 12 + m / 60;
face.style.setProperty('--s', s * 6 + 'deg');
face.style.setProperty('--m', m * 6 + 'deg');
face.style.setProperty('--h', h * 30 + 'deg');
time.textContent = n.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
time.dateTime = n.toISOString();
date.textContent = n.toLocaleDateString([], { weekday: 'long', day: 'numeric', month: 'long' });
}
tick(); setInterval(tick, 1000);
});
About this set
The widgets soft UI is best known for, built to work rather than just to look right. A music player shows a record in a raised ring, a seek bar that is a real range input reading 1:42 of 3:58, and shuffle, previous, play, next and repeat controls; shuffle and repeat press in and add a dot when on, and play turns into pause. A thermostat dial draws the target temperature as a 270 degree conic-gradient arc around a raised core, and its minus and plus buttons move the target in half degrees. Four light switches are tiles with role=”switch” that press in and say On or Off. Three progress rings show a battery, a water tank and a filter as progressbars with their value in the middle. An analogue clock with ticks cut from a repeating conic gradient keeps time next to the written time and date. Use them for a home control screen, a media panel or a status board. Around forty lines of vanilla JavaScript drive the seek times, the thermostat and the clock; without it every widget still shows sensible values.
What’s included
Thermostat dial with a conic-gradient arc and working minus and plus buttons
Music player with a range seek bar and checkbox play, shuffle and repeat
Light switch tiles with role="switch" and visible On or Off
Progress rings exposed as progressbars with a label
Analogue clock with the time also written out as text
Accessibility
The seek bar announces 1:42 of 3:58 through aria-valuetext, the thermostat is a labelled group whose target is announced politely when it changes, and the rings are progressbars with names and values. Play, shuffle, repeat and each light have a name and a checked state, and the clock face is hidden in favour of the written time. Focus shows a 3px accent ring, and transitions stop under prefers-reduced-motion.
Customise it
Change the --neu-* tokens for colour and depth; each ring takes --p (0 to 100) and the dial reads data-min and data-max for its range. In Tailwind the arc and ring are bg-[conic-gradient(...)] utilities with a mask, and the Bootstrap version reuses .form-range for the seek bar.