/* ── Neobrutalism UI kit tokens (uiCookies) — identical in every set ─────────────
Thick ink borders, hard offset shadows and flat saturated fills on cream paper.
Change a token and the whole kit follows; the border and shadow recipes below are built from them. */
:root{
--nb-paper:#fff6e0; /* page background, cream */
--nb-surface:#ffffff; /* card, field and button faces */
--nb-ink:#111111; /* text, 17.5:1 on paper */
--nb-muted:#454036; /* secondary text, 9.6:1 on paper */
--nb-line:#111111; /* border colour */
--nb-bw:3px; /* border width */
--nb-shadow-x:4px; /* hard shadow offset; pressed controls move this far into it */
--nb-shadow-y:4px;
--nb-shadow-color:#111111;
--nb-radius:12px; /* cards and panels */
--nb-radius-sm:8px; /* buttons and fields */
--nb-focus:#1d4ed8; /* dashed focus outline, 6.2:1 on paper, never the shadow */
--nb-on-fill:#111111; /* text on every palette fill below, 6.8:1 or more */
--nb-on-fill-muted:#2b2822;/* secondary text on fills, 5.2:1 or more */
--nb-lemon:#ffe14d;--nb-pink:#ff90d0;--nb-cyan:#62e2e8;--nb-lime:#b6f36a;
--nb-orange:#ffa94d;--nb-violet:#b9a3ff;--nb-red:#ff6b6b;
--nb-blue:#2448e0;--nb-on-blue:#ffffff; /* the only fill that takes white text, 6.8:1 */
--nb-danger:#b42318; /* destructive text, 6.1:1 on paper */
--nb-font:"Space Grotesk","Archivo","Helvetica Neue",Arial,system-ui,sans-serif;
--nb-font-display:"Archivo Black","Arial Black","Helvetica Neue",Arial,sans-serif;
}
/* Dark brutalism: data-nb="dark" on any wrapper, or a checked .nb-theme-input inside a [data-nb] element.
:is() is a forgiving list, so browsers without :has() still apply data-nb="dark". Fills keep dark text. */
[data-nb]:is([data-nb="dark"],:has(.nb-theme-input:checked)){
--nb-paper:#17150f;--nb-surface:#25221b;--nb-ink:#fbf3dc;--nb-muted:#d6ccb2;--nb-line:#fbf3dc;
--nb-shadow-color:#fbf3dc;--nb-focus:#8db4ff;--nb-danger:#ff9b8f;color-scheme:dark}
/* More contrast requested: secondary text turns full ink */
@media (prefers-contrast:more){:root,[data-nb]:is([data-nb],:has(.nb-theme-input:checked)){--nb-muted:var(--nb-ink);--nb-on-fill-muted:var(--nb-on-fill)}}
:root,[data-nb]{
--nb-border:var(--nb-bw) solid var(--nb-line);
--nb-shadow:var(--nb-shadow-x) var(--nb-shadow-y) 0 0 var(--nb-shadow-color);
--nb-shadow-sm:calc(var(--nb-shadow-x)*.5) calc(var(--nb-shadow-y)*.5) 0 0 var(--nb-shadow-color);
--nb-shadow-lg:calc(var(--nb-shadow-x)*1.75) calc(var(--nb-shadow-y)*1.75) 0 0 var(--nb-shadow-color);
--nb-shadow-hover:calc(var(--nb-shadow-x) + 2px) calc(var(--nb-shadow-y) + 2px) 0 0 var(--nb-shadow-color);
--nb-press:var(--nb-shadow-x) var(--nb-shadow-y); /* translate for a pressed control */
--nb-press-sm:calc(var(--nb-shadow-x)*.5) calc(var(--nb-shadow-y)*.5); /* …for one with the small shadow */
}
[data-nb]{background:var(--nb-paper);color:var(--nb-ink);font-family:var(--nb-font)}
/* Components */
:where([class*="nb-"]),:where([class*="nb-"])::before,:where([class*="nb-"])::after{box-sizing:border-box}
.nb-sr{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
.nb-ico{width:20px;height:20px;flex:none}
.nb-display{font-family:var(--nb-font-display);font-weight:900;letter-spacing:-.01em;line-height:1.05}
.nb-link{color:inherit;font-weight:700;text-decoration:underline;text-decoration-thickness:2px;text-underline-offset:3px}
.nb-link:hover{text-decoration-thickness:4px}
.nb-link:focus-visible{outline:3px dashed var(--nb-focus);outline-offset:3px;border-radius:2px}
/* Buttons: a face, a thick border and a hard shadow. Hover lifts it, pressing moves it into its shadow */
.nb-btn{--nb-fill:var(--nb-surface);--nb-text:var(--nb-ink);
display:inline-flex;align-items:center;justify-content:center;gap:.5rem;min-height:46px;padding:.6rem 1.2rem;
border:var(--nb-border);border-radius:var(--nb-radius-sm);background:var(--nb-fill);color:var(--nb-text);
font:700 1rem/1.2 var(--nb-font);text-decoration:none;cursor:pointer;box-shadow:var(--nb-shadow);
transition:translate .12s,box-shadow .12s}
.nb-btn:hover{translate:-2px -2px;box-shadow:var(--nb-shadow-hover)}
.nb-btn:active{translate:var(--nb-press);box-shadow:none}
.nb-btn:focus-visible{outline:3px dashed var(--nb-focus);outline-offset:5px}
.nb-btn--lemon{--nb-fill:var(--nb-lemon);--nb-text:var(--nb-on-fill)}
.nb-btn--pink{--nb-fill:var(--nb-pink);--nb-text:var(--nb-on-fill)}
.nb-btn--cyan{--nb-fill:var(--nb-cyan);--nb-text:var(--nb-on-fill)}
.nb-btn--lime{--nb-fill:var(--nb-lime);--nb-text:var(--nb-on-fill)}
.nb-btn--red{--nb-fill:var(--nb-red);--nb-text:var(--nb-on-fill)}
.nb-btn--blue{--nb-fill:var(--nb-blue);--nb-text:var(--nb-on-blue)}
.nb-btn--ink{--nb-fill:var(--nb-ink);--nb-text:var(--nb-paper)}
.nb-btn--sm{min-height:38px;padding:.4rem .85rem;font-size:.875rem}
.nb-btn--lg{min-height:56px;padding:.8rem 1.6rem;font-size:1.15rem}
.nb-btn--block{width:100%}
.nb-btn--icon{width:48px;height:48px;min-height:0;padding:0}
.nb-btn--icon-sm{width:38px;height:38px;box-shadow:var(--nb-shadow-sm)}
.nb-btn--icon-sm:hover{box-shadow:var(--nb-shadow)}
.nb-btn--icon-sm:active{translate:var(--nb-press-sm)}
.nb-btn--round{border-radius:50%}
.nb-btn:disabled{--nb-fill:var(--nb-paper);--nb-text:var(--nb-muted);border-style:dashed;box-shadow:none;translate:none;cursor:not-allowed}
/* Layout helpers */
.nb-cluster{display:flex;flex-wrap:wrap;align-items:center;gap:18px}
.nb-tiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:20px}
.nb-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,260px),1fr));gap:28px;align-items:start}
.nb-stack{display:grid;gap:1.25rem}
/* Fields: a white well with the thick border and a small shadow; focus adds a dashed outline */
.nb-field{display:grid;gap:.4rem;min-width:0}
.nb-label{font-weight:800;font-size:.95rem}
.nb-input,.nb-select,.nb-textarea{width:100%;min-height:48px;padding:.65rem .9rem;border:var(--nb-border);border-radius:var(--nb-radius-sm);background:var(--nb-surface);color:var(--nb-ink);
font:500 1rem/1.4 var(--nb-font);box-shadow:var(--nb-shadow-sm);transition:box-shadow .12s}
.nb-textarea{min-height:110px;resize:vertical}
.nb-input::placeholder,.nb-textarea::placeholder{color:var(--nb-muted);opacity:1}
.nb-input:focus,.nb-select:focus,.nb-textarea:focus{outline:3px dashed var(--nb-focus);outline-offset:4px;box-shadow:var(--nb-shadow)}
.nb-hint{font-size:.85rem;color:var(--nb-muted)}
.nb-row{display:flex;justify-content:space-between;align-items:baseline;gap:1rem}
.nb-control{position:relative}
.nb-control>.nb-ico{position:absolute;top:50%;left:14px;translate:0 -50%;pointer-events:none}
.nb-control>.nb-ico~.nb-input{padding-left:2.75rem}
.nb-select{appearance:none;padding-right:2.75rem;cursor:pointer}
.nb-control--select::after{content:"";position:absolute;top:50%;right:18px;width:10px;height:10px;border:solid var(--nb-ink);border-width:0 3px 3px 0;translate:0 -70%;rotate:45deg;pointer-events:none}
.nb-input[aria-invalid="true"]{box-shadow:var(--nb-shadow-x) var(--nb-shadow-y) 0 0 var(--nb-red)}
.nb-error{display:flex;align-items:center;gap:.45rem;width:fit-content;max-width:100%;padding:.3rem .65rem;border:2px solid var(--nb-line);border-radius:6px;background:var(--nb-red);color:var(--nb-on-fill);font-size:.85rem;font-weight:700}
.nb-error .nb-ico{width:16px;height:16px}
/* Alerts: the type is named by an icon, a bold title and hidden text, never by colour alone */
.nb-alert{display:flex;align-items:flex-start;gap:.85rem;padding:1rem 1.1rem;border:var(--nb-border);border-radius:var(--nb-radius);background:var(--nb-tone,var(--nb-cyan));color:var(--nb-on-fill);box-shadow:var(--nb-shadow);font-family:var(--nb-font)}
.nb-alert--success{--nb-tone:var(--nb-lime)}.nb-alert--warning{--nb-tone:var(--nb-lemon)}.nb-alert--error{--nb-tone:var(--nb-red)}
.nb-alert__icon{display:grid;place-items:center;flex:none;width:38px;height:38px;border-radius:50%;background:var(--nb-on-fill);color:var(--nb-tone,var(--nb-cyan))}
.nb-alert__body{flex:1;min-width:0}
.nb-alert__title{margin:0;font-weight:800}
.nb-alert__body p+p{margin:.15rem 0 0}
.nb-alert a{color:inherit;font-weight:800;text-decoration:underline;text-decoration-thickness:2px;text-underline-offset:3px}
.nb-alert a:focus-visible{outline:3px dashed var(--nb-on-fill);outline-offset:3px}
/* Toasts: in a polite live region, each with its own close button */
.nb-toasts{display:grid;gap:1rem;max-width:400px}
.nb-toast{display:flex;align-items:center;gap:.8rem;padding:.8rem .9rem;border:var(--nb-border);border-radius:var(--nb-radius);background:var(--nb-surface);color:var(--nb-ink);box-shadow:var(--nb-shadow-lg);font-family:var(--nb-font)}
.nb-toast[hidden]{display:none}
.nb-toast__icon{display:grid;place-items:center;flex:none;width:38px;height:38px;border:var(--nb-border);border-radius:8px;background:var(--nb-lime);color:var(--nb-on-fill);rotate:-5deg}
.nb-toast__icon--cyan{background:var(--nb-cyan)}
.nb-toast__body{flex:1;min-width:0;line-height:1.35}
.nb-toast__title{display:block;font-weight:800}
.nb-toast__text{font-size:.9rem;color:var(--nb-muted)}
.nb-toast__action{padding:.2rem .1rem;border:0;background:none;color:var(--nb-ink);font:800 .9rem/1.2 var(--nb-font);text-decoration:underline;text-decoration-thickness:2px;text-underline-offset:3px;cursor:pointer}
.nb-toast__action:focus-visible{outline:3px dashed var(--nb-focus);outline-offset:3px}
/* Tooltips: shown on hover and keyboard focus, tied to the trigger with aria-describedby, hidden again with Escape */
.nb-tip{position:relative;display:inline-flex}
.nb-tip__bubble{position:absolute;bottom:calc(100% + 14px);left:50%;z-index:5;width:max-content;max-width:220px;padding:.45rem .7rem;border:var(--nb-border);border-radius:var(--nb-radius-sm);
background:var(--nb-lemon);color:var(--nb-on-fill);font:700 .85rem/1.3 var(--nb-font);box-shadow:var(--nb-shadow-sm);translate:-50% 0;visibility:hidden;opacity:0;transition:opacity .12s,visibility .12s}
.nb-tip__bubble::after{content:"";position:absolute;top:100%;left:50%;width:12px;height:12px;margin:-5px 0 0 -6px;border:solid var(--nb-line);border-width:0 var(--nb-bw) var(--nb-bw) 0;background:var(--nb-lemon);rotate:45deg}
.nb-tip:hover .nb-tip__bubble,.nb-tip:focus-within .nb-tip__bubble,.nb-tip--open .nb-tip__bubble{visibility:visible;opacity:1}
.nb-tip.is-dismissed .nb-tip__bubble{visibility:hidden;opacity:0}
.nb-tip--start .nb-tip__bubble{left:0;translate:none}
.nb-tip--start .nb-tip__bubble::after{left:24px}
.nb-tip--right .nb-tip__bubble{bottom:auto;top:50%;left:calc(100% + 14px);translate:0 -50%}
.nb-tip--right .nb-tip__bubble::after{top:50%;left:0;margin:-6px 0 0 -5px;border-width:0 0 var(--nb-bw) var(--nb-bw)}
@media (max-width:600px){.nb-tip--right .nb-tip__bubble{top:auto;bottom:calc(100% + 14px);left:50%;translate:-50% 0}.nb-tip--right .nb-tip__bubble::after{top:100%;left:50%;margin:-5px 0 0 -6px;border-width:0 var(--nb-bw) var(--nb-bw) 0}}
/* Dropdown: a disclosure button and a list of actions; Escape or a click outside closes it */
.nb-dd{position:relative;display:inline-block}
.nb-dd__btn[aria-expanded="true"]{translate:var(--nb-press);box-shadow:none}
.nb-dd__btn .nb-ico:last-child{transition:rotate .12s}
.nb-dd__btn[aria-expanded="true"] .nb-ico:last-child{rotate:180deg}
.nb-dd__menu{position:absolute;top:calc(100% + 12px);left:0;z-index:10;display:grid;gap:2px;min-width:230px;margin:0;padding:.4rem;border:var(--nb-border);border-radius:var(--nb-radius);
background:var(--nb-surface);box-shadow:var(--nb-shadow-lg);list-style:none}
.nb-dd__menu[hidden]{display:none}
.nb-dd__item{display:flex;align-items:center;gap:.65rem;width:100%;padding:.5rem .65rem;border:2px solid transparent;border-radius:6px;background:none;color:var(--nb-ink);
font:700 .95rem/1.2 var(--nb-font);text-align:left;cursor:pointer}
.nb-dd__item kbd{margin-left:auto;padding:0 .35rem;border:2px solid currentColor;border-radius:4px;font:700 .72rem/1.4 var(--nb-font)}
.nb-dd__item:hover{border-color:var(--nb-line);background:var(--nb-lemon);color:var(--nb-on-fill)}
.nb-dd__item:focus-visible{outline:3px dashed var(--nb-focus);outline-offset:1px}
.nb-dd__item--danger{color:var(--nb-danger)}
.nb-dd__item--danger:hover{background:var(--nb-red);color:var(--nb-on-fill)}
.nb-dd__sep{height:0;margin:.3rem 0;border-top:var(--nb-border)}
/* Modal: the native <dialog>. showModal() traps focus, makes the page inert and closes on Escape;
buttons inside <form method="dialog"> close it without script */
.nb-dialog{width:min(100% - 32px,460px);padding:0;border:var(--nb-border);border-radius:var(--nb-radius);background:var(--nb-surface);color:var(--nb-ink);
box-shadow:calc(var(--nb-shadow-x)*2.5) calc(var(--nb-shadow-y)*2.5) 0 0 var(--nb-shadow-color);font-family:var(--nb-font);overflow:visible}
.nb-dialog::backdrop{background:rgba(17,17,17,.6)}
.nb-dialog[open]{animation:nb-drop .18s ease-out}
@keyframes nb-drop{from{translate:0 -14px;opacity:0}}
.nb-dialog form{margin:0}
.nb-dialog__head{display:flex;align-items:center;gap:.8rem;padding:1rem 1.2rem;border-bottom:var(--nb-border);border-radius:calc(var(--nb-radius) - var(--nb-bw)) calc(var(--nb-radius) - var(--nb-bw)) 0 0;background:var(--nb-pink);color:var(--nb-on-fill)}
.nb-dialog__title{flex:1;margin:0;font-size:1.3rem}
.nb-dialog__head .nb-btn{--nb-fill:var(--nb-surface);--nb-text:var(--nb-ink)}
.nb-dialog__body{padding:1.2rem 1.2rem .4rem}
.nb-dialog__body p{margin:0 0 .8rem}
.nb-dialog__foot{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:14px;padding:.8rem 1.2rem 1.3rem}
@media (prefers-reduced-motion:reduce){[class*="nb-"],[class*="nb-"]::before,[class*="nb-"]::after{transition:none!important;animation:none!important}}
JavaScript
// Modal: any button with data-nb-open="dialog-id" opens that <dialog>; focus returns to the button on close
document.querySelectorAll('[data-nb-open]').forEach(function (btn) {
var dialog = document.getElementById(btn.getAttribute('data-nb-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-nb-tip]').forEach(function (t) { if (t.matches(':hover, :focus-within')) t.classList.add('is-dismissed'); });
});
document.querySelectorAll('[data-nb-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-nb-dismiss]').forEach(function (b) {
b.addEventListener('click', function () { b.closest('[data-nb-toast]').hidden = true; });
});
// Dropdown: toggle on click; Escape (focus back to the button) or a click outside closes it
document.querySelectorAll('[data-nb-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(); } });
});
/* ── Neobrutalism UI kit tokens (uiCookies) — identical in every set ─────────────
Thick ink borders, hard offset shadows and flat saturated fills on cream paper.
Change a token and the whole kit follows; the border and shadow recipes below are built from them. */
:root{
--nb-paper:#fff6e0; /* page background, cream */
--nb-surface:#ffffff; /* card, field and button faces */
--nb-ink:#111111; /* text, 17.5:1 on paper */
--nb-muted:#454036; /* secondary text, 9.6:1 on paper */
--nb-line:#111111; /* border colour */
--nb-bw:3px; /* border width */
--nb-shadow-x:4px; /* hard shadow offset; pressed controls move this far into it */
--nb-shadow-y:4px;
--nb-shadow-color:#111111;
--nb-radius:12px; /* cards and panels */
--nb-radius-sm:8px; /* buttons and fields */
--nb-focus:#1d4ed8; /* dashed focus outline, 6.2:1 on paper, never the shadow */
--nb-on-fill:#111111; /* text on every palette fill below, 6.8:1 or more */
--nb-on-fill-muted:#2b2822;/* secondary text on fills, 5.2:1 or more */
--nb-lemon:#ffe14d;--nb-pink:#ff90d0;--nb-cyan:#62e2e8;--nb-lime:#b6f36a;
--nb-orange:#ffa94d;--nb-violet:#b9a3ff;--nb-red:#ff6b6b;
--nb-blue:#2448e0;--nb-on-blue:#ffffff; /* the only fill that takes white text, 6.8:1 */
--nb-danger:#b42318; /* destructive text, 6.1:1 on paper */
--nb-font:"Space Grotesk","Archivo","Helvetica Neue",Arial,system-ui,sans-serif;
--nb-font-display:"Archivo Black","Arial Black","Helvetica Neue",Arial,sans-serif;
}
/* Dark brutalism: data-nb="dark" on any wrapper, or a checked .nb-theme-input inside a [data-nb] element.
:is() is a forgiving list, so browsers without :has() still apply data-nb="dark". Fills keep dark text. */
[data-nb]:is([data-nb="dark"],:has(.nb-theme-input:checked)){
--nb-paper:#17150f;--nb-surface:#25221b;--nb-ink:#fbf3dc;--nb-muted:#d6ccb2;--nb-line:#fbf3dc;
--nb-shadow-color:#fbf3dc;--nb-focus:#8db4ff;--nb-danger:#ff9b8f;color-scheme:dark}
/* More contrast requested: secondary text turns full ink */
@media (prefers-contrast:more){:root,[data-nb]:is([data-nb],:has(.nb-theme-input:checked)){--nb-muted:var(--nb-ink);--nb-on-fill-muted:var(--nb-on-fill)}}
:root,[data-nb]{
--nb-border:var(--nb-bw) solid var(--nb-line);
--nb-shadow:var(--nb-shadow-x) var(--nb-shadow-y) 0 0 var(--nb-shadow-color);
--nb-shadow-sm:calc(var(--nb-shadow-x)*.5) calc(var(--nb-shadow-y)*.5) 0 0 var(--nb-shadow-color);
--nb-shadow-lg:calc(var(--nb-shadow-x)*1.75) calc(var(--nb-shadow-y)*1.75) 0 0 var(--nb-shadow-color);
--nb-shadow-hover:calc(var(--nb-shadow-x) + 2px) calc(var(--nb-shadow-y) + 2px) 0 0 var(--nb-shadow-color);
--nb-press:var(--nb-shadow-x) var(--nb-shadow-y); /* translate for a pressed control */
--nb-press-sm:calc(var(--nb-shadow-x)*.5) calc(var(--nb-shadow-y)*.5); /* …for one with the small shadow */
}
[data-nb]{background:var(--nb-paper);color:var(--nb-ink);font-family:var(--nb-font)}
/* Bootstrap's own variables mapped onto the kit tokens */
:root,[data-nb]{--bs-body-bg:var(--nb-paper);--bs-body-color:var(--nb-ink);--bs-body-font-family:var(--nb-font);--bs-emphasis-color:var(--nb-ink);
--bs-heading-color:var(--nb-ink);--bs-secondary-color:var(--nb-muted);--bs-tertiary-color:var(--nb-muted);
--bs-border-color:var(--nb-line);--bs-border-width:var(--nb-bw);--bs-border-radius:var(--nb-radius-sm);--bs-border-radius-lg:var(--nb-radius);
--bs-focus-ring-color:var(--nb-focus)}
/* Components */
.nb-ico{width:20px;height:20px;flex:none}
.nb-display{font-family:var(--nb-font-display);font-weight:900;letter-spacing:-.01em;line-height:1.05}
.nb-link{color:inherit;font-weight:700;text-decoration:underline;text-decoration-thickness:2px;text-underline-offset:3px}
.nb-link:hover{color:inherit;text-decoration-thickness:4px}
.nb-link:focus-visible{outline:3px dashed var(--nb-focus);outline-offset:3px;border-radius:2px}
/* Buttons: Bootstrap .btn with a .btn-nb variant set through --bs-btn-* variables.
Hover lifts it, pressing (or a checked .btn-check) moves it into its shadow */
.btn-nb{--nb-fill:var(--nb-surface);--nb-text:var(--nb-ink);--nb-btn-shadow:var(--nb-shadow);
--bs-btn-padding-x:1.2rem;--bs-btn-padding-y:.6rem;--bs-btn-font-family:var(--nb-font);--bs-btn-font-size:1rem;--bs-btn-font-weight:700;--bs-btn-line-height:1.2;
--bs-btn-border-width:var(--nb-bw);--bs-btn-border-radius:var(--nb-radius-sm);
--bs-btn-bg:var(--nb-fill);--bs-btn-color:var(--nb-text);--bs-btn-border-color:var(--nb-line);
--bs-btn-hover-bg:var(--nb-fill);--bs-btn-hover-color:var(--nb-text);--bs-btn-hover-border-color:var(--nb-line);
--bs-btn-active-bg:var(--nb-fill);--bs-btn-active-color:var(--nb-text);--bs-btn-active-border-color:var(--nb-line);
--bs-btn-disabled-bg:var(--nb-paper);--bs-btn-disabled-color:var(--nb-muted);--bs-btn-disabled-border-color:var(--nb-line);--bs-btn-disabled-opacity:1;
--bs-btn-focus-box-shadow:var(--nb-btn-shadow);
display:inline-flex;align-items:center;justify-content:center;gap:.5rem;min-height:46px;box-shadow:var(--nb-btn-shadow);transition:translate .12s,box-shadow .12s}
.btn-nb:hover{translate:-2px -2px;--nb-btn-shadow:var(--nb-shadow-hover)}
.btn-nb:active{translate:var(--nb-press);--nb-btn-shadow:none}
.btn-nb:focus-visible,.btn-check:focus-visible+.btn-nb{outline:3px dashed var(--nb-focus);outline-offset:5px;box-shadow:var(--nb-btn-shadow)}
.btn-nb-lemon{--nb-fill:var(--nb-lemon);--nb-text:var(--nb-on-fill)}
.btn-nb-pink{--nb-fill:var(--nb-pink);--nb-text:var(--nb-on-fill)}
.btn-nb-cyan{--nb-fill:var(--nb-cyan);--nb-text:var(--nb-on-fill)}
.btn-nb-lime{--nb-fill:var(--nb-lime);--nb-text:var(--nb-on-fill)}
.btn-nb-red{--nb-fill:var(--nb-red);--nb-text:var(--nb-on-fill)}
.btn-nb-blue{--nb-fill:var(--nb-blue);--nb-text:var(--nb-on-blue)}
.btn-nb-ink{--nb-fill:var(--nb-ink);--nb-text:var(--nb-paper)}
.btn-nb.btn-sm{--bs-btn-padding-x:.85rem;--bs-btn-padding-y:.4rem;--bs-btn-font-size:.875rem;--bs-btn-border-radius:var(--nb-radius-sm);min-height:38px}
.btn-nb.btn-lg{--bs-btn-padding-x:1.6rem;--bs-btn-padding-y:.8rem;--bs-btn-font-size:1.15rem;--bs-btn-border-radius:var(--nb-radius-sm);min-height:56px}
.btn-nb-icon{--bs-btn-padding-x:0;--bs-btn-padding-y:0;width:48px;height:48px;min-height:0}
.btn-nb-icon-sm{width:38px;height:38px;--nb-btn-shadow:var(--nb-shadow-sm)}
.btn-nb-icon-sm:hover{--nb-btn-shadow:var(--nb-shadow)}
.btn-nb-icon-sm:active{translate:var(--nb-press-sm)}
.btn-nb.rounded-circle{border-radius:50%}
.btn-nb:disabled{border-style:dashed;--nb-btn-shadow:none;translate:none}
.nb-cluster{display:flex;flex-wrap:wrap;align-items:center;gap:18px}
.nb-tiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:20px}
.nb-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,260px),1fr));gap:28px;align-items:start}
.nb-stack{display:grid;gap:1.25rem}
/* Alerts: Bootstrap .alert; the type is named by an icon, a bold title and hidden text, never by colour alone */
.alert.nb-alert{--bs-alert-bg:var(--nb-tone,var(--nb-cyan));--bs-alert-color:var(--nb-on-fill);--bs-alert-border-color:var(--nb-line);--bs-alert-border:var(--nb-border);
--bs-alert-border-radius:var(--nb-radius);--bs-alert-padding-x:1.1rem;--bs-alert-padding-y:1rem;--bs-alert-margin-bottom:0;--bs-alert-link-color:var(--nb-on-fill);
display:flex;align-items:flex-start;gap:.85rem;box-shadow:var(--nb-shadow);font-family:var(--nb-font)}
.nb-alert-success{--nb-tone:var(--nb-lime)}.nb-alert-warning{--nb-tone:var(--nb-lemon)}.nb-alert-error{--nb-tone:var(--nb-red)}
.nb-alert__icon{display:grid;place-items:center;flex:none;width:38px;height:38px;border-radius:50%;background:var(--nb-on-fill);color:var(--nb-tone,var(--nb-cyan))}
.nb-alert__body{flex:1;min-width:0}
.nb-alert__title{margin:0;font-weight:800}
.nb-alert__body p+p{margin:.15rem 0 0}
.nb-alert .alert-link{font-weight:800;text-decoration:underline;text-decoration-thickness:2px;text-underline-offset:3px}
.nb-alert .alert-link:focus-visible{outline:3px dashed var(--nb-on-fill);outline-offset:3px}
/* Toasts: Bootstrap .toast in a polite live region, each with its own close button */
.nb-toasts{display:grid;gap:1rem;max-width:400px}
.toast.nb-toast{--bs-toast-max-width:100%;--bs-toast-bg:var(--nb-surface);--bs-toast-color:var(--nb-ink);--bs-toast-border-width:var(--nb-bw);--bs-toast-border-color:var(--nb-line);
--bs-toast-border-radius:var(--nb-radius);--bs-toast-box-shadow:var(--nb-shadow-lg);--bs-toast-font-size:1rem;width:100%;font-family:var(--nb-font)}
.nb-toast .toast-body{display:flex;align-items:center;gap:.8rem;padding:.8rem .9rem}
.nb-toast__icon{display:grid;place-items:center;flex:none;width:38px;height:38px;border:var(--nb-border);border-radius:8px;background:var(--nb-lime);color:var(--nb-on-fill);rotate:-5deg}
.nb-toast__icon-cyan{background:var(--nb-cyan)}
.nb-toast__body{flex:1;min-width:0;line-height:1.35}
.nb-toast__title{display:block;font-weight:800}
.nb-toast__text{font-size:.9rem;color:var(--nb-muted)}
.nb-toast__action{padding:.2rem .1rem;border:0;background:none;color:var(--nb-ink);font:800 .9rem/1.2 var(--nb-font);text-decoration:underline;text-decoration-thickness:2px;text-underline-offset:3px;cursor:pointer}
.nb-toast__action:focus-visible{outline:3px dashed var(--nb-focus);outline-offset:3px}
/* Tooltips: shown on hover and keyboard focus, tied to the trigger with aria-describedby, hidden again with Escape */
.nb-tip{position:relative;display:inline-flex}
.nb-tip__bubble{position:absolute;bottom:calc(100% + 14px);left:50%;z-index:5;width:max-content;max-width:220px;padding:.45rem .7rem;border:var(--nb-border);border-radius:var(--nb-radius-sm);
background:var(--nb-lemon);color:var(--nb-on-fill);font:700 .85rem/1.3 var(--nb-font);box-shadow:var(--nb-shadow-sm);translate:-50% 0;visibility:hidden;opacity:0;transition:opacity .12s,visibility .12s}
.nb-tip__bubble::after{content:"";position:absolute;top:100%;left:50%;width:12px;height:12px;margin:-5px 0 0 -6px;border:solid var(--nb-line);border-width:0 var(--nb-bw) var(--nb-bw) 0;background:var(--nb-lemon);rotate:45deg}
.nb-tip:hover .nb-tip__bubble,.nb-tip:focus-within .nb-tip__bubble,.nb-tip--open .nb-tip__bubble{visibility:visible;opacity:1}
.nb-tip.is-dismissed .nb-tip__bubble{visibility:hidden;opacity:0}
.nb-tip--start .nb-tip__bubble{left:0;translate:none}
.nb-tip--start .nb-tip__bubble::after{left:24px}
.nb-tip--right .nb-tip__bubble{bottom:auto;top:50%;left:calc(100% + 14px);translate:0 -50%}
.nb-tip--right .nb-tip__bubble::after{top:50%;left:0;margin:-6px 0 0 -5px;border-width:0 0 var(--nb-bw) var(--nb-bw)}
@media (max-width:600px){.nb-tip--right .nb-tip__bubble{top:auto;bottom:calc(100% + 14px);left:50%;translate:-50% 0}.nb-tip--right .nb-tip__bubble::after{top:100%;left:50%;margin:-5px 0 0 -6px;border-width:0 var(--nb-bw) var(--nb-bw) 0}}
/* Dropdown: Bootstrap .dropdown-menu restyled through --bs-dropdown-*; Escape or a click outside closes it */
.nb-dd .dropdown-toggle::after{display:none}
.nb-dd .btn-nb.show{translate:var(--nb-press);--nb-btn-shadow:none}
.nb-dd .btn-nb .nb-ico:last-child{transition:rotate .12s}
.nb-dd .btn-nb.show .nb-ico:last-child{rotate:180deg}
.nb-dd .dropdown-menu{--bs-dropdown-min-width:230px;--bs-dropdown-padding-x:.4rem;--bs-dropdown-padding-y:.4rem;--bs-dropdown-spacer:12px;--bs-dropdown-bg:var(--nb-surface);
--bs-dropdown-border-color:var(--nb-line);--bs-dropdown-border-width:var(--nb-bw);--bs-dropdown-border-radius:var(--nb-radius);--bs-dropdown-box-shadow:var(--nb-shadow-lg);
--bs-dropdown-link-color:var(--nb-ink);--bs-dropdown-link-hover-color:var(--nb-on-fill);--bs-dropdown-link-hover-bg:var(--nb-lemon);--bs-dropdown-link-active-color:var(--nb-on-fill);--bs-dropdown-link-active-bg:var(--nb-lemon);
--bs-dropdown-item-padding-x:.65rem;--bs-dropdown-item-padding-y:.5rem;--bs-dropdown-item-border-radius:6px;--bs-dropdown-divider-bg:var(--nb-line);--bs-dropdown-divider-margin-y:.3rem;
box-shadow:var(--bs-dropdown-box-shadow)}
.nb-dd .dropdown-menu>li+li{margin-top:2px}
.nb-dd .dropdown-item{display:flex;align-items:center;gap:.65rem;border:2px solid transparent;font:700 .95rem/1.2 var(--nb-font)}
.nb-dd .dropdown-item kbd{margin-left:auto;padding:0 .35rem;border:2px solid currentColor;border-radius:4px;background:none;color:inherit;font:700 .72rem/1.4 var(--nb-font)}
.nb-dd .dropdown-item:hover{border-color:var(--nb-line)}
.nb-dd .dropdown-item:focus-visible{outline:3px dashed var(--nb-focus);outline-offset:1px}
.nb-dd .dropdown-item.nb-danger{color:var(--nb-danger)}
.nb-dd .dropdown-item.nb-danger:hover,.nb-dd .dropdown-item.nb-danger:focus{background:var(--nb-red);color:var(--nb-on-fill)}
.nb-dd .dropdown-divider{border-top-width:var(--nb-bw);opacity:1}
/* Modal: the native <dialog>. showModal() traps focus, makes the page inert and closes on Escape;
buttons inside <form method="dialog"> close it without script */
.nb-dialog{width:min(100% - 32px,460px);padding:0;border:var(--nb-border);border-radius:var(--nb-radius);background:var(--nb-surface);color:var(--nb-ink);
box-shadow:calc(var(--nb-shadow-x)*2.5) calc(var(--nb-shadow-y)*2.5) 0 0 var(--nb-shadow-color);font-family:var(--nb-font);overflow:visible}
.nb-dialog::backdrop{background:rgba(17,17,17,.6)}
.nb-dialog[open]{animation:nb-drop .18s ease-out}
@keyframes nb-drop{from{translate:0 -14px;opacity:0}}
.nb-dialog form{margin:0}
.nb-dialog__head{display:flex;align-items:center;gap:.8rem;padding:1rem 1.2rem;border-bottom:var(--nb-border);border-radius:calc(var(--nb-radius) - var(--nb-bw)) calc(var(--nb-radius) - var(--nb-bw)) 0 0;background:var(--nb-pink);color:var(--nb-on-fill)}
.nb-dialog__title{flex:1;margin:0;font-size:1.3rem}
.nb-dialog__head .nb-btn{--nb-fill:var(--nb-surface);--nb-text:var(--nb-ink)}
.nb-dialog__body{padding:1.2rem 1.2rem .4rem}
.nb-dialog__body p{margin:0 0 .8rem}
.nb-dialog__foot{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:14px;padding:.8rem 1.2rem 1.3rem}
@media (prefers-reduced-motion:reduce){[class*="nb-"],[class*="nb-"]::before,[class*="nb-"]::after{transition:none!important;animation:none!important}}
JavaScript
// Modal: any button with data-nb-open="dialog-id" opens that <dialog>; focus returns to the button on close
document.querySelectorAll('[data-nb-open]').forEach(function (btn) {
var dialog = document.getElementById(btn.getAttribute('data-nb-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-nb-tip]').forEach(function (t) { if (t.matches(':hover, :focus-within')) t.classList.add('is-dismissed'); });
});
document.querySelectorAll('[data-nb-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); });
/* ── Neobrutalism UI kit tokens (uiCookies) — identical in every set ─────────────
Thick ink borders, hard offset shadows and flat saturated fills on cream paper.
Change a token and the whole kit follows; the border and shadow recipes below are built from them. */
:root{
--nb-paper:#fff6e0; /* page background, cream */
--nb-surface:#ffffff; /* card, field and button faces */
--nb-ink:#111111; /* text, 17.5:1 on paper */
--nb-muted:#454036; /* secondary text, 9.6:1 on paper */
--nb-line:#111111; /* border colour */
--nb-bw:3px; /* border width */
--nb-shadow-x:4px; /* hard shadow offset; pressed controls move this far into it */
--nb-shadow-y:4px;
--nb-shadow-color:#111111;
--nb-radius:12px; /* cards and panels */
--nb-radius-sm:8px; /* buttons and fields */
--nb-focus:#1d4ed8; /* dashed focus outline, 6.2:1 on paper, never the shadow */
--nb-on-fill:#111111; /* text on every palette fill below, 6.8:1 or more */
--nb-on-fill-muted:#2b2822;/* secondary text on fills, 5.2:1 or more */
--nb-lemon:#ffe14d;--nb-pink:#ff90d0;--nb-cyan:#62e2e8;--nb-lime:#b6f36a;
--nb-orange:#ffa94d;--nb-violet:#b9a3ff;--nb-red:#ff6b6b;
--nb-blue:#2448e0;--nb-on-blue:#ffffff; /* the only fill that takes white text, 6.8:1 */
--nb-danger:#b42318; /* destructive text, 6.1:1 on paper */
--nb-font:"Space Grotesk","Archivo","Helvetica Neue",Arial,system-ui,sans-serif;
--nb-font-display:"Archivo Black","Arial Black","Helvetica Neue",Arial,sans-serif;
}
/* Dark brutalism: data-nb="dark" on any wrapper, or a checked .nb-theme-input inside a [data-nb] element.
:is() is a forgiving list, so browsers without :has() still apply data-nb="dark". Fills keep dark text. */
[data-nb]:is([data-nb="dark"],:has(.nb-theme-input:checked)){
--nb-paper:#17150f;--nb-surface:#25221b;--nb-ink:#fbf3dc;--nb-muted:#d6ccb2;--nb-line:#fbf3dc;
--nb-shadow-color:#fbf3dc;--nb-focus:#8db4ff;--nb-danger:#ff9b8f;color-scheme:dark}
/* More contrast requested: secondary text turns full ink */
@media (prefers-contrast:more){:root,[data-nb]:is([data-nb],:has(.nb-theme-input:checked)){--nb-muted:var(--nb-ink);--nb-on-fill-muted:var(--nb-on-fill)}}
:root,[data-nb]{
--nb-border:var(--nb-bw) solid var(--nb-line);
--nb-shadow:var(--nb-shadow-x) var(--nb-shadow-y) 0 0 var(--nb-shadow-color);
--nb-shadow-sm:calc(var(--nb-shadow-x)*.5) calc(var(--nb-shadow-y)*.5) 0 0 var(--nb-shadow-color);
--nb-shadow-lg:calc(var(--nb-shadow-x)*1.75) calc(var(--nb-shadow-y)*1.75) 0 0 var(--nb-shadow-color);
--nb-shadow-hover:calc(var(--nb-shadow-x) + 2px) calc(var(--nb-shadow-y) + 2px) 0 0 var(--nb-shadow-color);
--nb-press:var(--nb-shadow-x) var(--nb-shadow-y); /* translate for a pressed control */
--nb-press-sm:calc(var(--nb-shadow-x)*.5) calc(var(--nb-shadow-y)*.5); /* …for one with the small shadow */
}
[data-nb]{background:var(--nb-paper);color:var(--nb-ink);font-family:var(--nb-font)}
/* The tokens above, exposed as Tailwind utilities: bg-nb-paper, bg-nb-lemon, text-nb-ink, border-nb-line,
shadow-nb, shadow-nb-sm, shadow-nb-lg, rounded-nb, rounded-nb-sm, font-nb, font-nb-display, outline-nb-focus… */
@theme inline {
--color-nb-paper: var(--nb-paper); --color-nb-surface: var(--nb-surface); --color-nb-ink: var(--nb-ink);
--color-nb-muted: var(--nb-muted); --color-nb-line: var(--nb-line); --color-nb-focus: var(--nb-focus);
--color-nb-on-fill: var(--nb-on-fill); --color-nb-on-fill-muted: var(--nb-on-fill-muted);
--color-nb-lemon: var(--nb-lemon); --color-nb-pink: var(--nb-pink); --color-nb-cyan: var(--nb-cyan); --color-nb-lime: var(--nb-lime);
--color-nb-orange: var(--nb-orange); --color-nb-violet: var(--nb-violet); --color-nb-red: var(--nb-red);
--color-nb-blue: var(--nb-blue); --color-nb-on-blue: var(--nb-on-blue); --color-nb-danger: var(--nb-danger);
--shadow-nb: var(--nb-shadow); --shadow-nb-sm: var(--nb-shadow-sm); --shadow-nb-lg: var(--nb-shadow-lg); --shadow-nb-hover: var(--nb-shadow-hover);
--radius-nb: var(--nb-radius); --radius-nb-sm: var(--nb-radius-sm);
--font-nb: var(--nb-font); --font-nb-display: var(--nb-font-display);
}
/* Starburst sticker outline (Tailwind has no clip-path utility) */
@utility clip-nb-burst {
clip-path: polygon(50% 0%,59.1% 16.1%,75% 6.7%,74.8% 25.2%,93.3% 25%,83.9% 40.9%,100% 50%,83.9% 59.1%,93.3% 75%,74.8% 74.8%,75% 93.3%,59.1% 83.9%,50% 100%,40.9% 83.9%,25% 93.3%,25.2% 74.8%,6.7% 75%,16.1% 59.1%,0% 50%,16.1% 40.9%,6.7% 25%,25.2% 25.2%,25% 6.7%,40.9% 16.1%);
}
JavaScript
// Modal: any button with data-nb-open="dialog-id" opens that <dialog>; focus returns to the button on close
document.querySelectorAll('[data-nb-open]').forEach(function (btn) {
var dialog = document.getElementById(btn.getAttribute('data-nb-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-nb-tip]').forEach(function (t) { if (t.matches(':hover, :focus-within')) t.classList.add('is-dismissed'); });
});
document.querySelectorAll('[data-nb-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-nb-dismiss]').forEach(function (b) {
b.addEventListener('click', function () { b.closest('[data-nb-toast]').hidden = true; });
});
// Dropdown: toggle on click; Escape (focus back to the button) or a click outside closes it
document.querySelectorAll('[data-nb-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 with thick borders and hard shadows. Four alerts for information, success, a warning and an error fill cyan, lime, lemon and red, and carry their type in a round icon, a bold title and hidden text such as Warning:, so colour is never the only signal; links inside them are underlined. Two toasts sit in a polite live region, one with an Undo action, and each closes with its own button. Tooltips are lemon bubbles with an arrow that appear above or beside their trigger on hover and keyboard focus, are tied to it with aria-describedby and hide again with Escape; the first is pinned open in the preview. A dropdown of episode actions presses its button into its shadow while open and lists items with keyboard hints, a divider and a red Delete episode. The modal is the native dialog element with a pink header and an extra-deep shadow: 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, returns focus to the button, and runs the toasts and dropdown. Bootstrap uses .alert, .toast and .dropdown with its own 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 dashed focus outline. Animations and transitions stop under prefers-reduced-motion.
Customise it
Change the palette tokens for the alert tones (--nb-cyan, --nb-lime, --nb-lemon, --nb-red) and --nb-danger for the destructive menu item. Bootstrap maps the tokens onto --bs-alert-*, --bs-toast-* and --bs-dropdown-*; Tailwind uses bg-nb-red, shadow-nb-lg and a backdrop: utility on the dialog.