/* ── 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}
/* 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)}
/* Toasts: a polite live region; each toast can be dismissed with its close button */
.neu-toasts{display:grid;gap:14px}
.neu-toast{display:flex;align-items:center;gap:.85rem;padding:.85rem .85rem .85rem 1rem;border:var(--neu-bw) solid var(--neu-line);border-radius:var(--neu-radius);background:var(--neu-bg);box-shadow:var(--neu-raised)}
.neu-toast[hidden]{display:none}
.neu-toast__icon{display:grid;place-items:center;flex:none;width:40px;height:40px;border-radius:50%;background:var(--neu-accent);color:var(--neu-on-accent);box-shadow:var(--neu-raised-sm)}
.neu-toast__text{flex:1;min-width:0;margin:0;font-size:.92rem;line-height:1.35}
.neu-toast__text strong{display:block}
.neu-toast__text span{color:var(--neu-muted)}
/* Tooltips: shown on hover and keyboard focus, hidden again with Escape; the trigger points at the bubble with aria-describedby */
.neu-tips{display:flex;flex-wrap:wrap;align-items:flex-end;gap:22px;padding-top:84px}
.neu-tip{position:relative;display:inline-flex}
.neu-tip__bubble{position:absolute;left:50%;bottom:calc(100% + 12px);z-index:5;width:max-content;max-width:210px;padding:.55rem .8rem;border-radius:calc(var(--neu-radius)*.6);
background:var(--neu-ink);color:var(--neu-bg);font-size:.85rem;font-weight:500;line-height:1.35;text-align:center;
box-shadow:var(--neu-raised-sm);opacity:0;visibility:hidden;transform:translate(-50%,4px);transition:opacity .15s,transform .15s,visibility .15s;pointer-events:none}
.neu-tip__bubble::after{content:"";position:absolute;left:50%;top:100%;width:10px;height:10px;margin:-5px 0 0 -5px;background:inherit;transform:rotate(45deg)}
.neu-tip:hover .neu-tip__bubble,.neu-tip:focus-within .neu-tip__bubble,.neu-tip--open .neu-tip__bubble{opacity:1;visibility:visible;transform:translate(-50%,0)}
.neu-tip.is-dismissed .neu-tip__bubble{opacity:0;visibility:hidden}
.neu-tip__label{display:inline-flex;align-items:center;gap:.5rem;font-weight:600}
/* Dropdown: a disclosure button (aria-expanded) and a raised panel of buttons; Escape or a click outside closes it */
.neu-dropdown{position:relative;display:inline-block}
.neu-dropdown__btn .neu-ico:last-child{transition:transform .2s}
.neu-dropdown__btn[aria-expanded="true"]{border-color:var(--neu-accent);box-shadow:var(--neu-inset-sm)}
.neu-dropdown__btn[aria-expanded="true"] .neu-ico:last-child{transform:rotate(180deg)}
.neu-menu{position:absolute;top:calc(100% + 12px);left:0;z-index:10;display:grid;gap:4px;min-width:230px;margin:0;padding:.5rem;list-style:none;
border:var(--neu-bw) solid var(--neu-line);border-radius:var(--neu-radius);background:var(--neu-bg);box-shadow:var(--neu-raised)}
.neu-menu[hidden]{display:none}
.neu-menu__item{display:flex;align-items:center;gap:.7rem;width:100%;min-height:42px;padding:.5rem .75rem;border:var(--neu-bw) solid transparent;border-radius:calc(var(--neu-radius)*.55);
background:none;color:var(--neu-ink);font-family:inherit;font-size:.92rem;font-weight:600;text-align:left;cursor:pointer}
.neu-menu__item .neu-ico{color:var(--neu-muted)}
.neu-menu__item:hover{box-shadow:var(--neu-inset-sm)}
.neu-menu__item:focus-visible{outline:3px solid var(--neu-accent);outline-offset:-1px}
.neu-menu__item--danger,.neu-menu__item--danger .neu-ico{color:var(--neu-danger)}
.neu-menu__sep{height:1px;margin:.3rem .5rem;background:var(--neu-edge);opacity:.5}
/* Modal: the native <dialog>. showModal() traps focus, makes the page inert and closes on Escape;
buttons inside <form method="dialog"> close it without script */
.neu-dialog{width:min(100% - 32px,420px);padding:0;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),0 30px 60px -20px rgba(15,23,42,.35)}
.neu-dialog::backdrop{background:rgba(30,41,59,.5)}
.neu-dialog[open]{animation:neu-pop .2s ease-out}
@keyframes neu-pop{from{opacity:0;transform:translateY(8px) scale(.98)}}
.neu-dialog__body{position:relative;display:grid;justify-items:center;gap:.6rem;margin:0;padding:2rem 1.75rem 1.75rem;text-align:center}
.neu-dialog__close{position:absolute;top:.9rem;right:.9rem}
.neu-dialog__icon{display:grid;place-items:center;width:64px;height:64px;margin-bottom:.4rem;border-radius:50%;color:var(--neu-accent);box-shadow:var(--neu-inset)}
.neu-dialog__icon .neu-ico{width:28px;height:28px}
.neu-dialog__title{margin:0;font-size:1.3rem;font-weight:700;line-height:1.3}
.neu-dialog__text{margin:0;color:var(--neu-muted)}
.neu-dialog__actions{display:flex;flex-wrap:wrap;justify-content:center;gap:14px;margin-top:1rem}
@media (prefers-reduced-motion:reduce){.neu-btn,.neu-tip__bubble,.neu-dropdown__btn .neu-ico,.neu-dialog[open]{transition:none;animation:none}}
JavaScript
// Modal: any button with data-neu-open="dialog-id" opens that <dialog>; focus returns to the button on close
document.querySelectorAll('[data-neu-open]').forEach(function (btn) {
var dialog = document.getElementById(btn.getAttribute('data-neu-open'));
if (!dialog) return;
btn.addEventListener('click', function () {
dialog.showModal();
dialog.addEventListener('close', function () { btn.focus(); }, { once: true });
});
dialog.addEventListener('click', function (e) { if (e.target === dialog) dialog.close(); }); // backdrop click
});
// Tooltips: Escape hides the visible tooltip until the pointer or focus leaves it
document.addEventListener('keydown', function (e) {
if (e.key !== 'Escape') return;
document.querySelectorAll('[data-neu-tip]').forEach(function (t) { if (t.matches(':hover, :focus-within')) t.classList.add('is-dismissed'); });
});
document.querySelectorAll('[data-neu-tip]').forEach(function (t) {
['mouseleave', 'focusout'].forEach(function (ev) { t.addEventListener(ev, function () { t.classList.remove('is-dismissed'); }); });
});
// Toasts: a close button removes its toast
document.querySelectorAll('[data-neu-dismiss]').forEach(function (b) {
b.addEventListener('click', function () { b.closest('[data-neu-toast]').hidden = true; });
});
// Dropdown: toggle on click; Escape (focus back to the button) or a click outside closes it
document.querySelectorAll('[data-neu-dropdown]').forEach(function (btn) {
var menu = document.getElementById(btn.getAttribute('aria-controls'));
function set(open) { btn.setAttribute('aria-expanded', open); menu.hidden = !open; }
btn.addEventListener('click', function () { set(btn.getAttribute('aria-expanded') !== 'true'); });
document.addEventListener('click', function (e) { if (!btn.parentNode.contains(e.target)) set(false); });
btn.parentNode.addEventListener('keydown', function (e) { if (e.key === 'Escape' && btn.getAttribute('aria-expanded') === 'true') { set(false); btn.focus(); } });
});
/* ── 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}
/* 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)}
/* Toasts (.toast, closed by Bootstrap's data-bs-dismiss="toast"): a polite live region; each toast can be dismissed with its close button */
.neu-toasts{display:grid;gap:14px}
.toast.neu-toast{--bs-toast-max-width:100%;--bs-toast-bg:var(--neu-bg);--bs-toast-color:var(--neu-ink);--bs-toast-border-color:var(--neu-line);--bs-toast-border-width:var(--neu-bw);--bs-toast-border-radius:var(--neu-radius);--bs-toast-box-shadow:var(--neu-raised);--bs-toast-font-size:1rem;width:100%;display:flex;align-items:center;gap:.85rem;padding:.85rem .85rem .85rem 1rem;border:var(--neu-bw) solid var(--neu-line);border-radius:var(--neu-radius);background:var(--neu-bg);box-shadow:var(--neu-raised)}
.toast.neu-toast:not(.show){display:none}
.neu-toast__icon{display:grid;place-items:center;flex:none;width:40px;height:40px;border-radius:50%;background:var(--neu-accent);color:var(--neu-on-accent);box-shadow:var(--neu-raised-sm)}
.neu-toast__text{flex:1;min-width:0;margin:0;font-size:.92rem;line-height:1.35}
.neu-toast__text strong{display:block}
.neu-toast__text span{color:var(--neu-muted)}
/* Tooltips (Bootstrap .tooltip markup, shown by CSS): shown on hover and keyboard focus, hidden again with Escape; the trigger points at the bubble with aria-describedby */
.neu-tips{display:flex;flex-wrap:wrap;align-items:flex-end;gap:22px;padding-top:84px}
.neu-tip{position:relative;display:inline-flex}
.neu-tip .tooltip{position:absolute;left:50%;top:auto;bottom:calc(100% + 12px);z-index:5;width:max-content;max-width:210px;padding:0;font-family:inherit;font-size:.85rem;font-weight:500;line-height:1.35;opacity:0;visibility:hidden;transform:translate(-50%,4px);transition:opacity .15s,transform .15s,visibility .15s;pointer-events:none}
.neu-tip .tooltip-inner{display:block;max-width:none;padding:.55rem .8rem;border-radius:calc(var(--neu-radius)*.6);background:var(--neu-ink);color:var(--neu-bg);box-shadow:var(--neu-raised-sm)}
.neu-tip .tooltip-inner::after{content:"";position:absolute;left:50%;top:100%;width:10px;height:10px;margin:-5px 0 0 -5px;background:var(--neu-ink);transform:rotate(45deg)}
.neu-tip:hover .tooltip,.neu-tip:focus-within .tooltip,.neu-tip--open .tooltip{opacity:1;visibility:visible;transform:translate(-50%,0)}
.neu-tip.is-dismissed .tooltip{opacity:0;visibility:hidden}
.neu-tip__label{display:inline-flex;align-items:center;gap:.5rem;font-weight:600}
/* Dropdown: Bootstrap .dropdown with its plugin (arrow keys, Escape, click outside). Open = pressed in, accent edge */
.neu-dropdown .dropdown-toggle::after{display:none}
.neu-dropdown__btn .neu-ico:last-child{transition:transform .2s}
.neu-dropdown__btn.show{--neu-btn-shadow:var(--neu-inset-sm);border-color:var(--neu-accent)!important}
.neu-dropdown__btn.show .neu-ico:last-child{transform:rotate(180deg)}
.neu-dropdown .dropdown-menu{--bs-dropdown-min-width:230px;--bs-dropdown-padding-x:.5rem;--bs-dropdown-padding-y:.5rem;--bs-dropdown-spacer:12px;--bs-dropdown-bg:var(--neu-bg);
--bs-dropdown-border-color:var(--neu-line);--bs-dropdown-border-width:var(--neu-bw);--bs-dropdown-border-radius:var(--neu-radius);--bs-dropdown-box-shadow:var(--neu-raised);
--bs-dropdown-link-color:var(--neu-ink);--bs-dropdown-link-hover-color:var(--neu-ink);--bs-dropdown-link-hover-bg:transparent;--bs-dropdown-link-active-bg:transparent;--bs-dropdown-link-active-color:var(--neu-ink);
--bs-dropdown-divider-bg:var(--neu-edge);--bs-dropdown-divider-margin-y:.3rem;--bs-dropdown-item-padding-x:.75rem;--bs-dropdown-item-padding-y:.5rem;--bs-dropdown-item-border-radius:calc(var(--neu-radius)*.55);
box-shadow:var(--bs-dropdown-box-shadow)}
.neu-dropdown .dropdown-menu{display:none}.neu-dropdown .dropdown-menu.show{display:grid;gap:4px}
.neu-dropdown .dropdown-item{display:flex;align-items:center;gap:.7rem;min-height:42px;border:var(--neu-bw) solid transparent;font-size:.92rem;font-weight:600}
.neu-dropdown .dropdown-item .neu-ico{color:var(--neu-muted)}
.neu-dropdown .dropdown-item:hover,.neu-dropdown .dropdown-item:focus{box-shadow:var(--neu-inset-sm)}
.neu-dropdown .dropdown-item:focus-visible{outline:3px solid var(--neu-accent);outline-offset:-1px}
.neu-dropdown .dropdown-item.text-danger,.neu-dropdown .dropdown-item.text-danger .neu-ico{color:var(--neu-danger)!important}
.neu-dropdown .dropdown-divider{margin:.3rem .5rem;opacity:.5}
/* Modal: the native <dialog>. showModal() traps focus, makes the page inert and closes on Escape;
buttons inside <form method="dialog"> close it without script */
.neu-dialog{width:min(100% - 32px,420px);padding:0;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),0 30px 60px -20px rgba(15,23,42,.35)}
.neu-dialog::backdrop{background:rgba(30,41,59,.5)}
.neu-dialog[open]{animation:neu-pop .2s ease-out}
@keyframes neu-pop{from{opacity:0;transform:translateY(8px) scale(.98)}}
.neu-dialog__body{position:relative;display:grid;justify-items:center;gap:.6rem;margin:0;padding:2rem 1.75rem 1.75rem;text-align:center}
.neu-dialog__close{position:absolute;top:.9rem;right:.9rem}
.neu-dialog h2,.neu-dialog p{margin:0}
.neu-dialog__icon{display:grid;place-items:center;width:64px;height:64px;margin-bottom:.4rem;border-radius:50%;color:var(--neu-accent);box-shadow:var(--neu-inset)}
.neu-dialog__icon .neu-ico{width:28px;height:28px}
.neu-dialog__title{margin:0;font-size:1.3rem;font-weight:700;line-height:1.3}
.neu-dialog__text{margin:0;color:var(--neu-muted)}
.neu-dialog__actions{display:flex;flex-wrap:wrap;justify-content:center;gap:14px;margin-top:1rem}
@media (prefers-reduced-motion:reduce){.btn-neu,.neu-tip .tooltip,.neu-dropdown__btn .neu-ico,.neu-dialog[open]{transition:none;animation:none}}
JavaScript
// Modal: any button with data-neu-open="dialog-id" opens that <dialog>; focus returns to the button on close
document.querySelectorAll('[data-neu-open]').forEach(function (btn) {
var dialog = document.getElementById(btn.getAttribute('data-neu-open'));
if (!dialog) return;
btn.addEventListener('click', function () {
dialog.showModal();
dialog.addEventListener('close', function () { btn.focus(); }, { once: true });
});
dialog.addEventListener('click', function (e) { if (e.target === dialog) dialog.close(); }); // backdrop click
});
// Tooltips: Escape hides the visible tooltip until the pointer or focus leaves it
document.addEventListener('keydown', function (e) {
if (e.key !== 'Escape') return;
document.querySelectorAll('[data-neu-tip]').forEach(function (t) { if (t.matches(':hover, :focus-within')) t.classList.add('is-dismissed'); });
});
document.querySelectorAll('[data-neu-tip]').forEach(function (t) {
['mouseleave', 'focusout'].forEach(function (ev) { t.addEventListener(ev, function () { t.classList.remove('is-dismissed'); }); });
});
// Dropdown: create the instance now, so a menu that starts open also closes on an outside click
document.querySelectorAll('[data-bs-toggle="dropdown"]').forEach(function (el) { bootstrap.Dropdown.getOrCreateInstance(el); });
/* ── 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);
}
@keyframes neu-pop{from{opacity:0;transform:translateY(8px) scale(.98)}}
JavaScript
// Modal: any button with data-neu-open="dialog-id" opens that <dialog>; focus returns to the button on close
document.querySelectorAll('[data-neu-open]').forEach(function (btn) {
var dialog = document.getElementById(btn.getAttribute('data-neu-open'));
if (!dialog) return;
btn.addEventListener('click', function () {
dialog.showModal();
dialog.addEventListener('close', function () { btn.focus(); }, { once: true });
});
dialog.addEventListener('click', function (e) { if (e.target === dialog) dialog.close(); }); // backdrop click
});
// Tooltips: Escape hides the visible tooltip until the pointer or focus leaves it
document.addEventListener('keydown', function (e) {
if (e.key !== 'Escape') return;
document.querySelectorAll('[data-neu-tip]').forEach(function (t) { if (t.matches(':hover, :focus-within')) t.classList.add('is-dismissed'); });
});
document.querySelectorAll('[data-neu-tip]').forEach(function (t) {
['mouseleave', 'focusout'].forEach(function (ev) { t.addEventListener(ev, function () { t.classList.remove('is-dismissed'); }); });
});
// Toasts: a close button removes its toast
document.querySelectorAll('[data-neu-dismiss]').forEach(function (b) {
b.addEventListener('click', function () { b.closest('[data-neu-toast]').hidden = true; });
});
// Dropdown: toggle on click; Escape (focus back to the button) or a click outside closes it
document.querySelectorAll('[data-neu-dropdown]').forEach(function (btn) {
var menu = document.getElementById(btn.getAttribute('aria-controls'));
function set(open) { btn.setAttribute('aria-expanded', open); menu.hidden = !open; }
btn.addEventListener('click', function () { set(btn.getAttribute('aria-expanded') !== 'true'); });
document.addEventListener('click', function (e) { if (!btn.parentNode.contains(e.target)) set(false); });
btn.parentNode.addEventListener('keydown', function (e) { if (e.key === 'Escape' && btn.getAttribute('aria-expanded') === 'true') { set(false); btn.focus(); } });
});
About this set
Feedback and overlay components for a soft UI. Four alerts for information, success, a warning and an error carry their type in an icon, a coloured bar, a title and hidden text such as Warning:, so colour is never the only signal. Two toasts sit in a polite live region, one with an Undo action, and each closes with its own button. Tooltips appear above an icon button and beside a help icon on hover and keyboard focus, are tied to their trigger with aria-describedby, and hide again with Escape; the first is pinned open in the preview. A dropdown opens a raised panel of room actions, with Remove room set apart in red. The modal is the native dialog element: showModal() traps focus, makes the page behind inert and closes on Escape, and its buttons close it through a form with method dialog. A short script opens it and returns focus to the button. Use the alerts for persistent status, toasts for confirmations, and the dialog to confirm actions such as turning off every light. The Bootstrap version uses .alert, .toast and .dropdown with Bootstrap’s plugins.
What’s included
Native dialog with focus trap, Escape to close and focus return
Alerts that state their type in an icon, a title and hidden text
Toasts in a polite live region with working close buttons
Tooltips on hover and focus that hide with Escape
Dropdown that closes on Escape or an outside click
Accessibility
The dialog has aria-labelledby and aria-describedby and its trigger says aria-haspopup="dialog". Toasts are announced politely, tooltips use role="tooltip" and aria-describedby, and the dropdown button reports aria-expanded. Every control shows a 3px accent focus ring. Animations and transitions stop under prefers-reduced-motion, and prefers-contrast: more outlines every panel.
Customise it
Change the --neu-* tokens for the surface and accent, and --neu-success, --neu-warning and --neu-danger for the alert tones. Bootstrap maps them onto --bs-alert-*, --bs-toast-* and --bs-dropdown-*; Tailwind uses shadow-neu, text-neu-warning and a backdrop: utility on the dialog.