/* ── 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}
/* Sidebar: current item = pressed in, accent edge and text, and a dot at the end */
.neu-side{display:grid;align-content:start;gap:.2rem;padding:1.1rem;border:var(--neu-bw) solid var(--neu-line);border-radius:var(--neu-radius);background:var(--neu-bg);box-shadow:var(--neu-raised)}
.neu-side__heading{margin:.5rem .8rem .4rem;font-size:.72rem;font-weight:700;letter-spacing:.1em;text-transform:uppercase;color:var(--neu-muted)}
.neu-side__list{display:grid;gap:6px;margin:0;padding:0;list-style:none}
.neu-sidelink{display:flex;align-items:center;gap:.75rem;min-height:44px;padding:.55rem .8rem;border:var(--neu-bw) solid transparent;border-radius:calc(var(--neu-radius)*.6);
color:var(--neu-muted);font-weight:600;text-decoration:none;transition:box-shadow .2s,color .2s}
.neu-sidelink:hover{color:var(--neu-ink);box-shadow:var(--neu-raised-sm)}
.neu-sidelink[aria-current="page"]{border-color:var(--neu-accent);color:var(--neu-accent);box-shadow:var(--neu-inset-sm)}
.neu-sidelink[aria-current="page"]::after{content:"";width:8px;height:8px;margin-left:auto;border-radius:50%;background:currentColor}
.neu-sidelink:focus-visible{outline:3px solid var(--neu-accent);outline-offset:2px}
.neu-count{margin-left:auto;min-width:24px;padding:.1rem .45rem;border-radius:999px;background:var(--neu-accent);color:var(--neu-on-accent);font-size:.75rem;font-weight:700;text-align:center}
/* Switches: a checkbox with role="switch". On = knob travels right, track fills with accent and the word On appears */
.neu-switch{position:relative;display:flex;align-items:center;gap:.9rem;cursor:pointer}
.neu-switch__text{flex:1;min-width:0;font-weight:600;line-height:1.3}
.neu-switch__text small{display:block;margin-top:.15rem;font-size:.82rem;font-weight:400;color:var(--neu-muted)}
.neu-switch__state{min-width:2.1em;font-size:.75rem;font-weight:700;letter-spacing:.08em;text-align:right;text-transform:uppercase;color:var(--neu-muted)}
.neu-switch__state::after{content:"Off"}
.neu-switch__track{position:relative;flex:none;width:58px;height:32px;border:var(--neu-bw) solid var(--neu-edge);border-radius:999px;
background:var(--neu-bg);box-shadow:var(--neu-inset-sm);transition:background-color .25s,border-color .25s}
.neu-switch__track::before{content:"";position:absolute;top:50%;left:3px;width:24px;height:24px;margin-top:-12px;border-radius:50%;
border:var(--neu-bw) solid var(--neu-edge);background:var(--neu-bg);box-shadow:var(--neu-raised-sm);transition:transform .25s,background-color .25s}
.neu-switch input:checked~.neu-switch__track{background:var(--neu-accent);border-color:var(--neu-accent)}
.neu-switch input:checked~.neu-switch__track::before{transform:translateX(calc(28px - 2*var(--neu-bw)));background:var(--neu-on-accent);border-color:var(--neu-on-accent)}
.neu-switch input:checked~.neu-switch__state{color:var(--neu-accent)}
.neu-switch input:checked~.neu-switch__state::after{content:"On"}
.neu-switch input:focus-visible~.neu-switch__track{outline:3px solid var(--neu-accent);outline-offset:3px}
/* Stat tile: icon in a pressed well, label, a big value, and a change line that says which way in words */
.neu-stat{display:grid;gap:.2rem;padding:1.25rem;border:var(--neu-bw) solid var(--neu-line);border-radius:var(--neu-radius);background:var(--neu-bg);box-shadow:var(--neu-raised)}
.neu-stat__icon{display:grid;place-items:center;width:44px;height:44px;margin-bottom:.6rem;border-radius:50%;color:var(--neu-accent);box-shadow:var(--neu-inset-sm)}
.neu-stat__label{margin:0;font-size:.85rem;font-weight:600;color:var(--neu-muted)}
.neu-stat__value{margin:0;font-size:1.9rem;font-weight:700;line-height:1.15;letter-spacing:-.02em;font-variant-numeric:tabular-nums}
.neu-stat__value small{font-size:.9rem;font-weight:600;letter-spacing:0;color:var(--neu-muted)}
.neu-stat__delta{display:flex;align-items:center;gap:.3rem;margin:.35rem 0 0;font-size:.82rem;font-weight:600;color:var(--neu-muted)}
.neu-stat__delta .neu-ico{width:16px;height:16px}
.neu-stat__delta--good{color:var(--neu-success)}
.neu-stat__delta--warn{color:var(--neu-warning)}
/* 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)}
/* Segmented control: radios in a fieldset. The chosen option rises out of the well, turns accent and gains a dot */
.neu-seg{min-width:0;margin:0;padding:0;border:0}
.neu-seg__legend{margin-bottom:.6rem;padding:0;font-size:.88rem;font-weight:600}
.neu-seg__opts{display:flex;flex-wrap:wrap;gap:6px;padding:6px;border:var(--neu-bw) solid var(--neu-edge);border-radius:calc(var(--neu-radius)*.75);box-shadow:var(--neu-inset-sm)}
.neu-seg__opt{position:relative;flex:1 1 auto}
.neu-seg__opt span{display:flex;align-items:center;justify-content:center;gap:.4rem;min-height:40px;padding:.4rem .9rem;border:var(--neu-bw) solid transparent;
border-radius:calc(var(--neu-radius)*.5);color:var(--neu-muted);font-size:.9rem;font-weight:600;cursor:pointer;transition:box-shadow .2s,color .2s}
.neu-seg__opt input:checked+span{border-color:var(--neu-accent);color:var(--neu-accent);box-shadow:var(--neu-raised-sm)}
.neu-seg__opt input:checked+span::before{content:"";width:7px;height:7px;border-radius:50%;background:currentColor}
.neu-seg__opt input:focus-visible+span{outline:3px solid var(--neu-accent);outline-offset:2px}
/* Mini player: art in a pressed well, title, a progress groove, and a play / pause toggle (checkbox) */
.neu-mini{display:flex;align-items:center;gap:1rem;padding:1rem 1.1rem;border:var(--neu-bw) solid var(--neu-line);border-radius:var(--neu-radius);background:var(--neu-bg);box-shadow:var(--neu-raised)}
.neu-mini__art{flex:none;width:52px;height:52px;border-radius:calc(var(--neu-radius)*.6);box-shadow:var(--neu-inset-sm);
background:radial-gradient(circle at 30% 30%,#f59e0b 0 18%,transparent 19%),linear-gradient(160deg,#4f46e5,#0e7490)}
.neu-mini__meta{flex:1;min-width:0}
.neu-mini__title{margin:0;font-weight:700;line-height:1.25;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.neu-mini__artist{margin:0;font-size:.82rem;color:var(--neu-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.neu-mini__bar{height:8px;margin-top:.55rem;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(--p,40%) 100% no-repeat}
.neu-play{position:relative;flex:none;display:inline-flex}
.neu-play__face{display:grid;place-items:center;width:52px;height:52px;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-sm);cursor:pointer;transition:box-shadow .2s}
.neu-play__face .neu-ico{width:24px;height:24px}
.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}
/* Alerts: the type is carried by an icon, a title and hidden "Warning:" style text, not by colour alone */
.neu-alerts{display:grid;gap:16px}
.neu-alert{--tone:var(--neu-accent);position:relative;display:flex;align-items:flex-start;gap:.9rem;padding:1rem 1.1rem 1rem 1.35rem;border:var(--neu-bw) solid var(--neu-line);
border-radius:var(--neu-radius);background:var(--neu-bg);box-shadow:var(--neu-raised-sm)}
.neu-alert::before{content:"";position:absolute;left:.55rem;top:1rem;bottom:1rem;width:4px;border-radius:4px;background:var(--tone)}
.neu-alert--success{--tone:var(--neu-success)}.neu-alert--warning{--tone:var(--neu-warning)}.neu-alert--danger{--tone:var(--neu-danger)}
.neu-alert__icon{display:grid;place-items:center;flex:none;width:40px;height:40px;border-radius:50%;color:var(--tone);box-shadow:var(--neu-inset-sm)}
.neu-alert__title{margin:0;font-weight:700;line-height:1.3}
.neu-alert__text{margin:.15rem 0 0;font-size:.9rem;color:var(--neu-muted)}
/* Dashboard shell. The theme switch is a checkbox with class .neu-theme-input: the token block's
[data-neu]:has(.neu-theme-input:checked) rule swaps every token to dark, so no script is involved */
.neu-home{display:grid;grid-template-columns:250px minmax(0,1fr);gap:32px;min-height:100vh;padding:28px}
.neu-home__side{position:sticky;top:28px;align-self:start}
.neu-home__brand{display:flex;align-items:center;gap:.65rem;margin:0 .4rem .9rem;font-size:1.1rem;font-weight:800}
.neu-home__mark{display:grid;place-items:center;width:40px;height:40px;border-radius:50%;color:var(--neu-accent);box-shadow:var(--neu-inset-sm)}
.neu-home__main{display:grid;grid-template-columns:minmax(0,1fr);align-content:start;gap:28px;min-width:0}
.neu-home__head{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:16px 24px}
.neu-home__title{margin:0;font-size:1.75rem;font-weight:800;line-height:1.2;letter-spacing:-.02em}
.neu-home__sub{margin:.25rem 0 0;color:var(--neu-muted)}
.neu-home__theme{min-width:250px;padding:.7rem 1rem;border:var(--neu-bw) solid var(--neu-line);border-radius:calc(var(--neu-radius)*.75);box-shadow:var(--neu-raised-sm)}
.neu-home__stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,190px),1fr));gap:22px}
.neu-home__grid{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.25fr);gap:22px;align-items:start}
.neu-home__wide{grid-column:1/-1}
.neu-home__row{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,300px),1fr));gap:22px;align-items:center}
.neu-home__row>:last-child{grid-column:1/-1}
.neu-home__panel{display:grid;grid-template-columns:minmax(0,1fr);gap:1.1rem}
.neu-home__h{display:flex;justify-content:space-between;align-items:baseline;gap:.5rem;margin:0;font-size:1rem;font-weight:700}
.neu-home__h small{font-size:.82rem;font-weight:600;color:var(--neu-muted)}
.neu-home__lights{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px}
@media (max-width:1100px){.neu-home__grid{grid-template-columns:minmax(0,1fr)}}
@media (max-width:900px){.neu-home{grid-template-columns:minmax(0,1fr);gap:24px;padding:16px}.neu-home__side{position:static}
.neu-home .neu-side__list{display:flex;flex-wrap:wrap}.neu-home .neu-side__heading{display:none}}
.neu-home .neu-toggle__face{min-width:0}
@media (prefers-reduced-motion:reduce){.neu-btn,.neu-sidelink,.neu-switch__track,.neu-switch__track::before,.neu-toggle__face,.neu-toggle__icon,.neu-seg__opt span,.neu-play__face,.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));
});
});
});
/* ── 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}
/* Sidebar: .card holding .nav.nav-pills.flex-column; .active + aria-current = pressed in, accent, a dot */
.neu-side .card-body{display:grid;align-content:start;gap:.2rem;padding:1.1rem}
.neu-side .neu-side__heading{margin:.5rem .8rem .4rem;font-size:.72rem;font-weight:700;line-height:1.5;letter-spacing:.1em;text-transform:uppercase;color:var(--neu-muted)}
.neu-side .nav{--bs-nav-link-padding-x:.8rem;--bs-nav-link-padding-y:.55rem;--bs-nav-link-color:var(--neu-muted);--bs-nav-link-hover-color:var(--neu-ink);--bs-nav-link-font-weight:600;
--bs-nav-pills-border-radius:calc(var(--neu-radius)*.6);--bs-nav-pills-link-active-bg:var(--neu-bg);--bs-nav-pills-link-active-color:var(--neu-accent);gap:6px}
.neu-side .nav-link{display:flex;align-items:center;gap:.75rem;min-height:44px;border:var(--neu-bw) solid transparent;transition:box-shadow .2s,color .2s}
.neu-side .nav-link:hover{box-shadow:var(--neu-raised-sm)}
.neu-side .nav-link.active{border-color:var(--neu-accent);box-shadow:var(--neu-inset-sm)}
.neu-side .nav-link.active::after{content:"";width:8px;height:8px;margin-left:auto;border-radius:50%;background:currentColor}
.neu-side .nav-link:focus-visible{outline:3px solid var(--neu-accent);outline-offset:2px;box-shadow:none}
.neu-side .badge{--bs-badge-font-size:.75rem;--bs-badge-padding-x:.45rem;--bs-badge-padding-y:.25rem;--bs-badge-color:var(--neu-on-accent);margin-left:auto;min-width:24px;background:var(--neu-accent)}
/* Switches: Bootstrap .form-check.form-switch. The knob is a token-coloured radial gradient so it follows light and dark */
.neu-switch{display:flex;align-items:center;gap:.9rem;min-height:0;margin:0;padding:0}
.neu-switch .form-check-label{flex:1;min-width:0;font-weight:600;line-height:1.3;cursor:pointer}
.neu-switch .form-check-label small{display:block;margin-top:.15rem;font-size:.82rem;font-weight:400;color:var(--neu-muted)}
.neu-switch .neu-switch-state{min-width:2.1em;font-size:.75rem;font-weight:700;letter-spacing:.08em;text-align:right;text-transform:uppercase;color:var(--neu-muted)}
.neu-switch .neu-switch-state::after{content:"Off"}
.neu-switch.form-switch .form-check-input,.neu-switch.form-switch .form-check-input:focus,.neu-switch.form-switch .form-check-input:checked{
--bs-form-switch-bg:radial-gradient(circle at 50% 50%,var(--neu-knob) 0 10.5px,var(--neu-knob-edge) 11px 12px,transparent 12.5px),radial-gradient(circle at 58% 60%,var(--neu-dark) 0 9px,transparent 14px)}
.neu-switch .form-check-input{--neu-knob:var(--neu-bg);--neu-knob-edge:var(--neu-edge);
order:3;flex:none;float:none;width:58px;height:32px;margin:0;border:var(--neu-bw) solid var(--neu-edge);border-radius:999px;
background-color:var(--neu-bg);background-size:30px 30px;background-position:left center;cursor:pointer;
box-shadow:var(--neu-inset-sm);transition:background-position .25s,background-color .25s,border-color .25s}
.neu-switch .form-check-input:focus{border-color:var(--neu-edge);box-shadow:var(--neu-inset-sm)}
.neu-switch .form-check-input:checked{--neu-knob:var(--neu-on-accent);--neu-knob-edge:var(--neu-on-accent);background-color:var(--neu-accent);border-color:var(--neu-accent);background-position:right center}
.neu-switch .form-check-input:focus-visible{outline:3px solid var(--neu-accent);outline-offset:3px}
.neu-switch .form-check-input:checked~.neu-switch-state{color:var(--neu-accent)}
.neu-switch .form-check-input:checked~.neu-switch-state::after{content:"On"}
/* Stat tile: icon in a pressed well, label, a big value, and a change line that says which way in words */
.neu-stat{display:grid;gap:.2rem;padding:1.25rem;border:var(--neu-bw) solid var(--neu-line);border-radius:var(--neu-radius);background:var(--neu-bg);box-shadow:var(--neu-raised)}
.neu-stat__icon{display:grid;place-items:center;width:44px;height:44px;margin-bottom:.6rem;border-radius:50%;color:var(--neu-accent);box-shadow:var(--neu-inset-sm)}
.neu-stat__label{margin:0;font-size:.85rem;font-weight:600;color:var(--neu-muted)}
.neu-stat__value{margin:0;font-size:1.9rem;font-weight:700;line-height:1.15;letter-spacing:-.02em;font-variant-numeric:tabular-nums}
.neu-stat__value small{font-size:.9rem;font-weight:600;letter-spacing:0;color:var(--neu-muted)}
.neu-stat__delta{display:flex;align-items:center;gap:.3rem;margin:.35rem 0 0;font-size:.82rem;font-weight:600;color:var(--neu-muted)}
.neu-stat__delta .neu-ico{width:16px;height:16px}
.neu-stat__delta--good{color:var(--neu-success)}
.neu-stat__delta--warn{color:var(--neu-warning)}
/* 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)}
/* Segmented control: .btn-group of .btn-check radios. The chosen option rises, turns accent and gains a dot */
.neu-seg{min-width:0;margin:0;padding:0;border:0}
.neu-seg legend{float:none;width:auto;margin-bottom:.6rem;padding:0;font-size:.88rem;font-weight:600}
.neu-seg .btn-group{display:flex;flex-wrap:wrap;gap:6px;width:100%;padding:6px;border:var(--neu-bw) solid var(--neu-edge);border-radius:calc(var(--neu-radius)*.75);box-shadow:var(--neu-inset-sm)}
.neu-seg .btn{--bs-btn-padding-x:.9rem;--bs-btn-padding-y:.4rem;--bs-btn-font-size:.9rem;--bs-btn-font-weight:600;--bs-btn-color:var(--neu-muted);--bs-btn-border-color:transparent;
--bs-btn-hover-color:var(--neu-ink);--bs-btn-hover-border-color:transparent;--bs-btn-active-color:var(--neu-accent);--bs-btn-active-border-color:var(--neu-accent);--bs-btn-active-bg:transparent;
flex:1 1 auto;display:flex;align-items:center;justify-content:center;gap:.4rem;min-height:40px;border-radius:calc(var(--neu-radius)*.5)!important;margin:0!important;transition:box-shadow .2s,color .2s}
.neu-seg .btn-check:checked+.btn{box-shadow:var(--neu-raised-sm)}
.neu-seg .btn-check:checked+.btn::before{content:"";width:7px;height:7px;border-radius:50%;background:currentColor}
.neu-seg .btn-check:focus-visible+.btn{outline:3px solid var(--neu-accent);outline-offset:2px;box-shadow:var(--neu-raised-sm)}
/* Mini player: art in a pressed well, title, a progress groove, and a play / pause toggle (checkbox) */
.neu-mini{display:flex;align-items:center;gap:1rem;padding:1rem 1.1rem;border:var(--neu-bw) solid var(--neu-line);border-radius:var(--neu-radius);background:var(--neu-bg);box-shadow:var(--neu-raised)}
.neu-mini__art{flex:none;width:52px;height:52px;border-radius:calc(var(--neu-radius)*.6);box-shadow:var(--neu-inset-sm);
background:radial-gradient(circle at 30% 30%,#f59e0b 0 18%,transparent 19%),linear-gradient(160deg,#4f46e5,#0e7490)}
.neu-mini__meta{flex:1;min-width:0}
.neu-mini__title{margin:0;font-weight:700;line-height:1.25;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.neu-mini__artist{margin:0;font-size:.82rem;color:var(--neu-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.neu-mini__bar{height:8px;margin-top:.55rem;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(--p,40%) 100% no-repeat}
.neu-play{position:relative;flex:none;display:inline-flex}
.neu-play__face{display:grid;place-items:center;width:52px;height:52px;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-sm);cursor:pointer;transition:box-shadow .2s}
.neu-play__face .neu-ico{width:24px;height:24px}
.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}
/* Alerts (.alert with --bs-alert-* set to the kit): the type is carried by an icon, a title and hidden "Warning:" style text, not by colour alone */
.neu-alerts{display:grid;gap:16px}
.alert.neu-alert{--bs-alert-bg:var(--neu-bg);--bs-alert-color:var(--neu-ink);--bs-alert-border-color:var(--neu-line);--bs-alert-margin-bottom:0;--tone:var(--neu-accent);position:relative;display:flex;align-items:flex-start;gap:.9rem;padding:1rem 1.1rem 1rem 1.35rem;border:var(--neu-bw) solid var(--neu-line);
border-radius:var(--neu-radius);background:var(--neu-bg);box-shadow:var(--neu-raised-sm)}
.neu-alert::before{content:"";position:absolute;left:.55rem;top:1rem;bottom:1rem;width:4px;border-radius:4px;background:var(--tone)}
.alert.neu-alert--success{--tone:var(--neu-success)}.alert.neu-alert--warning{--tone:var(--neu-warning)}.alert.neu-alert--danger{--tone:var(--neu-danger)}
.neu-alert__icon{display:grid;place-items:center;flex:none;width:40px;height:40px;border-radius:50%;color:var(--tone);box-shadow:var(--neu-inset-sm)}
.neu-alert .alert-heading{margin:0;font-size:1rem;color:var(--neu-ink);font-weight:700;line-height:1.3}
.neu-alert__text{margin:.15rem 0 0;font-size:.9rem;color:var(--neu-muted)}
/* Dashboard shell. The theme switch is a checkbox with class .neu-theme-input: the token block's
[data-neu]:has(.neu-theme-input:checked) rule swaps every token to dark, so no script is involved */
.neu-home{display:grid;grid-template-columns:250px minmax(0,1fr);gap:32px;min-height:100vh;padding:28px}
.neu-home__side{position:sticky;top:28px;align-self:start}
.neu-home__brand{display:flex;align-items:center;gap:.65rem;margin:0 .4rem .9rem;font-size:1.1rem;font-weight:800}
.neu-home__mark{display:grid;place-items:center;width:40px;height:40px;border-radius:50%;color:var(--neu-accent);box-shadow:var(--neu-inset-sm)}
.neu-home__main{display:grid;grid-template-columns:minmax(0,1fr);align-content:start;gap:28px;min-width:0}
.neu-home__head{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:16px 24px}
.neu-home__title{margin:0;font-size:1.75rem;font-weight:800;line-height:1.2;letter-spacing:-.02em}
.neu-home__sub{margin:.25rem 0 0;color:var(--neu-muted)}
.neu-home__theme{min-width:250px;padding:.7rem 1rem;border:var(--neu-bw) solid var(--neu-line);border-radius:calc(var(--neu-radius)*.75);box-shadow:var(--neu-raised-sm)}
.neu-home__stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,190px),1fr));gap:22px}
.neu-home__grid{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.25fr);gap:22px;align-items:start}
.neu-home__wide{grid-column:1/-1}
.neu-home__row{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,300px),1fr));gap:22px;align-items:center}
.neu-home__row>:last-child{grid-column:1/-1}
.neu-home__panel .card-body{display:grid;grid-template-columns:minmax(0,1fr);gap:1.1rem}
.neu-home__h{display:flex;justify-content:space-between;align-items:baseline;gap:.5rem;margin:0;font-size:1rem;font-weight:700}
.neu-home__h small{font-size:.82rem;font-weight:600;color:var(--neu-muted)}
.neu-home__lights{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px}
@media (max-width:1100px){.neu-home__grid{grid-template-columns:minmax(0,1fr)}}
@media (max-width:900px){.neu-home{grid-template-columns:minmax(0,1fr);gap:24px;padding:16px}.neu-home__side{position:static}
.neu-home .neu-side .nav{flex-direction:row!important;flex-wrap:wrap}.neu-home .neu-side__heading{display:none}}
.neu-home .btn-neu-tile{min-width:0}
.neu-home__theme.form-check{padding:.7rem 1rem}
@media (prefers-reduced-motion:reduce){.btn-neu,.neu-side .nav-link,.neu-switch .form-check-input,.btn-neu-tile .neu-tile-icon,.neu-seg .btn,.neu-play__face,.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));
});
});
});
/* ── 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);
}
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));
});
});
});
About this set
A full smart-home dashboard assembled from the kit, with a light and dark switch that needs no JavaScript. A sidebar with the brand, grouped links and an unread count sits beside a greeting, four stat tiles for energy, solar, humidity and security, a climate panel with the thermostat dial, a lights panel with four switch tiles, and a scene and music panel with a segmented scene picker, a mini player and a window-open warning. The dark mode switch is a checkbox with the class neu-theme-input; the token block’s [data-neu]:has(.neu-theme-input:checked) rule swaps every token to the dark set, so the whole dashboard changes theme in CSS alone. Use it as a starting point for a home control app, or copy single panels into your own layout. On screens narrower than 900 pixels the sidebar moves above the content and its links wrap into rows, and the panels stack into one column. The thermostat buttons use the same short script as the widgets set; everything else is HTML and CSS.
What’s included
Light and dark theme switch built on :has(), no JavaScript
Sidebar, stat tiles, thermostat, light switches, scenes, player and alert
Uses the same tokens and components as the rest of the kit
Stacks to one column with a wrapping nav under 900 pixels
Bootstrap version on .card, .nav-pills, .form-switch and .btn-check
Accessibility
The page has a labelled main navigation with aria-current, one h1 and an h2 for each panel, and every panel is a section named by its heading. The theme switch is a native checkbox with role="switch", the scene picker is a radio group with a legend, and the lights are named switches. Focus shows a 3px accent ring in both themes, transitions stop under prefers-reduced-motion, and prefers-contrast: more outlines the dashboard in either theme.
Customise it
Change the --neu-* tokens on :root for the light theme and in the [data-neu] dark rule for the dark one. Panel widths come from .neu-home grid columns; in Tailwind edit the grid-cols-[250px_minmax(0,1fr)] utility on the root.