GLM 5.3
by Z.aiReasoningClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 2 min 39 s
- Cost
- $0.040
- Output tokens
- 20,463
- Spent thinking
- 12,754 tokens
- Speed
- 128.9 tok/s
- Console errors
- 0
- First token after
- 661 ms
- First visible text after
- 1 min 55 s
- Prompt tokens
- 493
- Code size
- 25.5 KB
- Finish reason
- stop
- Served by
- Decart
- Model id
z-ai/glm-5.3- Reasoning sent
{"effort":"high"}- Max output
- 943,718 tokens
- Generated
- Nov 12, 2026
What it was asked
The prompt
Build an interactive Kanban board in vanilla HTML, CSS and JavaScript (no framework, no library). Requirements: - Three columns: "To do", "In progress", "Done", each showing its card count. - Pre-populate the board with 6 realistic cards spread across the columns so the first screenshot looks alive. - Each card has a title and a priority (low / medium / high) shown as a colored badge. - Add a card through a small modal form (title required), and delete a card with a button on it. - Drag and drop cards between columns with the mouse, with a clear visual drop indicator. - A search field that filters cards live by title. - A light / dark theme toggle. - State lives in memory only (no storage APIs).
System prompt, sent with every prompt
You are generating a self-contained web demo. It will be rendered automatically in a sandboxed iframe and compared side by side with the output of other AI models given the exact same task. Output rules: - Reply with exactly ONE fenced code block containing the complete file: ```html (or ```svg when the task asks for an SVG). No text before or after the code block. - Everything must be inline: CSS in <style>, JavaScript in <script>. No external files, images, fonts or network requests, except the CDN URLs explicitly allowed by the task. - Do not use localStorage, sessionStorage, cookies, IndexedDB, alert(), confirm(), prompt() or window.open(): they are blocked in the sandbox and will throw. - The page must look right in a 1280×800 viewport and adapt gracefully down to 375px wide. No horizontal scrollbar. - Something meaningful must be visible on first paint: a screenshot is taken automatically about 3 seconds after load, without any interaction. - Everything that makes the demo interesting must be visible within the first seconds: nothing hidden behind a high score, a later level or a long play session. - Games must start in a self-playing demo mode (a simple AI plays) behind a "Click or press a key to start" message, so the game can be judged in action without playing. - The iframe only receives keyboard events after the user clicks it: if the demo uses the keyboard, display a clear "Click or press a key to start" message. - The code must run with zero errors in the console of the latest Chrome.
Code run-1.html · 600 lines
<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Delivery Board — Kanban</title>
<style>
:root{
color-scheme: light;
--serif: "Iowan Old Style","Palatino Linotype","Book Antiqua",Palatino,Georgia,serif;
--mono: ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace;
--bg:#F2ECDF; --col-bg:#FAF6EC; --card-bg:#FFFDF8;
--ink:#241F17; --muted:#877A66;
--line:#E2D8C4; --line-strong:#CEC2A9;
--accent:#B4441E; --accent-ink:#FFF6EA;
--pri-low:#4F7D58; --pri-med:#9A7414; --pri-high:#B23F27;
--shadow: 0 1px 2px rgba(60,45,20,.08), 0 4px 14px rgba(60,45,20,.05);
}
html[data-theme="dark"]{
color-scheme: dark;
--bg:#141109; --col-bg:#1E1A12; --card-bg:#282217;
--ink:#EDE5D3; --muted:#948972;
--line:#37301F; --line-strong:#4A4128;
--accent:#E06A38; --accent-ink:#1C0F06;
--pri-low:#7FB689; --pri-med:#D5A44A; --pri-high:#E76A4B;
--shadow: 0 1px 2px rgba(0,0,0,.5), 0 6px 18px rgba(0,0,0,.35);
}
[hidden]{ display:none !important; }
*{ box-sizing:border-box; margin:0; padding:0; }
html,body{ height:100%; }
body{
font-family:var(--serif); background:var(--bg); color:var(--ink);
overflow:hidden;
transition: background-color .3s ease, color .3s ease;
}
#app{ height:100dvh; display:flex; flex-direction:column; }
/* ---------- header ---------- */
header.top{
display:flex; align-items:flex-end; justify-content:space-between; gap:18px;
padding:20px 22px 14px; border-bottom:1px solid var(--line); flex-wrap:wrap;
}
.overline{
font:600 10px/1 var(--mono); letter-spacing:.2em; text-transform:uppercase;
color:var(--muted); display:flex; align-items:center; gap:8px; margin-bottom:7px;
}
.overline::before{ content:""; width:8px; height:8px; background:var(--accent); border-radius:2px; }
h1{ font:600 29px/1 var(--serif); letter-spacing:-.01em; }
.controls{ display:flex; align-items:center; gap:10px; flex-wrap:wrap; }
.search{
display:flex; align-items:center; gap:8px; background:var(--card-bg);
border:1px solid var(--line-strong); border-radius:9px; padding:0 10px; height:36px;
min-width:210px; transition:border-color .2s;
}
.search:focus-within{ border-color:var(--accent); }
.search svg{ color:var(--muted); flex:none; }
.search input{
border:none; outline:none; background:transparent; color:var(--ink);
font:400 14px/1 var(--serif); width:100%;
}
.search input::placeholder{ color:var(--muted); opacity:.75; }
button{ font:inherit; cursor:pointer; }
.icon-btn{
display:inline-flex; align-items:center; justify-content:center;
width:36px; height:36px; border-radius:9px; background:var(--card-bg);
border:1px solid var(--line-strong); color:var(--muted);
transition: color .2s, border-color .2s, background-color .3s;
}
.icon-btn:hover{ color:var(--ink); border-color:var(--ink); }
.clear{ width:22px; height:22px; border:none; background:transparent; border-radius:5px; }
.clear:hover{ color:var(--accent); background:color-mix(in srgb, var(--accent) 12%, transparent); }
html[data-theme="light"] .ic-sun{ display:none; }
html[data-theme="dark"] .ic-moon{ display:none; }
.btn-primary{
display:inline-flex; align-items:center; gap:8px; height:36px; padding:0 16px;
background:var(--accent); color:var(--accent-ink); border:1px solid var(--accent);
border-radius:9px; font:600 11px/1 var(--mono); letter-spacing:.12em; text-transform:uppercase;
transition: filter .2s, transform .12s;
}
.btn-primary:hover{ filter:brightness(1.08); }
.btn-primary:active{ transform:translateY(1px); }
.btn-ghost{
height:34px; padding:0 14px; background:transparent; color:var(--muted);
border:1px solid var(--line-strong); border-radius:9px;
font:600 11px/1 var(--mono); letter-spacing:.12em; text-transform:uppercase;
}
.btn-ghost:hover{ color:var(--ink); border-color:var(--ink); }
/* ---------- board ---------- */
#board{
flex:1; min-height:0; display:grid;
grid-template-columns:repeat(3, minmax(0,1fr)); gap:16px; padding:16px 22px 14px;
}
.column{
display:flex; flex-direction:column; min-height:0; min-width:0;
background:var(--col-bg); border:1px solid var(--line);
border-top:3px solid var(--col-accent); border-radius:12px;
transition: background-color .3s, border-color .2s;
}
.column[data-col="todo"]{ --col-accent:#8B8069; }
.column[data-col="doing"]{ --col-accent:#C08A1E; }
.column[data-col="done"]{ --col-accent:#4F7D58; }
html[data-theme="dark"] .column[data-col="todo"]{ --col-accent:#9A8F75; }
html[data-theme="dark"] .column[data-col="doing"]{ --col-accent:#D5A44A; }
html[data-theme="dark"] .column[data-col="done"]{ --col-accent:#7FB689; }
.column.over{
border-color:color-mix(in srgb, var(--accent) 60%, var(--line));
background:color-mix(in srgb, var(--accent) 5%, var(--col-bg));
}
.col-head{
display:flex; align-items:center; gap:9px; padding:12px 14px 10px;
border-bottom:1px solid var(--line); flex:none;
}
.dot{ width:8px; height:8px; border-radius:50%; background:var(--col-accent); flex:none; }
.col-name{
font:600 11px/1 var(--mono); letter-spacing:.14em; text-transform:uppercase; color:var(--ink);
}
.count{
margin-left:auto; font:600 10.5px/1 var(--mono); color:var(--muted);
border:1px solid var(--line-strong); border-radius:999px; padding:4px 9px; min-width:30px; text-align:center;
}
.cards{
flex:1; min-height:0; overflow-y:auto; padding:11px;
display:flex; flex-direction:column; gap:9px;
scrollbar-width:thin; scrollbar-color:var(--line-strong) transparent;
}
.cards::-webkit-scrollbar{ width:8px; }
.cards::-webkit-scrollbar-thumb{ background:var(--line-strong); border-radius:4px; }
/* ---------- cards ---------- */
.card{
flex:none; background:var(--card-bg); border:1px solid var(--line);
border-radius:9px; padding:11px 12px 12px; cursor:grab;
box-shadow:var(--shadow);
transition: transform .15s ease, border-color .2s, background-color .3s, opacity .15s;
}
.card:hover{ transform:translateY(-1px); border-color:var(--line-strong); }
.card:active{ cursor:grabbing; }
.card.dragging{ opacity:.35; }
.card.filtered-out{ display:none; }
.card.enter{ animation:cardIn .38s cubic-bezier(.2,.8,.3,1); }
@keyframes cardIn{ from{ opacity:0; transform:translateY(9px) scale(.96); } }
.card-top{ display:flex; align-items:flex-start; gap:8px; }
.card-title{ font:600 15.5px/1.35 var(--serif); flex:1; overflow-wrap:anywhere; }
.del{
flex:none; width:22px; height:22px; display:inline-flex; align-items:center; justify-content:center;
border:none; background:transparent; color:var(--muted); border-radius:5px; opacity:.55;
transition: opacity .15s, color .15s, background-color .15s;
}
.card:hover .del{ opacity:1; }
.del:hover{ color:var(--pri-high); background:color-mix(in srgb, var(--pri-high) 13%, transparent); }
.card-meta{ display:flex; align-items:center; gap:8px; margin-top:9px; }
.badge{
font:700 9.5px/1 var(--mono); letter-spacing:.13em; text-transform:uppercase;
padding:4px 8px; border-radius:999px;
}
.badge.low{ --p:var(--pri-low); } .badge.medium{ --p:var(--pri-med); } .badge.high{ --p:var(--pri-high); }
.badge{
color:var(--p);
background:color-mix(in srgb, var(--p) 12%, transparent);
border:1px solid color-mix(in srgb, var(--p) 38%, transparent);
}
.chip{ font:500 10.5px/1 var(--mono); color:var(--muted); letter-spacing:.06em; }
.empty{
font:400 12.5px/1.5 var(--serif); font-style:italic; color:var(--muted);
text-align:center; padding:16px 10px; border:1px dashed var(--line-strong); border-radius:9px;
}
/* ---------- drop indicator ---------- */
.drop-indicator{
flex:none; height:38px; border:1.5px dashed var(--accent); border-radius:9px;
background:color-mix(in srgb, var(--accent) 8%, transparent);
display:flex; align-items:center; justify-content:center;
animation:phIn .18s ease;
}
.drop-indicator span{
font:700 9.5px/1 var(--mono); letter-spacing:.16em; text-transform:uppercase; color:var(--accent);
}
@keyframes phIn{ from{ opacity:0; transform:scale(.98); } }
/* ---------- footer ---------- */
footer{
flex:none; display:flex; justify-content:space-between; gap:12px; flex-wrap:wrap;
padding:9px 22px; border-top:1px solid var(--line);
font:500 10.5px/1 var(--mono); letter-spacing:.05em; color:var(--muted);
}
kbd{
font:inherit; border:1px solid var(--line-strong); border-bottom-width:2px;
border-radius:4px; padding:2px 5px; color:var(--ink);
}
/* ---------- modal ---------- */
.overlay{
position:fixed; inset:0; z-index:20; display:flex; align-items:center; justify-content:center;
background:rgba(20,14,6,.45); padding:20px;
opacity:0; transition:opacity .18s ease;
}
.overlay.open{ opacity:1; }
.modal{
width:100%; max-width:420px; background:var(--card-bg); border:1px solid var(--line-strong);
border-radius:14px; padding:22px 22px 20px; box-shadow:0 24px 60px rgba(20,12,4,.35);
transform:translateY(10px) scale(.98); transition:transform .18s cubic-bezier(.2,.8,.3,1);
}
.overlay.open .modal{ transform:none; }
.modal h2{ font:600 22px/1.2 var(--serif); margin-bottom:18px; }
.field-label{
display:block; font:600 10px/1 var(--mono); letter-spacing:.16em; text-transform:uppercase;
color:var(--muted); margin:0 0 7px;
}
.field-label .req{ color:var(--accent); letter-spacing:.05em; text-transform:none; margin-left:5px; }
.modal input[type=text], .modal select{
width:100%; height:40px; padding:0 12px; background:var(--bg);
border:1px solid var(--line-strong); border-radius:9px; color:var(--ink);
font:400 14.5px/1 var(--serif); outline:none; transition:border-color .2s;
}
.modal input[type=text]:focus, .modal select:focus{ border-color:var(--accent); }
.modal input.invalid{ border-color:var(--pri-high); }
.modal input.shake{ animation:shake .32s; }
@keyframes shake{ 25%{transform:translateX(-5px)} 50%{transform:translateX(5px)} 75%{transform:translateX(-2px)} }
.err{ font:500 11px/1.4 var(--mono); color:var(--pri-high); margin-top:7px; }
.stack{ margin-top:16px; }
.seg{ display:flex; gap:6px; }
.seg button{
flex:1; height:36px; display:inline-flex; align-items:center; justify-content:center; gap:7px;
background:var(--bg); border:1px solid var(--line-strong); border-radius:9px;
color:var(--muted); font:600 11px/1 var(--mono); letter-spacing:.08em; text-transform:uppercase;
transition:border-color .15s, color .15s;
}
.seg button .swatch{ width:8px; height:8px; border-radius:50%; }
.seg button[data-p="low"] .swatch{ background:var(--pri-low); }
.seg button[data-p="medium"] .swatch{ background:var(--pri-med); }
.seg button[data-p="high"] .swatch{ background:var(--pri-high); }
.seg button.sel{ border-color:var(--ink); color:var(--ink); box-shadow:inset 0 0 0 1px var(--ink); }
.modal-actions{ display:flex; justify-content:flex-end; gap:9px; margin-top:22px; }
/* ---------- toast ---------- */
.toast{
position:fixed; left:50%; bottom:22px; z-index:30; transform:translate(-50%, 12px);
display:flex; align-items:center; gap:14px; padding:10px 12px 10px 16px;
background:var(--card-bg); border:1px solid var(--line-strong); border-radius:10px;
box-shadow:0 12px 34px rgba(15,10,3,.28); opacity:0; pointer-events:none;
transition:opacity .22s ease, transform .22s ease;
}
.toast.show{ opacity:1; transform:translate(-50%,0); pointer-events:auto; }
.toast p{ font:500 12.5px/1 var(--serif); color:var(--ink); }
.toast button{
background:none; border:none; font:700 10.5px/1 var(--mono); letter-spacing:.12em;
text-transform:uppercase; color:var(--accent); padding:6px 4px; border-bottom:1.5px solid var(--accent);
}
/* ---------- responsive ---------- */
@media (max-width:760px){
body{ overflow:auto; }
#app{ height:auto; min-height:100dvh; }
header.top{ align-items:flex-start; }
h1{ font-size:25px; }
.search{ min-width:0; flex:1; }
#board{ grid-template-columns:1fr; padding:14px; }
.cards{ overflow:visible; }
.column{ min-height:220px; }
footer{ justify-content:center; }
}
</style>
</head>
<body>
<div id="app">
<header class="top">
<div class="brand">
<p class="overline">Acme product · sprint 12</p>
<h1>Delivery Board</h1>
</div>
<div class="controls">
<div class="search">
<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><circle cx="7" cy="7" r="4.5"/><path d="M10.5 10.5L14 14"/></svg>
<input id="search" type="text" placeholder="Search cards by title…" autocomplete="off" spellcheck="false" aria-label="Search cards by title">
<button id="clearSearch" class="icon-btn clear" aria-label="Clear search" hidden>
<svg viewBox="0 0 12 12" width="11" height="11" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"><path d="M2.5 2.5l7 7M9.5 2.5l-7 7"/></svg>
</button>
</div>
<button id="themeToggle" class="icon-btn" aria-label="Toggle light or dark theme">
<svg class="ic-moon" viewBox="0 0 20 20" width="16" height="16" fill="currentColor"><path d="M14.8 12.9A6.5 6.5 0 0 1 7.1 3.3a.6.6 0 0 0-.8-.7 7.3 7.3 0 1 0 9.4 9.4.6.6 0 0 0-.9-.1z"/></svg>
<svg class="ic-sun" viewBox="0 0 20 20" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><circle cx="10" cy="10" r="3.4"/><path d="M10 1.8v2M10 16.2v2M1.8 10h2M16.2 10h2M4.3 4.3l1.4 1.4M14.3 14.3l1.4 1.4M15.7 4.3l-1.4 1.4M5.7 14.3l-1.4 1.4"/></svg>
</button>
<button id="newCardBtn" class="btn-primary">
<svg viewBox="0 0 14 14" width="11" height="11" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M7 1.5v11M1.5 7h11"/></svg>
New card
</button>
</div>
</header>
<main id="board">
<section class="column" data-col="todo" aria-label="To do column">
<header class="col-head"><span class="dot"></span><h2 class="col-name">To do</h2><span class="count">0</span></header>
<div class="cards"></div>
</section>
<section class="column" data-col="doing" aria-label="In progress column">
<header class="col-head"><span class="dot"></span><h2 class="col-name">In progress</h2><span class="count">0</span></header>
<div class="cards"></div>
</section>
<section class="column" data-col="done" aria-label="Done column">
<header class="col-head"><span class="dot"></span><h2 class="col-name">Done</h2><span class="count">0</span></header>
<div class="cards"></div>
</section>
</main>
<footer>
<p><kbd>N</kbd> new card · <kbd>/</kbd> search · drag cards between columns</p>
<p>State lives in memory — refresh resets the board</p>
</footer>
</div>
<div class="overlay" id="overlay" hidden>
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="mTitle">
<h2 id="mTitle">New card</h2>
<form id="cardForm" novalidate>
<label class="field-label" for="fTitle">Card title <span class="req">required</span></label>
<input id="fTitle" type="text" maxlength="90" placeholder="e.g. Ship search analytics export" autocomplete="off">
<p class="err" id="fErr" hidden>Give the card a title first.</p>
<div class="stack">
<span class="field-label">Priority</span>
<div class="seg" id="segPriority">
<button type="button" data-p="low"><span class="swatch"></span>Low</button>
<button type="button" data-p="medium" class="sel"><span class="swatch"></span>Medium</button>
<button type="button" data-p="high"><span class="swatch"></span>High</button>
</div>
</div>
<div class="stack">
<label class="field-label" for="fCol">Column</label>
<select id="fCol">
<option value="todo">To do</option>
<option value="doing">In progress</option>
<option value="done">Done</option>
</select>
</div>
<div class="modal-actions">
<button type="button" id="mCancel" class="btn-ghost">Cancel</button>
<button type="submit" class="btn-primary">Add card</button>
</div>
</form>
</div>
</div>
<div class="toast" id="toast" role="status">
<p>Card deleted.</p>
<button id="undoBtn">Undo</button>
</div>
<script>
(function(){
"use strict";
const $ = (s, r) => (r || document).querySelector(s);
const $$ = (s, r) => Array.from((r || document).querySelectorAll(s));
/* ---------------- state (memory only) ---------------- */
let refCounter = 107, uid = 7;
const state = { cards: [
{ id:"c1", ref:101, title:"Audit onboarding funnel analytics", priority:"high", column:"todo" },
{ id:"c2", ref:102, title:"Draft Q3 pricing page copy", priority:"low", column:"todo" },
{ id:"c3", ref:103, title:"Spike: evaluate edge rendering options", priority:"medium", column:"todo" },
{ id:"c4", ref:104, title:"Redesign settings navigation", priority:"medium", column:"doing" },
{ id:"c5", ref:105, title:"Fix flaky checkout integration tests", priority:"high", column:"doing" },
{ id:"c6", ref:106, title:"Ship dark mode color tokens", priority:"medium", column:"done" }
]};
let dragId = null, justAdded = null, lastDeleted = null, toastTimer = null, chosenPriority = "medium";
const X_SVG = '<svg viewBox="0 0 12 12" width="11" height="11" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"><path d="M2.5 2.5l7 7M9.5 2.5l-7 7"/></svg>';
const PRI_LABEL = { low:"Low", medium:"Medium", high:"High" };
/* ---------------- card rendering ---------------- */
function cardEl(c){
const el = document.createElement("article");
el.className = "card" + (c.id === justAdded ? " enter" : "");
el.draggable = true;
el.dataset.id = c.id;
el.innerHTML =
'<div class="card-top"><h3 class="card-title"></h3>' +
'<button class="del" aria-label="Delete card">' + X_SVG + '</button></div>' +
'<div class="card-meta"><span class="badge ' + c.priority + '">' + PRI_LABEL[c.priority] + '</span>' +
'<span class="chip">K-' + c.ref + '</span></div>';
$(".card-title", el).textContent = c.title;
$(".del", el).addEventListener("click", () => deleteCard(c.id));
enableDrag(el, c.id);
return el;
}
function render(){
$$(".column").forEach(col => {
const list = $(".cards", col);
list.innerHTML = "";
state.cards.filter(c => c.column === col.dataset.col)
.forEach(c => list.appendChild(cardEl(c)));
});
applyFilter();
}
/* ---------------- search / counts / empty states ---------------- */
const searchInput = $("#search");
function applyFilter(){
const q = searchInput.value.trim().toLowerCase();
$$(".column").forEach(col => {
const list = $(".cards", col);
let total = 0, visible = 0;
$$(".card", list).forEach(el => {
total++;
const match = !q || $(".card-title", el).textContent.toLowerCase().includes(q);
el.classList.toggle("filtered-out", !match);
if (match) visible++;
});
$(".count", col).textContent = q ? visible + "/" + total : String(total);
let hint = $(".empty", list);
if (total === 0 || (q && visible === 0)) {
if (!hint) { hint = document.createElement("p"); hint.className = "empty"; list.appendChild(hint); }
hint.textContent = total === 0 ? "Nothing here yet — drop a card or add one."
: "No cards match your search.";
} else if (hint) hint.remove();
});
$("#clearSearch").hidden = !searchInput.value;
}
searchInput.addEventListener("input", applyFilter);
$("#clearSearch").addEventListener("click", () => { searchInput.value = ""; applyFilter(); searchInput.focus(); });
/* ---------------- drag & drop ---------------- */
const ph = document.createElement("div");
ph.className = "drop-indicator";
ph.innerHTML = "<span>drop here</span>";
function enableDrag(el, id){
el.addEventListener("dragstart", e => {
dragId = id;
e.dataTransfer.effectAllowed = "move";
e.dataTransfer.setData("text/plain", id);
setTimeout(() => el.classList.add("dragging"), 0);
});
el.addEventListener("dragend", () => {
el.classList.remove("dragging");
dragId = null;
ph.remove();
$$(".column").forEach(c => c.classList.remove("over"));
});
}
$$(".column").forEach(col => {
const list = $(".cards", col);
col.addEventListener("dragover", e => {
if (!dragId) return;
e.preventDefault();
e.dataTransfer.dropEffect = "move";
col.classList.add("over");
const visible = $$(".card", list).filter(el =>
!el.classList.contains("dragging") && !el.classList.contains("filtered-out"));
let ref = null;
for (const el of visible) {
const r = el.getBoundingClientRect();
if (e.clientY < r.top + r.height / 2) { ref = el; break; }
}
if (ref) list.insertBefore(ph, ref); else list.appendChild(ph);
});
col.addEventListener("dragleave", e => {
if (!col.contains(e.relatedTarget)) col.classList.remove("over");
});
col.addEventListener("drop", e => {
if (!dragId) return;
e.preventDefault();
const ref = ph.parentElement === list ? ph.nextElementSibling : null;
const moved = $('.card[data-id="' + CSS.escape(dragId) + '"]');
if (moved) { if (ref) list.insertBefore(moved, ref); else list.appendChild(moved); }
/* rebuild card order from the DOM */
const order = [];
$$(".column").forEach(c => {
const colId = c.dataset.col;
$$(".card", $(".cards", c)).forEach(el => {
const card = state.cards.find(x => x.id === el.dataset.id);
if (card) { card.column = colId; order.push(card); }
});
});
state.cards = order;
dragId = null;
ph.remove();
col.classList.remove("over");
render();
});
});
/* ---------------- delete + undo toast ---------------- */
function deleteCard(id){
const idx = state.cards.findIndex(c => c.id === id);
if (idx < 0) return;
lastDeleted = { card: state.cards[idx], idx: idx };
state.cards.splice(idx, 1);
render();
showToast();
}
function showToast(){
const t = $("#toast");
t.classList.add("show");
clearTimeout(toastTimer);
toastTimer = setTimeout(hideToast, 4500);
}
function hideToast(){
clearTimeout(toastTimer);
$("#toast").classList.remove("show");
}
$("#undoBtn").addEventListener("click", () => {
if (!lastDeleted) return;
state.cards.splice(Math.min(lastDeleted.idx, state.cards.length), 0, lastDeleted.card);
lastDeleted = null;
hideToast();
render();
});
/* ---------------- modal ---------------- */
const overlay = $("#overlay"), form = $("#cardForm"),
fTitle = $("#fTitle"), fCol = $("#fCol"), fErr = $("#fErr"), seg = $("#segPriority");
function syncSeg(){
$$("button", seg).forEach(b => b.classList.toggle("sel", b.dataset.p === chosenPriority));
}
seg.addEventListener("click", e => {
const b = e.target.closest("button");
if (!b) return;
chosenPriority = b.dataset.p;
syncSeg();
});
function openModal(){
form.reset();
chosenPriority = "medium"; syncSeg();
fErr.hidden = true;
fTitle.classList.remove("invalid");
fCol.value = "todo";
overlay.hidden = false;
requestAnimationFrame(() => overlay.classList.add("open"));
setTimeout(() => fTitle.focus(), 40);
}
function closeModal(){
overlay.classList.remove("open");
setTimeout(() => { overlay.hidden = true; }, 190);
$("#newCardBtn").focus();
}
$("#newCardBtn").addEventListener("click", openModal);
$("#mCancel").addEventListener("click", closeModal);
overlay.addEventListener("click", e => { if (e.target === overlay) closeModal(); });
fTitle.addEventListener("input", () => {
fErr.hidden = true;
fTitle.classList.remove("invalid");
});
form.addEventListener("submit", e => {
e.preventDefault();
const title = fTitle.value.trim();
if (!title) {
fErr.hidden = false;
fTitle.classList.add("invalid");
fTitle.classList.remove("shake"); void fTitle.offsetWidth;
fTitle.classList.add("shake");
fTitle.focus();
return;
}
const col = fCol.value;
const card = { id: "c" + (uid++), ref: refCounter++, title: title,
priority: chosenPriority, column: col };
const firstIdx = state.cards.findIndex(c => c.column === col);
if (firstIdx === -1) state.cards.push(card); else state.cards.splice(firstIdx, 0, card);
justAdded = card.id;
closeModal();
render();
justAdded = null;
});
/* ---------------- theme toggle ---------------- */
$("#themeToggle").addEventListener("click", () => {
const root = document.documentElement;
root.dataset.theme = root.dataset.theme === "dark" ? "light" : "dark";
});
/* ---------------- keyboard shortcuts ---------------- */
document.addEventListener("keydown", e => {
if (e.key === "Escape" && !overlay.hidden) { closeModal(); return; }
const t = e.target;
if (t.tagName === "INPUT" || t.tagName === "SELECT" || t.tagName === "TEXTAREA" || t.isContentEditable) return;
if (!overlay.hidden) return;
if (e.key === "n" || e.key === "N") { e.preventDefault(); openModal(); }
if (e.key === "/") { e.preventDefault(); searchInput.focus(); }
});
/* ---------------- init ---------------- */
render();
})();
</script>
</body>
</html>Reasoning trace (12,754 tokens)
Let me build a polished Kanban board. I want it to look genuinely designed, not like a generic admin panel. I'll avoid gradients, default blue/purple, emoji icons, and uniform card grids. The Kanban column structure is inherent to the task, but I can give it character through typography, color choices, and details.
Design direction:
- A warm paper-like light theme with an ink-dark accent, and a rich dark theme with warm undertones. Accent color: something like a burnt orange / amber (#E85D2F-ish) or a deep teal. Let me go with a distinctive palette: light theme — cream paper (#F7F3EC), ink text (#1F1B16), accent terracotta (#C4501B or #D9552F). Dark theme — deep warm charcoal (#171512 / #1B1917), cream text, same accent slightly brightened.
- Fonts: use Google Fonts CDN? The task says "no framework, no library" — fonts are fine as CDN but let me be careful: "Everything must be inline... No external files, images, fonts or network requests, except the CDN URLs explicitly allowed by the task." The task doesn't explicitly allow CDN URLs. So I should use system fonts but avoid making Inter/Roboto/Arial the identity. I can use a font stack like Georgia for headings and a monospace for meta, or use "Iowan Old Style, Palatino, ' Palatino Linotype', serif" for headings and system UI for body? Actually I can craft identity through a serif display stack: `"Iowan Old Style", "Palatino Linotype", Palatino, Georgia, serif` for the board title and card titles maybe, and ui-monospace for badges/counts. That gives a distinct editorial feel without external fonts.
Layout:
- Header: board title (serif, large), search field, theme toggle, "New card" button.
- Three columns with header: column name, count badge (monospace pill), colored top rule or left accent per column? Keep it restrained: each column has a thin top border in column-specific muted color? Maybe subtle: To do — neutral, In progress — amber, Done — green. Small colored dot next to column title.
Cards: paper cards with title (serif, medium weight), priority badge (small pill: low = sage green, medium = amber, high = terracotta red), delete button (×, appears on hover or always small ghost). Cards have subtle border, slight shadow on drag.
Drag and drop: HTML5 drag events. Drop indicator: a horizontal line/placeholder element showing insertion position. I'll compute insertion by mouse Y relative to card midpoints, insert a placeholder element with dashed border + accent tint. Also allow dropping into empty columns.
Add modal: overlay + dialog with title input (required), priority select (segmented radio buttons maybe), column select. Validate title required (show inline error, shake). Escape closes, click on backdrop closes.
Search: filters live by title (case-insensitive). Cards that don't match get hidden (display:none). Counts could stay total counts — I'll keep counts as total cards in column, or show filtered count? Requirement says "each showing its card count" — I'll show count of visible cards when searching, e.g. "3/5"? Simpler: show matching/total when searching. Nice detail.
Theme toggle: button with sun/moon icon drawn as inline SVG (no emoji). Toggles class on root, CSS variables.
Extra polish:
- Drag ghost: default browser drag image is fine, but I'll style dragging card with reduced opacity.
- Column gets "drag-over" highlight.
- Keyboard: maybe not needed, but "press N" to open modal? Keyboard events need iframe focus; I'll skip global keyboard except Escape in modal, and maybe 'n' shortcut noted subtly. Keep it simple but Escape for modal works after click focus. Actually Escape listener on document works once user interacts; fine.
- Animations: cards fade/slide in on creation, placeholder smooth.
Also demo mode requirement: "Games must start in a self-playing demo mode" — this is not a game, so that rule doesn't apply. But "Something meaningful must be visible on first paint" — pre-populated 6 cards covers that.
Interactions to make it interesting within seconds: pre-populated board, theme toggle, search. Good.
Let me write the code.
Data model:
```js
let state = {
cards: [ {id, title, priority, column, createdAt} ... ]
}
const columns = ['todo', 'doing', 'done'];
```
Render: full re-render on state change is simplest but breaks drag animations a bit; still fine. I'll re-render columns' card lists.
Drag & drop details:
- card.draggable = true; dragstart: store dragged id, add class 'dragging' (use setTimeout to add class so drag image unaffected... actually adding class immediately affects ghost; use e.dataTransfer or setTimeout). I'll set dragging style via `dragging` class added in a setTimeout(0). Also set dataTransfer.setData('text/plain', id) and effectAllowed = 'move'.
- On each column's card-list: dragover → preventDefault, compute insertBefore target, move placeholder. Placeholder is a single DOM element reused, appended where needed.
- drop → preventDefault, insert card at placeholder position, update state column and order. Then re-render.
- dragleave on column: remove highlight if leaving (check relatedTarget not inside).
- dragend: cleanup.
Order: cards array order matters globally; column ordering = filter by column preserving array order. When dropping, I need to splice the card to the right position in the array. Approach: before re-render, compute target index. Eimpler: after drop, DOM insert card element where placeholder is, then rebuild state order from DOM. I can rebuild the full cards array by walking DOM: for each column, for each card element in DOM, push its data. But hidden cards by search are display:none — they still exist in DOM, fine. But cards removed from DOM? I keep hidden cards in DOM with hidden class, so DOM walk works. Actually let me manage it more carefully:
On drop:
1. Get placeholder's parent list and its next sibling (or determine index).
2. Remove dragged card element from its old position, insert at placeholder position.
3. Rebuild state order: iterate all column lists in document order, map element data-id to card objects, set column property, then set state.cards to that flattened order.
4. Re-render (or just leave DOM and sync state; re-render is safe).
Placeholder removal on drop: replace placeholder with card.
Simpler approach: on drop, get the insertion reference: `const next = placeholder.nextElementSibling` before removing; then insert card before next in that list; if none, append. Then rebuild.
Empty column: placeholder appended to list; drop appends.
Search filtering: on input, for each card element toggle 'filtered-out' class. Also update counts: show `visible/total` if query non-empty.
Counts: each column header count element updated on render and on search.
Modal:
- Form: title text input, priority segmented control (three radio-like buttons: Low/Medium/High, default Medium), column select (three buttons or a <select>). I'll do a native <select> styled for column, and segmented buttons for priority.
- Submit: if title empty → inline error "Title is required", input gets error styling, shake animation. Else create card in chosen column at top, close modal, re-render with a small "pop" animation on the new card.
- Open: "New card" button, or pressing "n"? I'll add N shortcut with a hint but that requires focus. I'll include keydown listener for 'n' when modal closed and not typing in an input — it works after user clicks the iframe. Nice extra. Show hint in footer.
Delete: button on card (small ×). With confirm? No confirm() allowed. Just delete directly, maybe with a tiny undo toast? Undo toast would be a nice touch: "Card deleted — Undo" toast for 5s. That's a good detail and avoids destructive accidental loss. Implement small toast.
Theme toggle: SVG sun/moon icons, swap.
Colors:
Light:
- --bg: #F4EFE6 (warm paper)
- --surface: #FDFBF7
- --ink: #23201B
- --muted: #7A7266
- --line: #E4DCCD (borders)
- --accent: #C2410C-ish terracotta... let's pick #BF4722 or #C2451E.
- column accents: todo: #8A8375 (neutral gray-taupe), doing: #B7791F amber, done: #3F7D53 green.
- priority: low #587B5B (sage), medium #B07C1F (amber), high #B3402B (terracotta red).
Dark:
- --bg: #16130F
- --surface: #1F1B16
- --ink: #EFE9DD
- --muted: #9A907F
- --line: #332C22
- accent: #E06A3F
- Adjusted priority colors brighter.
Theme via `data-theme="dark"` on html, CSS variables.
Typography:
- Board title: serif stack, big (e.g. 34px), maybe with italic accent word? "Sprint Board" or something with character: "Product — Sprint 24" Let's title: "Fieldnotes" hmm. Realistic board: "Product Roadmap — Sprint 12". I'll go with a header: small overline "ACME PRODUCT TEAM · SPRINT 12", big serif title "Delivery Board". Keep it realistic.
- Card titles: 15px, weight 600, serif? Serif card titles in a kanban can look elegant. I'll use the serif for card titles too, creating identity. Meta in monospace (ui-monospace stack).
Background texture: subtle — avoid gradients. Maybe a very faint dotted pattern? Gradients as decoration banned; a radial dot grid via background-image radial-gradient — that's technically a gradient but used as dot pattern... risky, skip. Plain solid bg with strong borders is fine. Maybe hairline grid lines? Keep solid.
Layout responsiveness: columns flex, min-width 0, wrap on small screens (grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)) at <760px maybe). At 375px, columns stack vertically full-width. Use CSS grid: `grid-template-columns: repeat(3, 1fr)` desktop; media query stacks.
Board height: cards lists scroll within column? Set column list max-height with overflow-y auto so no page horizontal scroll; vertical page scroll fine. I'll give the main board a height calc(100vh - header) with columns overflow auto on desktop; on mobile natural flow. Let me do: `#board { display:grid; gap; }` each column is flex column, `.cards { overflow-y:auto; }` with `max-height` maybe. Simpler: let page scroll naturally. But drop indicator with scrolling lists... natural page scroll is fine. I'll allow columns to scroll internally on tall screens: set `.cards { flex: 1; overflow-y: auto; min-height: 120px; }` and make the whole app height: 100dvh with hidden overflow? At 1280×800 screenshot, board should fit nicely. 6 cards fit. I'll make the app fill viewport: header fixed height, board flex:1 with columns overflow-auto. On mobile (<700px), switch to document flow with auto height and stacked columns.
Let me write it.
Card element structure:
```html
<article class="card" draggable="true" data-id="...">
<div class="card-head">
<p class="card-title">...</p>
<button class="del" aria-label="Delete">×svg</button>
</div>
<div class="card-meta">
<span class="badge high">High</span>
<span class="tag mono">ENG-241</span>?
</div>
</article>
```
Maybe add a card ID like "KAN-104" generated, adds realism. And a small meta line. Keep it minimal: badge + maybe column-specific? Just badge and an id chip. Realistic cards prepopulated:
To do:
1. "Audit onboarding funnel analytics" — high
2. "Draft Q3 pricing page copy" — low
3. "Spike: evaluate edge rendering" — medium
In progress:
4. "Redesign settings navigation" — medium
5. "Fix flaky checkout tests" — high
Done:
6. "Ship dark mode tokens" — medium
Good, 6 cards.
Drag placeholder styling: height ~ 40px, dashed 1.5px border in accent color, rounded, faint accent tint background (rgba). Add small "+" ? Keep line. Also when dragging over column, column gets highlight: border color change / subtle inset ring.
Drag over card midpoints: on dragover of the list, iterate children (excluding placeholder & dragging card), find first whose midpoint Y > e.clientY → insert placeholder before it; else append.
Counts pill: monospace, e.g. "3". When searching: "1/3".
Search input: with a small magnifier SVG icon, and clear button (×) when text present. Live filter with input event.
Theme toggle: button with sun and moon SVGs, toggle aria-pressed. Transition colors smoothly (transition on background-color/color where feasible — apply transition to key elements or use `* { transition: background-color .25s }`? That can be heavy but okay; I'll add transitions on specific: body, .card, .col, inputs, buttons).
Toast: fixed bottom, dark pill with "Card deleted" + Undo button; auto-dismiss after 4.5s; clicking undo reinserts card at old index and column.
Modal: overlay with backdrop blur? backdrop-filter allowed (not a gradient). Simple: rgba backdrop. Dialog: surface bg, serif heading "New card", title input, priority segmented (3 buttons acting as radios via hidden input or just buttons with .selected class + aria), column select (native select styled). Buttons: "Add card" (primary, accent solid), "Cancel" (ghost). Inline error message under title. Focus management: focus title on open, Escape closes, return focus to "New card" button.
Also drag: disable draggable while searching? Not necessary; hidden cards just won't be visible. Fine.
Also handle: dragging while filtered — dropped card should remain subject to filter; re-render applies filter function to set classes. So render applies current query. Good — I'll have `applyFilter()` called at end of render.
Edge: HTML5 drag needs `e.preventDefault()` on dragover of list to allow drop. Also `drop` handler on list.
Let me also add nice touch: card hover lifts slightly (translateY(-1px), shadow). Column empty state: "No cards yet — drag one here" faint text shown when list has no visible cards (not counting filtered?). Show empty hint when zero cards total in column; if filtered to zero show "No matches". Small detail.
Accessibility: aria-labels, role="dialog".
Write the code now. Careful with sizes: viewport 1280×800, no horizontal scrollbar.
Font stacks:
```css
--serif: "Iowan Old Style", "Palatino Linotype", "Book Antiqua", Palatino, Georgia, serif;
--mono: ui-monospace, "SF Mono", "Cascadia Code", Menlo, Consolas, monospace;
```
Header layout: left: overline + title; right: search, new card button, theme toggle. At small widths wrap.
Let me draft full HTML.
Also "n" shortcut: document keydown, if modal open ignore, if e.target is input/select/textarea ignore; if key 'n' open modal. Footer hint: "N — new card". Also '/' to focus search. Nice keyboard touches (works after iframe click; that's fine, they're extras).
Delete button: small, top-right of card, shows a × SVG, low contrast until hover.
Card entrance animation: keyframes cardIn { from {opacity:0; transform: translateY(6px) scale(.98)} }.
Now, careful with rebuild-order logic:
```js
function drop(e){
e.preventDefault();
const id = dragId; if(!id) return;
const list = e.currentTarget; // .cards
const placeholder = ensurePlaceholder();
const ref = placeholder.parentElement === list ? placeholder.nextElementSibling : null;
const cardEl = document.querySelector(`.card[data-id="${id}"]`);
if (ref) list.insertBefore(cardEl, ref); else list.appendChild(cardEl);
// rebuild
const order = [];
document.querySelectorAll('.column').forEach(col => {
const colId = col.dataset.col;
col.querySelectorAll('.cards .card').forEach(el => {
const c = state.cards.find(c => c.id === el.dataset.id);
if (c) { c.column = colId; order.push(c); }
});
});
state.cards = order;
removePlaceholder();
render();
}
```
But wait: `ref` may be inside another list if placeholder's parent isn't the current list — I set placeholder in dragover of each list, so its parent should be the list being hovered. But drop event fires on list; placeholder should be in it. However if drop fires on column (not list), I attach drop handler on column too (delegation). I'll attach handlers to `.column` elements but compute list = column's .cards. Attach dragover/drop on the `.cards` list and also handle drop on column container padding. Simplest: attach to column, list inside.
Placeholder: created once `const ph = document.createElement('div'); ph.className='drop-indicator';` with label text "Drop here".
dragover on column:
```js
col.addEventListener('dragover', e => {
e.preventDefault();
if (!dragId) return;
col.classList.add('over');
const list = col.querySelector('.cards');
const els = [...list.querySelectorAll('.card:not(.dragging)')].filter(el => !el.classList.contains('filtered-out')? true : ...);
```
Should filtered-out cards count as insertion targets? If hidden, they take no space; skip them for midpoint calc. Insert placeholder before first visible card whose rect midpoint > clientY; else append at end (before nothing). Note placeholder itself is not a card.
```js
let ref = null;
for (const el of els) {
const r = el.getBoundingClientRect();
if (e.clientY < r.top + r.height/2) { ref = el; break; }
}
if (ref) list.insertBefore(ph, ref); else list.appendChild(ph);
});
```
dragleave: remove 'over' class when leaving column (check e.relatedTarget outside col). dragend on card: remove placeholder, remove classes, dragId=null.
Also dropping outside any column: dragend handles cleanup (state unchanged).
Re-render: `render()` builds each column list from state.cards filtered by column. That resets placeholder—fine since we remove it.
Actually for drop I re-render after rebuild anyway, so DOM insertion before rebuild is just to compute order — correct.
Counts: `updateCounts()` — total per column; when query: `visible/total`.
Search clear: '×' button inside input wrapper.
Toast for delete with undo: store lastDeleted = {card, index}; show toast; timer.
Now write CSS thoroughly. Estimated ~450 lines total. Let's write.
Column styling: surface bg? In light theme: columns slightly darker than page? Page bg cream, columns surface white-ish with 1px border. Cards inside columns: give cards a distinct bg (surface white) vs column (slightly tinted). Let: page --bg cream #F1EBDF, column --col-bg #FAF7F0 with border --line #E3DACB, card white #FFFFFF border #E6DECF with tiny shadow. Dark: page #131009, column #1D1913, card #26211A border #352E22.
Column header: dot (column color), name in mono uppercase small letterspaced? Or serif? I'll do: small colored dot, name in --mono uppercase 11px letterspacing, count pill right-aligned. A hairline rule under header.
Top of column: maybe 2px accent bar in column color across the top of each column — that's a flat color strip, not gradient, adds identity. Yes: `border-top: 3px solid var(--col-accent)` on column. Nice.
Theme toggle icons: two inline SVGs, show/hide by theme.
Modal priority segmented: buttons with data-priority; clicking sets selected; hidden state variable `chosenPriority`. Default 'medium'. Column: native select with three options; default 'todo'.
Form submit: form onsubmit preventDefault.
Escape: document keydown Escape → close modal if open.
Focus trap: minimal (just focus title; Tab may escape but fine).
Let me now also ensure "no horizontal scrollbar": overflow-x hidden on body just in case; grid with minmax(0,1fr).
Mobile: media max-width 760px: header wraps, board grid-template-columns: 1fr, columns min-height auto; app height auto with page scroll; cards list max-height none. I'll structure app as flex column height 100dvh on desktop; on mobile override height:auto, overflow visible.
Desktop:
```css
html,body{height:100%}
body{overflow:hidden} /* desktop */
@media (max-width:760px){ body{overflow:auto} #app{height:auto} .cards{overflow:visible; flex:none} }
```
Careful body overflow hidden could clip toast? Toast fixed, fine.
Hmm, but with body overflow hidden and app height 100dvh, if columns overflow they scroll internally. Good for 1280×800.
Write final code.
Card title serif: 15.5px, line-height 1.35, weight 600. Card id chip: mono 10.5px muted like "K-104". Badge: mono 10px uppercase, padding 2px 7px, border 1px solid + tinted bg. Use color-mix for tinted backgrounds from priority color: `color-mix(in srgb, var(--p) 14%, transparent)`. Supported in latest Chrome. Good.
Delete button svg: simple ×: `<svg viewBox="0 0 12 12"><path d="M2 2l8 8M10 2l-8 8" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>`.
Search icon svg: circle + line.
Sun/Moon svg: sun: circle with rays; moon: path crescent.
New card button: accent bg, ink? Accent bg with paper text: light theme accent #B4441E bg with #FFF7EC text; hover darken. Fine.
Header title: overline mono 10px letterspacing 2px uppercase muted: "ACME PRODUCT · SPRINT 12"; H1 serif 28px "Delivery Board" — maybe with a long dash detail. Right controls.
Footer hint bar bottom: mono 10.5px muted: "N new card · / search · drag cards between columns". Maybe put hint in header instead to save vertical space. Footer inside app bottom, small. On desktop with fixed height, footer takes ~28px; fine at 800px tall.
Let me compute vertical budget: header ~86px, footer ~30px, board rest ~680px. Fine.
Now write everything.
```html
<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Delivery Board — Kanban</title>
<style>...</style>
</head>
<body>
<div id="app">
<header>
<div class="brand">
<p class="overline">Acme product · sprint 12</p>
<h1>Delivery Board</h1>
</div>
<div class="controls">
<div class="search" id="searchWrap">
<svg ...></svg>
<input id="search" type="text" placeholder="Search cards…" />
<button id="clearSearch" class="icon-btn clear" aria-label="Clear search" hidden>×</button>
</div>
<button id="themeToggle" class="icon-btn" aria-label="Toggle theme">sun/moon svgs</button>
<button id="newCard" class="btn-primary">+ New card</button>
</div>
</header>
<main id="board">
<!-- columns generated? or static -->
</main>
<footer>
<p>...</p>
</footer>
</div>
<div class="overlay" id="overlay" hidden> modal </div>
<div class="toast" id="toast" ...>
<script>...</script>
```
Columns: I'll write static HTML for three columns (clearer markup), each with data-col, header with dot/name/count, `.cards` list, and JS renders cards into them.
Board CSS:
```css
#board { flex:1; display:grid; grid-template-columns: repeat(3, minmax(0,1fr)); gap:14px; padding: 0 22px 18px; min-height:0; }
.column { display:flex; flex-direction:column; min-height:0; background:var(--col-bg); border:1px solid var(--line); border-top:3px solid var(--accent-col); border-radius:10px; }
.column.over { outline/… border color accent, background tint }
.col-head { display:flex; align-items:center; gap:8px; padding:11px 12px 9px; border-bottom:1px solid var(--line); }
.dot {width:7px;height:7px;border-radius:50%;background:var(--accent-col)}
.col-name { font: 600 11px var(--mono); letter-spacing:.12em; text-transform:uppercase; color:var(--muted)? or ink }
.count { margin-left:auto; font: 600 10.5px var(--mono); background:...; border:1px solid var(--line); padding:2px 8px; border-radius: 999px; color: var(--muted) }
.cards { flex:1; overflow-y:auto; padding:10px; display:flex; flex-direction:column; gap:8px; min-height:60px }
.card { background:var(--card-bg); border:1px solid var(--line); border-radius:8px; padding:10px 12px 11px; cursor:grab; box-shadow:0 1px 2px rgba(0,0,0,.05); transition: transform .15s, box-shadow .15s, border-color .15s }
.card:hover { transform: translateY(-1px); border-color: ...; }
.card.dragging { opacity:.35; cursor:grabbing }
.card.filtered-out { display:none }
.drop-indicator { height: 3px? }
```
Drop indicator: I'll make it a slim slot: `height:6px; border-radius:3px; background:var(--accent); opacity:.8` plus maybe with dashed outline full card-height. A clear visual drop indicator: I'll do a rounded bar of ~4-5px in accent plus a subtle dashed container? Simplest clear: a 3px accent line with small round caps + faint accent glowless. Plus add "· · ·"? Keep: `border-radius:99px; background:var(--accent); box-shadow none;` plus label via ::after? Fine: bar only, very clear against muted bg. Also add slight margin. Also show a dashed outline of card size? I think a chunky bar (6px, accent, rounded) reads well. Also the hovered column tints. Good enough; maybe indicator shows a small circle at left? Skip.
Actually let me make the drop indicator more visible: a full-width rounded rect with dashed accent border and 34px height and tiny centered mono label "drop here"? That's a clearer "drop indicator" per requirement. Height 36px, border 1.5px dashed accent, radius 8, bg accent 8% tint, centered mono 10px accent label. Yes, do that — very explicit.
Badge colors:
```css
.badge{ --p: var(--pri); color:var(--p); background: color-mix(in srgb, var(--p) 12%, transparent); border:1px solid color-mix(in srgb, var(--p) 35%, transparent); ... }
.badge.low{--p:#5c8a63} .badge.medium{--p:#a97a1c} .badge.high{--p:#b8432c}
dark theme overrides brighter: low #7fb387, medium #d3a04a, high #e0684a.
```
Card head layout: title left, delete right (button 22px ghost). Meta row: badge + id chip.
Empty state per list: element `.empty` inside list shown via CSS when needed — easier in JS: after render, if column total==0 show `<p class="empty">Nothing here — drop a card or add one</p>`; if filtered==0 && total>0 `<p class="empty">No matches</p>`.
Modal markup:
```html
<div class="overlay" id="overlay" hidden>
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="mTitle">
<h2 id="mTitle">New card</h2>
<form id="cardForm" novalidate>
<label class="field-label" for="fTitle">Title <span class="req">required</span></label>
<input id="fTitle" type="text" maxlength="80" placeholder="e.g. Ship search analytics" autocomplete="off">
<p class="err" id="fErr" hidden>Give the card a title first.</p>
<label class="field-label">Priority</label>
<div class="seg" id="segPriority">
<button type="button" data-p="low">Low</button>
<button type="button" data-p="medium" class="sel">Medium</button>
<button type="button" data-p="high">High</button>
</div>
<label class="field-label" for="fCol">Column</label>
<select id="fCol">
<option value="todo">To do</option>
<option value="doing">In progress</option>
<option value="done">Done</option>
</select>
<div class="modal-actions">
<button type="button" id="mCancel" class="btn-ghost">Cancel</button>
<button type="submit" class="btn-primary">Add card</button>
</div>
</form>
</div>
</div>
```
Segmented buttons colored? Selected gets accent outline; maybe selected shows its priority color. I'll style selected with ink bg? Keep neutral: selected = ink-colored bg with paper text; but for high/low maybe show dot of the color before label. Add small dot inside each segment button colored by priority. Nice.
Overlay show: remove hidden, add class 'open' for fade/scale animation.
JS structure:
```js
const $ = s => document.querySelector(s);
let uid = 1; const nid = () => 'k' + (uid++);
const state = { cards: [...seed] };
seed cards with ids k1..k6? I'll generate ids like "K-101" displayed chip; internal id separate.
```
Displayed chip: `#${c.ref}` where ref like "K-104". I'll generate refs incrementing from 107.
Render:
```js
function render(){
document.querySelectorAll('.column').forEach(col => {
const id = col.dataset.col;
const list = col.querySelector('.cards');
list.innerHTML = '';
state.cards.filter(c=>c.column===id).forEach(c=>list.appendChild(cardEl(c)));
});
applyFilter();
updateCounts();
updateEmptyStates();
}
```
applyFilter sets .filtered-out per card, updateCounts computes.
Delete:
```js
function deleteCard(id, el){
const idx = state.cards.findIndex(c=>c.id===id);
const [card] = state.cards.splice(idx,1);
lastDeleted = {card, idx};
render();
showToast();
}
```
Undo: splice back, render, hide toast.
Toast markup: `<div id="toast" class="toast" hidden><p>Card deleted.</p><button id="undoBtn">Undo</button></div>` with timer 4500ms.
Theme toggle:
```js
const root=document.documentElement;
btn.onclick=()=>{ root.dataset.theme = root.dataset.theme==='dark'?'light':'dark'; }
```
Icons: `.icon-btn svg` show per theme via CSS: `html[data-theme=light] .icon-moon{display:none}` etc. Show moon in light (click → dark) — either way; show the icon of target theme. I'll show moon in light theme ("switch to dark").
Keyboard:
```js
document.addEventListener('keydown', e=>{
if(e.key==='Escape' && !overlay.hidden){ closeModal(); return; }
const tag = e.target.tagName;
if(tag==='INPUT'||tag==='SELECT'||tag==='TEXTAREA'||e.target.isContentEditable) return;
if(overlay.hidden){
if(e.key==='n'||e.key==='N'){ e.preventDefault(); openModal(); }
if(e.key==='/'){ e.preventDefault(); searchInput.focus(); }
}
});
```
Drag wiring: attach once to each column at init.
Also make cards draggable attribute true.
dataTransfer: `e.dataTransfer.setData('text/plain', id); e.dataTransfer.effectAllowed='move';` and on dragover `e.dataTransfer.dropEffect='move'`.
dragstart: dragId = id; setTimeout(()=>el.classList.add('dragging')); also store.
dragend: dragId=null; remove dragging class; remove placeholder; remove .over classes.
Note: full re-render on dragend isn't needed since drop already handled. If drop happened outside lists, no changes. But dragging class removal: element persists. fine.
One issue: after drop I call render() which recreates elements — dragging class gone anyway.
Search input events: 'input' → applyFilter + counts + empty states; clear button toggling.
Modal open: overlay.hidden=false; requestAnimationFrame add .open; fTitle.focus(); reset form: title '', err hidden, priority 'medium' selected, column 'todo'.
Segmented: click handler sets sel, stores chosenPriority variable.
Validation on submit: if title.trim()==='' → show err, shake input (re-add animation class), focus. else create card at beginning of chosen column: unshift? Insert at index after... to put at top of that column: insert before first card of that column in array. Compute firstIdx = state.cards.findIndex(c=>c.column===col); insert there; if none, push. New card gets highlight animation: add class 'new' on element in cardEl via flag, or after render find element and add class. I'll pass a `justAdded` id; cardEl checks.
updateCounts: query per column visible/total.
Let me also ensure the drop indicator isn't matched by `.card` queries — it's a div.drop-indicator, fine.
Midpoint loop must skip filtered-out cards: `.card:not(.filtered-out):not(.dragging)`.
Alright, footer: `<footer><p class="hint">Drag cards between columns · <kbd>N</kbd> new card · <kbd>/</kbd> search</p></footer>`.
Now, write final file. Watch out: no emoji anywhere. Use "·" separators fine. The "+" in "+ New card" — fine as text? I'll use a small plus SVG inside button instead to be classy. Or just text "New card". I'll add an inline SVG plus.
Also, transition for theme change: `body, .column, .card, input, select, button { transition: background-color .25s ease, color .2s ease, border-color .25s ease }` — careful: card has transform transition already; merge.
Let me write CSS variables:
```css
:root{
--bg:#F2ECDF;
--col-bg:#FAF6EC;
--card-bg:#FFFDF8;
--ink:#241F17;
--muted:#877A66;
--line:#E2D8C4;
--line-strong:#CEC2A9;
--accent:#B4441E;
--accent-ink:#FFF6EA;
--shadow:0 1px 2px rgba(60,45,20,.08), 0 4px 14px rgba(60,45,20,.05);
--pri-low:#4F7D58; --pri-med:#9A7414; --pri-high:#B23F27;
--sel:#241F17;
}
html[data-theme="dark"]{
--bg:#141109;
--col-bg:#1E1A12;
--card-bg:#282217;
--ink:#EDE5D3;
--muted:#948972;
--line:#37301F;
--line-strong:#4A4128;
--accent:#E06A38;
--accent-ink:#1C0F06;
--shadow:0 1px 2px rgba(0,0,0,.5), 0 6px 18px rgba(0,0,0,.35);
--pri-low:#7FB689; --pri-med:#D5A44A; --pri-high:#E76A4B;
--sel:#EDE5D3;
}
```
Wait accent-ink in dark: accent button bg #E06A38 with dark text #1C0F06 — good contrast.
Column accents per column:
```css
.column[data-col="todo"]{ --col-accent:#8B8069; }
.column[data-col="doing"]{ --col-accent:#C08A1E; }
.column[data-col="done"]{ --col-accent:#4F7D58; }
```
Dark variants slightly brighter:
```css
html[data-theme="dark"] .column[data-col="todo"]{--col-accent:#9A8F75}
... doing #D5A44A, done #7FB689
```
Dot uses --col-accent.
Column hover/over: `.column.over{ border-color: color-mix(in srgb, var(--accent) 55%, var(--line)); background: color-mix(in srgb, var(--accent) 5%, var(--col-bg)); }` plus maybe box-shadow ring.
Search field: bordered pill, bg card-bg, icon left, clear right. Width ~230px desktop.
OK. Also `select` and inputs styling in dark: color-scheme to make native select dropdown dark: `html[data-theme=dark]{ color-scheme: dark }` and light `color-scheme: light`.
Fonts on inputs: body font — body font: I'll use the serif as base? Body base: serif for titles, but UI text (buttons, inputs) maybe also serif could look nice editorial. Buttons in mono uppercase could be charming. Decide:
- H1: serif 30px.
- Card titles: serif 15.5px 600.
- Column names: mono 11px uppercase tracking.
- Buttons: mono 12px 600 uppercase? "NEW CARD" uppercase mono with letterspacing — distinctive.
- Inputs: serif 14px? Serif input text is okay. I'll use serif for input text (they're card titles anyway — matching). Good coherence.
- Badges/chips/counts/hints: mono.
Body font-family: set to serif stack globally, mono where needed. Fine.
Modal heading serif 22px.
Now finalize JS carefully to avoid bugs.
Full code:
```js
(function(){
const $ = (s,r=document)=>r.querySelector(s);
const $$ = (s,r=document)=>[...r.querySelectorAll(s)];
let refCounter = 107;
const state = { cards: [
{id:'c1', ref:101, title:'Audit onboarding funnel analytics', priority:'high', column:'todo'},
{id:'c2', ref:102, title:'Draft Q3 pricing page copy', priority:'low', column:'todo'},
{id:'c3', ref:103, title:'Spike: evaluate edge rendering options', priority:'medium', column:'todo'},
{id:'c4', ref:104, title:'Redesign settings navigation', priority:'medium', column:'doing'},
{id:'c5', ref:105, title:'Fix flaky checkout integration tests', priority:'high', column:'doing'},
{id:'c6', ref:106, title:'Ship dark mode color tokens', priority:'medium', column:'done'},
]};
let uid = 7;
```
cardEl(c, isNew):
```js
function cardEl(c){
const el = document.createElement('article');
el.className='card'; el.draggable=true; el.dataset.id=c.id;
el.innerHTML = `
<div class="card-top">
<h3 class="card-title"></h3>
<button class="del" aria-label="Delete card">${X_SVG}</button>
</div>
<div class="card-meta">
<span class="badge ${c.priority}">${c.priority}</span>
<span class="chip">K-${c.ref}</span>
</div>`;
el.querySelector('.card-title').textContent = c.title;
el.querySelector('.del').addEventListener('click', ()=>deleteCard(c.id));
enableDrag(el, c.id);
return el;
}
```
textContent for title (XSS safe).
Badge label capitalized: priority stored lowercase; label: c.priority[0].toUpperCase()+slice.
Drag:
```js
let dragId = null;
const ph = document.createElement('div');
ph.className='drop-indicator';
ph.innerHTML = '<span>drop here</span>';
```
CSS ::before? I'll put span inside.
enableDrag:
```js
function enableDrag(el, id){
el.addEventListener('dragstart', e=>{
dragId = id;
e.dataTransfer.effectAllowed='move';
e.dataTransfer.setData('text/plain', id);
setTimeout(()=>el.classList.add('dragging'),0);
});
el.addEventListener('dragend', ()=>{
el.classList.remove('dragging');
dragId=null;
ph.remove();
$$('.column').forEach(c=>c.classList.remove('over'));
});
}
```
Column wiring at init:
```js
$$('.column').forEach(col=>{
const list = col.querySelector('.cards');
col.addEventListener('dragover', e=>{
if(!dragId) return;
e.preventDefault();
e.dataTransfer.dropEffect='move';
col.classList.add('over');
const vis = $$('.card', list).filter(el=>!el.classList.contains('dragging') && !el.classList.contains('filtered-out'));
let ref=null;
for(const el of vis){
const r=el.getBoundingClientRect();
if(e.clientY < r.top + r.height/2){ ref=el; break; }
}
if(ref) list.insertBefore(ph, ref); else list.appendChild(ph);
});
col.addEventListener('dragleave', e=>{
if(!col.contains(e.relatedTarget)) col.classList.remove('over');
});
col.addEventListener('drop', e=>{
if(!dragId) return;
e.preventDefault();
const list2 = col.querySelector('.cards');
const ref = ph.parentElement===list2 ? ph.nextElementSibling : null;
const cardElMoved = $(`.card[data-id="${CSS.escape(dragId)}"]`);
...
});
});
```
CSS.escape for attribute selector safety (ids are simple though).
Drop rebuild:
```js
if(cardElMoved){
if(ref) list2.insertBefore(cardElMoved, ref); else list2.appendChild(cardElMoved);
}
const order=[];
$$('.column').forEach(c=>{
const colId=c.dataset.col;
$$('.card', c.querySelector('.cards')).forEach(el=>{
const card = state.cards.find(x=>x.id===el.dataset.id);
if(card){ card.column=colId; order.push(card); }
});
});
state.cards=order;
ph.remove(); dragId=null; col.classList.remove('over');
render();
```
Note drop fires then dragend fires too — dragend sets dragId null again, removes ph (already removed) — harmless.
Wait: in dragend, `el.classList.remove('dragging')` — but if render happened, original el replaced; new element never had class. Fine.
Empty states in render:
```js
function render(){
$$('.column').forEach(col=>{
const list=col.querySelector('.cards');
list.innerHTML='';
state.cards.filter(c=>c.column===col.dataset.col).forEach(c=>list.appendChild(cardEl(c)));
});
applyFilter();
}
```
applyFilter:
```js
const searchInput = $('#search');
function applyFilter(){
const q = searchInput.value.trim().toLowerCase();
$$('.column').forEach(col=>{
const list=col.querySelector('.cards');
let total=0, vis=0;
$$('.card', list).forEach(el=>{
total++;
const title = el.querySelector('.card-title').textContent.toLowerCase();
const match = !q || title.includes(q);
el.classList.toggle('filtered-out', !match);
if(match) vis++;
});
// counts
col.querySelector('.count').textContent = q ? `${vis}/${total}` : String(total);
// empty hint
let hint = list.querySelector('.empty');
if(total===0 || (q && vis===0)){
if(!hint){ hint=document.createElement('p'); hint.className='empty'; list.appendChild(hint); }
hint.textContent = total===0 ? 'Nothing here yet — drop a card or add one.' : 'No cards match your search.';
} else if(hint) hint.remove();
});
$('#clearSearch').hidden = !searchInput.value;
}
```
Hmm: count element — in col header. Total counts should show when query empty. Also show empty hint when total==0 even without query. Good.
Delete + toast:
```js
let lastDeleted=null, toastTimer=null;
function deleteCard(id){
const idx=state.cards.findIndex(c=>c.id===id);
if(idx<0) return;
lastDeleted={card:state.cards[idx], idx};
state.cards.splice(idx,1);
render();
showToast();
}
function showToast(){
const t=$('#toast');
t.hidden=false;
t.classList.add('show');
clearTimeout(toastTimer);
toastTimer=setTimeout(hideToast, 4500);
}
function hideToast(){ $('#toast').classList.remove('show'); clearTimeout(toastTimer); }
$('#undo').addEventListener('click', ()=>{
if(!lastDeleted) return;
state.cards.splice(Math.min(lastDeleted.idx, state.cards.length), 0, lastDeleted.card);
lastDeleted=null;
hideToast();
render();
});
```
Toast hide animation: class 'show' controls opacity/translate; keep hidden attr only initially. Simpler: control display via class only: `.toast{... opacity:0; pointer-events:none; transform:...} .toast.show{opacity:1}`. Use that; remove hidden attribute handling — set no hidden attr, just class. But initial screenshot shows nothing since opacity 0 & pointer-events none. OK.
Modal:
```js
const overlay=$('#overlay'), form=$('#cardForm'), fTitle=$('#fTitle'), fCol=$('#fCol'), fErr=$('#fErr');
let chosenPriority='medium', justAdded=null;
function openModal(){
form.reset(); chosenPriority='medium';
syncSeg(); fErr.hidden=true; fTitle.classList.remove('invalid');
fCol.value='todo';
overlay.hidden=false;
overlay.classList.add('open'); // for animation use requestAnimationFrame? Use: overlay.hidden=false then rAF add class
setTimeout(()=>fTitle.focus(),30);
}
```
Animation: define `.overlay{opacity:0; transition...} .overlay.open{opacity:1}` and `.modal{transform:translateY(8px) scale(.98)} .overlay.open .modal{transform:none}`. With hidden attribute toggling: when opening, remove hidden then rAF add open; when closing remove open, wait transitionend/150ms then hidden=true. Implement:
```js
function openModal(){
overlay.hidden=false;
requestAnimationFrame(()=>overlay.classList.add('open'));
...
}
function closeModal(){
overlay.classList.remove('open');
$('#newCardBtn').focus?;
setTimeout(()=>overlay.hidden=true,180);
}
```
Overlay click outside modal closes: overlay.addEventListener('click', e=>{ if(e.target===overlay) closeModal(); });
Segment:
```js
const seg=$('#segPriority');
function syncSeg(){ $$('button',seg).forEach(b=>b.classList.toggle('sel', b.dataset.p===chosenPriority)); }
seg.addEventListener('click', e=>{ const b=e.target.closest('button'); if(!b) return; chosenPriority=b.dataset.p; syncSeg(); });
```
Submit:
```js
form.addEventListener('submit', e=>{
e.preventDefault();
const title=fTitle.value.trim();
if(!title){ fErr.hidden=false; fTitle.classList.add('invalid'); fTitle.focus();
// shake
fTitle.classList.remove('shake'); void fTitle.offsetWidth; fTitle.classList.add('shake');
return; }
const col=fCol.value;
const card={id:'c'+(uid++), ref:refCounter++, title, priority:chosenPriority, column:col};
const firstIdx=state.cards.findIndex(c=>c.column===col);
if(firstIdx===-1) state.cards.push(card); else state.cards.splice(firstIdx,0,card);
justAdded=card.id;
closeModal();
render();
justAdded=null;
});
```
justAdded animation: in cardEl, if c.id===justAdded add class 'enter'. Also 'input' on fTitle clears error.
render also should clear search-filter classes appropriately — applyFilter runs each render.
Init: render(); theme listener; newCard button; keyboard shortcuts; search input listener.
Theme:
```js
$('#themeToggle').addEventListener('click', ()=>{
const next = document.documentElement.dataset.theme==='dark'?'light':'dark';
document.documentElement.dataset.theme=next;
});
```
Search:
```js
searchInput.addEventListener('input', applyFilter);
$('#clearSearch').addEventListener('click', ()=>{ searchInput.value=''; applyFilter(); searchInput.focus(); });
```
Keyboard shortcuts as above.
Now HTML for columns:
```html
<main id="board">
<section class="column" data-col="todo" aria-label="To do">
<header class="col-head">
<span class="dot" aria-hidden="true"></span>
<h2 class="col-name">To do</h2>
<span class="count">0</span>
</header>
<div class="cards" ...></div>
</section>
...
</main>
```
Header/footer as planned.
Check: no horizontal scrollbar at 375px: media query <760px: `#app{height:auto} body{overflow:auto} #board{grid-template-columns:1fr; padding:0 14px 14px} .cards{overflow:visible; max-height:none} header{flex-wrap:wrap}`.
Desktop: `body{overflow:hidden}` — but on very short windows content clipped... acceptable. Actually to be safe, on desktop maybe body overflow auto but app min-height 100dvh, columns internal scroll via max-height... complexity. I'll do: `#app{height:100dvh; display:flex; flex-direction:column}` `#board{flex:1; min-height:0}` `.cards{overflow-y:auto; min-height:0}`. body overflow hidden only ≥761px. Fine.
Scrollbar styling: thin scrollbars for .cards via ::-webkit-scrollbar.
Let me now also double check the "hidden" overlay initial: `hidden` attribute plus CSS `.overlay[hidden]{display:none}` maybe needed since we set display:flex which overrides hidden. Yes! If .overlay has display:flex, the hidden attribute is overridden. Add `[hidden]{display:none!important}` global.
Also toast: don't use hidden; use class-based.
Delete button visibility: `opacity:0; .card:hover .del{opacity:1}` — but touch devices... also `:focus-visible`. Keep hover + focus. Hmm, on first screenshot cards visible without delete buttons — fine, they appear on hover. But maybe judges want to see delete button... requirement says "delete a card with a button on it" — it exists. To be safe, make it always visible but subtle (muted color, becomes accent on hover). I'll keep it visible but low-contrast. Yes — visible at 40% opacity normally.
Card layout: `.card-top{display:flex; gap:8px; align-items:flex-start}` title flex:1.
Del button: 20px square, border-radius 5, color muted, hover: color high-priority red + bg tint.
Let me write everything now, final pass.
Font sizes ok at 1280×800: board fills nicely with 6 cards (3/2/1).
Title h1 30px serif; overline mono 10px tracking .18em.
One more flourish: header hairline bottom border; brand overline accent-colored small dash before text? Add `::before` small square accent. Fine.
Footer: border-top hairline, mono 10.5px muted, contains hints with kbd styled boxes. Right side maybe "state kept in memory — refresh resets" note. Nice honest touch: "In-memory state · nothing persisted". Sure.
kbd style: border, radius 4, padding 1px 5px, mono 10px.
Now write it all. Also verify no console errors: careful all getElementById exist.
Potential issue: dragover fires on column; when hovering over the placeholder itself (it's inside list), getBoundingClientRect loops fine.
dragleave relatedTarget may be null (when leaving window); `col.contains(null)` returns false → removes 'over'. Good.
Also when drop happens on the page body outside columns, no drop handler → dragend cleanup only. Good.
Edge: dropping onto the same position — reorder logic still works.
Filter + drag: dragging a visible card while some hidden — midpoint calc only over visible. On rebuild, hidden cards included via DOM walk (they're in DOM). Good.
Write the final answer now. Also small CSS detail: `.card{cursor:grab}` `.card:active{cursor:grabbing}`.
Badge colors using color-mix — Chrome supports. Good.
Let me write the complete file.
For the search icon and clear icon, plus sun/moon, plus X (delete), plus plus (new card) — inline SVGs.
Sun SVG:
```html
<svg class="ic-sun" viewBox="0 0 20 20" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><circle cx="10" cy="10" r="3.4"/><path d="M10 2v2M10 16v2M2 10h2M16 10h2M4.3 4.3l1.4 1.4M14.3 14.3l1.4 1.4M15.7 4.3l-1.4 1.4M5.7 14.3l-1.4 1.4"/></svg>
```
Moon:
```html
<svg class="ic-moon" viewBox="0 0 20 20" width="16" height="16" fill="currentColor"><path d="M14.8 12.9A6.5 6.5 0 0 1 7.1 3.3a.6.6 0 0 0-.8-.7 7.3 7.3 0 1 0 9.4 9.4.6.6 0 0 0-.9-.1z"/></svg>
```
Plus icon:
```html
<svg viewBox="0 0 14 14" width="12" height="12" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M7 2v10M2 7h10"/></svg>
```
Search icon:
```html
<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><circle cx="7" cy="7" r="4.5"/><path d="M10.5 10.5L14 14"/></svg>
```
X icon:
```html
<svg viewBox="0 0 12 12" width="11" height="11" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"><path d="M2.5 2.5l7 7M9.5 2.5l-7 7"/></svg>
```
Theme icon CSS: `html[data-theme="light"] .ic-sun{display:none}` `html[data-theme="dark"] .ic-moon{display:none}` — show moon in light. Wait: I want to show icon of what you'll switch TO or current? Convention: show moon in light mode. `.ic-sun` hidden in light. In dark, show sun. Good.
Toast styling: fixed bottom center, surface bg, border, shadow, mono text + underline accent undo button.
Write final. Also make sure `#board` columns: `min-width:0`.
Card enter animation:
```css
.card.enter{ animation: cardIn .35s cubic-bezier(.2,.8,.3,1) }
@keyframes cardIn{ from{ opacity:0; transform: translateY(8px) scale(.97) } }
```
But .card has hover transform transition; animation overrides during play — fine.
Shake:
```css
.shake{ animation: shake .3s }
@keyframes shake{ 25%{transform:translateX(-4px)} 50%{transform:translateX(4px)} 75%{transform:translateX(-2px)} }
.invalid{ border-color: var(--pri-high) !important }
```
Alright, writing the answer.
Estimate header controls wrap on medium width; use flex-wrap and gap.
Final check on requirement "A search field that filters cards live by title" ✓. "light/dark theme toggle" ✓. "counts" ✓. "modal form (title required)" ✓. "delete button" ✓. "drag and drop between columns with clear drop indicator" ✓. "6 realistic cards" ✓. "state in memory" ✓. No frameworks/libraries ✓. No storage APIs ✓.
Write it.