Nemotron 3.5 Lightning (free)
by NVIDIAReasoningClean
Running live in a sandboxed frame. Click inside it before using the keyboard.
How it went
- Total time
- 5 min 10 s
- Cost
- $0
- Output tokens
- 22,500
- Spent thinking
- 18,737 tokens
- Speed
- 72.7 tok/s
- Console errors
- 0
- First token after
- 314 ms
- First visible text after
- 4 min 22 s
- Prompt tokens
- 513
- Code size
- 13.9 KB
- Finish reason
- stop
- Served by
- Nvidia
- Model id
nvidia/nemotron-3.5-lightning:free- Reasoning sent
model default- Max output
- 32,000 tokens
- Generated
- Nov 9, 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 · 458 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Kanban Board</title>
<style>
/* Root variables for themes */
:root {
--bg-color: #f5f5f5;
--card-bg: #ffffff;
--text-color: #333333;
--priority-low: #4caf50;
--priority-medium: #ffc107;
--priority-high: #f44336;
--border-color: #ddd;
}
/* Dark theme */
[data-theme="dark"] {
--bg-color: #1e1e1e;
--card-bg: #252526;
--text-color: #e0e0e0;
--priority-low: #66bb6a;
--priority-medium: #ffb300;
--priority-high: #ef5350;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: system-ui, -apple-system, sans-serif;
background: var(--bg-color);
color: var(--text-color);
transition: background 0.3s, color 0.3s;
min-height: 100vh;
overflow-x: hidden;
}
/* Layout */
.columns {
display: flex;
flex-wrap: wrap;
gap: 1rem;
max-width: 1200px;
margin: 2rem auto;
padding: 0 1rem;
}
.column {
flex: 1 1 280px;
min-width: 250px;
background: var(--card-bg);
border-radius: 8px;
padding: 1rem;
position: relative;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.column.dragover { cursor: grab; }
.drop-indicator {
position: absolute;
left: 0; right: 0; top: 0; height: 3px;
background: var(--priority-high);
opacity: 0;
transition: opacity 0.1s;
z-index: 10;
}
.column.dragover .drop-indicator { opacity: 1; }
/* Header */
header {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 1rem;
}
header h1 { font-size: 1.5rem; }
.controls {
display: flex;
gap: 0.5rem;
align-items: baseline;
}
.controls input {
flex: 1;
padding: 0.4rem 0.6rem;
border: 1px solid var(--border-color);
border-radius: 4px;
}
.controls button {
padding: 0 0.8rem;
}
/* Column header */
.column h2 {
font-size: 1rem;
margin-bottom: 0.5rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.column h2 .count {
color: var(--priority-high);
font-weight: bold;
}
/* Priority badge */
.priority-badge {
display: inline-block;
padding: 2px 6px;
border-radius: 10px;
font-size: 0.65rem;
font-weight: bold;
margin-left: 0.4rem;
}
.priority-low { background: var(--priority-low); color: #fff; }
.priority-medium { background: var(--priority-medium); }
.priority-high { background: var(--priority-high); color: #fff; }
[data-theme="dark"] .priority-medium { color: #000; }
/* Cards */
.card {
position: relative;
padding: 0.8rem 1rem 0.8rem 1rem;
background: var(--card-bg);
border-radius: 4px;
margin-bottom: 0.6rem;
cursor: grab;
user-select: none;
transition: opacity 0.2s;
}
.card.dragging { opacity: 0.5; }
.card .title {
font-weight: bold;
font-size: 0.9rem;
margin-bottom: 0.2rem;
}
.priority-badge { position: absolute; top: 0.8rem; left: 0.8rem; }
.delete-btn {
position: absolute;
top: 0.8rem;
right: 0.8rem;
background: none;
border: none;
color: var(--text-color);
font-size: 1rem;
cursor: pointer;
}
.delete-btn:hover { color: var(--priority-high); }
/* Modal */
.modal {
display: none;
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
background: rgba(0,0,0,0.5);
backdrop-filter: blur(4px);
}
.modal-content {
background: var(--card-bg);
margin: 10% auto;
padding: 2rem;
border-radius: 8px;
width: 90%;
max-width: 400px;
}
.modal-content h2 { margin-bottom: 1rem; }
.modal-content form {
display: flex;
flex-direction: column;
gap: 0.8rem;
}
.modal-content input,
.modal-content select {
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 4px;
}
#close-modal {
align-self: flex-end;
}
/* Start message */
#start-message {
position: fixed;
top: 2rem;
left: 50%;
transform: translateX(-50%);
background: var(--priority-high);
color: #fff;
padding: 0.6rem 1rem;
border-radius: 20px;
font-size: 0.9rem;
z-index: 100;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
</style>
</head>
<body data-theme="light">
<div id="start-message">Click or press a key to start</div>
<header>
<h1>Kanban Board</h1>
<div class="controls">
<input type="text" id="search" placeholder="Search cards...">
<button id="theme-toggle" aria-pressed="false">🌙 Dark</button>
<button id="add-card-btn" aria-label="Add Card">➕ Add Card</button>
</div>
</header>
<div class="columns" id="columns">
<div class="column" id="to-do">
<h2>To Do <span class="count">3</span></h2>
<div class="drop-indicator"></div>
<div class="cards" id="cards-to-do"></div>
</div>
<div class="column" id="in-progress">
<h2>In Progress <span class="count">2</span></h2>
<div class="drop-indicator"></div>
<div class="cards" id="cards-in-progress"></div>
</div>
<div class="column" id="done">
<h2>Done <span class="count">1</span></h2>
<div class="drop-indicator"></div>
<div class="cards" id="cards-done"></div>
</div>
</div>
<!-- Modal -->
<div id="modal" class="modal">
<div class="modal-content">
<h2>Add Card</h2>
<form id="card-form">
<input type="text" id="card-title" required placeholder="Title">
<select id="card-priority">
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</select>
<button type="submit">Create</button>
<button type="button" id="close-modal">×</button>
</form>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
'use strict';
// Initial cards data
const cards = [
{ id: 1, title: 'Design mockup', priority: 'high', column: 'to-do' },
{ id: 2, title: 'Write API spec', priority: 'medium', column: 'to-do' },
{ id: 3, title: 'Setup repo', priority: 'low', column: 'to-do' },
{ id: 4, title: 'Implement login', priority: 'high', column: 'in-progress' },
{ id: 5, title: 'Fix bug #42', priority: 'medium', column: 'in-progress' },
{ id: 6, title: 'Refactor navbar', priority: 'low', column: 'done' }
];
// DOM elements
const startMessage = document.getElementById('start-message');
const columnsEl = document.getElementById('columns');
const columnElements = {
'to-do': document.getElementById('to-do'),
'in-progress': document.getElementById('in-progress'),
'done': document.getElementById('done')
};
const cardContainers = {
'to-do': document.getElementById('cards-to-do'),
'in-progress': document.getElementById('cards-in-progress'),
'done': document.getElementById('cards-done')
};
const columnHeaders = {
'to-do': document.querySelector('#to-do h2 .count'),
'in-progress': document.querySelector('#in-progress h2 .count'),
'done': document.querySelector('#done h2 .count')
};
const searchInput = document.getElementById('search');
const themeToggle = document.getElementById('theme-toggle');
const addCardBtn = document.getElementById('add-card-btn');
const cardForm = document.getElementById('card-form');
const modal = document.getElementById('modal');
const closeModal = document.getElementById('close-modal');
// Theme handling
function setTheme(theme) {
document.body.setAttribute('data-theme', theme);
themeToggle.setAttribute('aria-pressed', theme === 'dark');
themeToggle.textContent = theme === 'dark' ? '☀️ Light' : '🌙 Dark';
}
setTheme('light');
themeToggle.addEventListener('click', () => {
const newTheme = document.body.getAttribute('data-theme') === 'light' ? 'dark' : 'light';
setTheme(newTheme);
});
// Render cards
function renderCards(filter = '') {
// Clear all card containers
Object.values(cardContainers).forEach(c => c.innerHTML = '');
// Recalculate counts
const counts = { 'to-do': 0, 'in-progress': 0, 'done': 0 };
cards
.filter(card => card.title.toLowerCase().includes(filter.toLowerCase()))
.forEach(card => {
const cardEl = document.createElement('div');
cardEl.className = 'card';
cardEl.draggable = true;
cardEl.dataset.id = card.id;
cardEl.dataset.column = card.column;
// Priority badge
const badge = document.createElement('span');
badge.className = `priority-badge priority-${card.priority}`;
badge.textContent = card.priority.charAt(0).toUpperCase() + card.priority.slice(1);
cardEl.appendChild(badge);
// Title
const title = document.createElement('div');
title.className = 'title';
title.textContent = card.title;
cardEl.appendChild(title);
// Delete button
const delBtn = document.createElement('button');
delBtn.className = 'delete-btn';
delBtn.textContent = '×';
delBtn.addEventListener('click', (e) => {
e.stopPropagation();
deleteCard(card.id);
});
cardEl.appendChild(delBtn);
// Append to column container
const container = cardContainers[card.column];
container.appendChild(cardEl);
counts[card.column]++;
});
// Update column counts
Object.keys(counts).forEach(col => {
if (columnHeaders[col]) columnHeaders[col].textContent = counts[col];
});
// Initialize drag and drop after render
initDragAndDrop();
}
// Delete card
function deleteCard(id) {
const index = cards.findIndex(c => c.id === id);
if (index > -1) cards.splice(index, 1);
renderCards(searchInput.value);
}
// Modal handling
function openModal() { modal.style.display = 'block'; }
function closeModalFn() { modal.style.display = 'none'; cardForm.reset(); }
addCardBtn.addEventListener('click', openModal);
closeModal.addEventListener('click', closeModalFn);
modal.addEventListener('click', (e) => {
if (e.target === modal) closeModalFn();
});
cardForm.addEventListener('submit', (e) => {
e.preventDefault();
const title = document.getElementById('card-title').value.trim();
const priority = document.getElementById('card-priority').value;
if (!title) return;
const newCard = {
id: Date.now(),
title,
priority,
column: 'to-do'
};
cards.push(newCard);
closeModalFn();
renderCards(searchInput.value);
});
// Search filtering
searchInput.addEventListener('input', (e) => {
renderCards(e.target.value);
});
// Drag and drop
let draggedCard = null;
function initDragAndDrop() {
// Card drag start
document.querySelectorAll('.card:not(.dragging)').forEach(card => {
card.addEventListener('dragstart', (e) => {
draggedCard = card;
setTimeout(() => card.classList.add('dragging'), 0);
e.dataTransfer.setData('text/plain', card.dataset.id);
e.dataTransfer.effectAllowed = 'move';
});
card.addEventListener('dragend', () => {
card.classList.remove('dragging');
// Remove dragover from columns and hide indicators
Object.values(columnElements).forEach(col => col.classList.remove('dragover'));
document.querySelectorAll('.drop-indicator').forEach(ind => ind.style.opacity = '0');
});
});
// Column drag over / enter / leave / drop
Object.values(columnElements).forEach(col => {
col.addEventListener('dragover', (e) => {
e.preventDefault();
col.classList.add('dragover');
const indicator = col.querySelector('.drop-indicator');
if (indicator) indicator.style.opacity = '1';
});
col.addEventListener('dragleave', (e) => {
if (!col.contains(e.relatedTarget)) {
col.classList.remove('dragover');
const indicator = col.querySelector('.drop-indicator');
if (indicator) indicator.style.opacity = '0';
}
});
col.addEventListener('drop', (e) => {
e.preventDefault();
col.classList.remove('dragover');
const indicator = col.querySelector('.drop-indicator');
if (indicator) indicator.style.opacity = '0';
const cardId = e.dataTransfer.getData('text/plain');
if (!cardId) return;
const cardData = cards.find(c => c.id.toString() === cardId);
if (!cardData) return;
// Update column
cardData.column = col.id;
// Re-render to move card and update counts
renderCards(searchInput.value);
});
});
}
// Start message interaction
function hideStartMessage() {
startMessage.style.display = 'none';
}
window.addEventListener('mousedown', hideStartMessage);
window.addEventListener('keydown', hideStartMessage);
// Initial render
renderCards();
});
</script>
</body>
</html>Reasoning trace (18,737 tokens)
Here's a plan for the Kanban board:
1. **Structure**: Three columns (To Do, In Progress, Done) with card counts displayed.
2. **Pre-populate**: 6 realistic cards spread across columns.
3. **Card details**: Title + priority badge (low/medium/high colors).
4. **Add card**: Modal form with title required; delete button on each card.
5. **Drag & drop**: Between columns, with visual drop indicator.
6. **Search**: Live filter by title.
7. **Theme toggle**: Light/dark.
8. **State in memory only**.
Must output a single fenced code block with html, css, js inline. No external files, images, fonts, or network requests except allowed CDNs. Must work in 1280×800 viewport, adapt down to 375px, no horizontal scrollbar. First paint meaningful. Something visible within first seconds. Games must start in self-playing demo mode behind "Click or press a key to start" message — but this is not a game; it's a Kanban board. The requirement about games might not apply, but the "Click or press a key to start" message might be needed if keyboard usage. However, the note says: "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." Since the Kanban uses drag-and-drop with mouse, maybe keyboard isn't essential, but search could use keyboard. I'll include a startup message just in case, but perhaps not necessary. However, the requirement says "if the demo uses the keyboard, display a clear 'Click or press a key to start' message." Since there's a search field and maybe keyboard navigation, I'll include a message.
But the "games must start in a self-playing demo mode" doesn't apply. I'll skip that.
I need to ensure the code runs with zero errors in Chrome console.
Let's design the HTML structure:
- Container div.
- Header with title, theme toggle, search field.
- Three columns divs, each with header showing count.
- Cards inside each column.
- Modal backdrop + form.
CSS: Flexbox for columns, cards with drag state, drop indicator. Theme toggle switches colors.
JavaScript:
- Data structure for cards: each has id, title, priority, column.
- Functions to render cards, update counts.
- Drag and drop: setup drag start, over, drop, enter/leave for indicator.
- Modal: open on "Add Card" button, submit creates new card, close.
- Delete: remove card from array and re-render.
- Search: input event filters cards by title (case-insensitive).
- Theme toggle: change CSS variables or classes.
Need to be careful with drag and drop: use dragstart, dragenter, dragover, dragleave, drop. Prevent default. Use dataTransfer.setData.
Visual drop indicator: maybe a dashed rectangle or a line appearing between cards.
Let's code.
I'll use inline styles as much as possible, but CSS in <style> block.
Let's think about the viewport: 1280x800 down to 375px. Use responsive design: columns stack on narrow screens? Or keep horizontal scroll? The requirement says no horizontal scrollbar. So on narrow screens, columns should stack vertically. I'll use media query to switch from row to column layout.
Card count in column header.
Pre-populate 6 cards: I'll create 6 cards with titles and priorities distributed.
Let's assign:
Column "To Do": 3 cards
Column "In Progress": 2 cards
Column "Done": 1 card
Or maybe 2, 2, 2. I'll do 3, 2, 1.
Cards:
To Do:
- "Design mockup" (high)
- "Write API spec" (medium)
- "Setup repo" (low)
In Progress:
- "Implement login" (high)
- "Fix bug #42" (medium)
Done:
- "Refactor navbar" (low)
Now, drag and drop: when dragging a card, we need to identify which column it belongs to and where it's dropped. We'll use the column's drop zone.
Implementation plan:
- Each column has id: to-do, in-progress, done.
- Each card has draggable="true".
- On dragstart, set dataTransfer with card id and target column id.
- On dragover, prevent default.
- On drop, remove card from source column, append to target column, update state.
- Visual drop indicator: when dragging over a column, show a dashed box at the top or bottom, or between cards. I'll add a drop indicator div that becomes visible when dragging over a column, and position it absolute within the column.
Simplify: drop indicator appears as a horizontal dashed line across the column, and when dropped, the card inserts above or below? For simplicity, I'll just append to the end of the target column, or maybe insert at the pointer position. But for a simple demo, appending to the end is fine. However, requirement says "Drag and drop cards between columns with the mouse, with a clear visual drop indicator." So I need a visible indicator where the card will go. I'll implement a drop indicator that appears as a bar at the top of the column when dragging over it, and the card will be prepended to that column. Or maybe I can insert between cards.
Let's design: each column has a list of cards. When dragging over a column, a drop indicator (a thin rectangle) appears at the mouse y-position within the column. On drop, the card is inserted before the element under the mouse, or appended if at bottom. I'll track the clientY relative to column and insert accordingly.
But this might be complex. Alternative: always append to top or bottom, but show indicator of which column. I'll do: drop indicator is a full-width bar at the bottom of the column when dragging over it, card gets appended. That's simple and meets "clear visual drop indicator". I'll style a ::after pseudo or a div.
Let's do: when dragging over a column, a div.drop-indicator becomes visible with height ~4px, positioned at the top of the column's content area, or maybe follow mouse. I'll make it follow the mouse y within the column.
Simplify further: the drop indicator is a dashed outline that appears around the column when dragging over it. That's easy: on dragover, add class "dragover" to column, which gives a dashed border. On dragleave, remove. On drop, just move card. The dashed outline serves as drop indicator. I'll style .column.dragover { border: 2dashed #007acc; }.
But cards themselves also draggable; dragging a card might trigger drag events on underlying column. I need to handle dragenter/dragleave on column to add/remove dragover class.
Yes.
Now, modal form: "Add Card" button opens modal. Form has input for title, select for priority. On submit, create new card object, add to the first column (To Do), re-render, close modal.
Delete button on each card: removes from array and re-renders.
Search field: input event filters cards by title, hiding non-matching cards. But hiding cards might affect column counts? I'll filter by showing/hiding card elements, but keep them in DOM but with display none. Count might still show all? Better to filter and maybe hide, but count should reflect visible cards? Requirement: "A search field that filters cards live by title." I'll filter by title match, and hide non-matching cards. Column counts should maybe update to visible cards only, or keep total? I'll update counts to visible cards.
Theme toggle: light/dark. I'll use a data-theme attribute on body, and CSS variables for colors. Toggle button switches between "Light" and "Dark".
Now, the "Click or press a key to start" message: Since the demo uses mouse for drag-and-drop, and search via keyboard, I'll include a message at top: "Click or press a key to start". But it's not a game; maybe just show it initially and hide after click. The requirement: "if the demo uses the keyboard, display a clear 'Click or press a key to start' message." Since there is a search field, keyboard is used. I'll display it on load, and hide on first mousedown or keydown.
But the note: "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." So I need to show that message, and after user clicks, keyboard events will be received. I'll add a message div at top center, hide after click anywhere.
Now, putting it all together.
Let's write the code.
I'll structure:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Kanban Board</title>
<style>
/* CSS */
</style>
</head>
<body data-theme="light">
<!-- message -->
<div id="start-message">Click or press a key to start</div>
<!-- header -->
<header>
<h1>Kanban Board</h1>
<div class="controls">
<input type="text" id="search" placeholder="Search cards...">
<button id="theme-toggle" aria-pressed="false">🌙 Dark</button>
</div>
</header>
<!-- columns -->
<div class="columns" id="columns">
<div class="column" id="to-do">
<h2>To Do <span class="count">3</span></h2>
<div class="cards" id="cards-to-do"></div>
</div>
<div class="column" id="in-progress">
<h2>In Progress <span class="count">2</span></h2>
<div class="cards" id="cards-in-progress"></div>
</div>
<div class="column" id="done">
<h2>Done <span class="count">1</span></h2>
<div class="cards" id="cards-done"></div>
</div>
</div>
<!-- modal -->
<div id="modal" class="modal">
<div class="modal-content">
<h2>Add Card</h2>
<form id="card-form">
<input type="text" id="card-title" required placeholder="Title">
<select id="card-priority">
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</select>
<button type="submit">Create</button>
<button type="button" id="close-modal">×</button>
</form>
</div>
</div>
<script>
// JS
</script>
</body>
</html>
```
Now CSS.
I need responsive design. Columns layout: on wide screens, display inline-block or flex with gap. On narrow, columns stack.
Let's define CSS variables for themes: light and dark mode colors.
I'll use a simpler approach: toggle class 'dark' on body, and CSS rules for dark mode.
But requirement: "light / dark theme toggle." I'll use a button that toggles a data-theme attribute or class.
I'll use CSS variables:
:root { --bg-color: #f5f5f5; --card-bg: #fff; --text-color: #333; --priority-low: #4caf50; --priority-medium: #ffc107; --priority-high: #f44336; ... }
[data-theme="dark"] { --bg-color: #1e1e1e; --card-bg: #252526; --text-color: #e0e0e0; ... }
Now, columns: display: flex; flex-wrap: wrap; gap: 1rem; max-width: 1200px; margin: 0 auto;
Each column: flex: 1 1 300px; min-width: 250px; background: var(--card-bg); border-radius: 8px; padding: 1rem; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
Column header: display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 0.5rem; }
Count span: color: var(--priority-...); maybe.
Cards: margin-bottom: 0.8rem; padding: 0.8rem; background: #fff; border-radius: 4px; box-shadow: 0 1px 2px rgba(0,0,0,0.1); cursor: grab; user-select: none; }
Card title: font-weight: bold; margin-bottom: 0.3rem; }
Priority badge: display: inline-block; padding: 2px 6px; border-radius: 10px; font-size: 0.65rem; font-weight: bold; color: #fff; margin-left: 0.5rem; }
Low: #4caf50; Medium: #ffc107; High: #f44336.
Drag state: cursor: grabbing; during drag: cursor: grabbing.
Drop indicator: when column has class 'dragover', we can show a ::after pseudo element with dashed line. Or add a div.
I'll add a drop indicator div inside column, hidden by default, shown on dragover, positioned absolutely at top of cards area.
Simpler: use column's border color change as indicator. I'll style .column.dragover { border: 3px dashed var(--priority-high); border-radius: 8px; } But card may be dragged within column, border might be behind cards. I'll give column relative position, and cards absolute? No, cards are flow elements.
Better: add a drop indicator bar at the top of column content. I'll create a div.drop-indicator inside each column, height 4px, background var(--priority-high), opacity 0, transition. On dragover, set opacity 1 and top position based on mouse? That's complex.
Simplify: The dragover class changes the column's background color to a subtle highlight. That serves as indicator. I'll do that.
.column.dragover { background: #e3f2fd; } (light) or #1e3a5f (dark). That's clear.
I'll also prevent default on dragover and dragenter.
Now JS.
State: array of cards. Each card: { id, title, priority, column }. Column can be string 'to-do', 'in-progress', 'done'.
Initial cards:
[
{ id: 1, title: "Design mockup", priority: "high", column: "to-do" },
{ id: 2, title: "Write API spec", priority: "medium", column: "to-do" },
{ id: 3, title: "Setup repo", priority: "low", column: "to-do" },
{ id: 4, title: "Implement login", priority: "high", column: "in-progress" },
{ id: 5, title: "Fix bug #42", priority: "medium", column: "in-progress" },
{ id: 6, title: "Refactor navbar", priority: "low", column: "done" }
]
I'll map column strings to DOM ids: 'to-do' -> 'cards-to-do', etc.
Functions:
- renderCards(): clear all column card divs, loop over cards, if card.column matches, create card element, append. Also update column counts (visible cards only? I'll count cards that match search filter and are in column). I'll maintain a filtered list.
Better: keep all cards in array, and have a filter function that returns cards whose title includes search value (case-insensitive). Then render the filtered cards, but also assign them to columns? Actually, each card has a fixed column. Search should filter visibility. So I'll keep cards in their columns, and when searching, I'll toggle display none on card elements, and update counts to visible cards only.
I'll give each card element a data-id, and keep a mapping.
Simpler: on render, I'll generate card HTML strings for each column based on filtered cards. But cards have fixed column, so I'll just filter per column.
I'll do: render function iterates over columns, for each column, filters cards where card.column === columnId and title matches search, then creates elements.
Counts: number of visible cards in column.
Let's implement.
Data structure: global array `cards`.
Search input event: on input, get value, render.
Theme toggle: on click, toggle data-theme attribute, update button text.
Modal: on "Add Card" button click, open modal. Form submit: prevent default, get title and priority, create new card with random id, push to columns[0] (to-do), re-render, close modal.
Delete button on card: data-id, remove from cards array, re-render.
Drag and drop:
- Make each card draggable: set draggable="true" in HTML, or add attribute via JS.
- Add event listeners: dragstart, dragover, dragenter, dragleave, drop.
For dragstart: set dataTransfer.setData('text/plain', card.id); also maybe set dataTransfer.effectAllowed.
For column dragover: prevent default, add class 'dragoor' to column. On dragleave, remove class.
On drop: get card id from dataTransfer, find card object, remove from its current column's DOM and array, then append to target column's card div. Need to know target column id: drop target is the column div. We can get column id from event.target.id or closest.
But careful: drop target might be the cards div inside column. Use event.target.closest('.column').
Then after drop, re-render to update counts and UI.
Also, need to update column counts after each operation.
Let's code.
I'll write the JS as a script tag with strict mode.
Now, the start message: I'll add a div with id="start-message". On first click or keydown, hide it.
But note: "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." So I need to show the message initially, and after user clicks the iframe (or anywhere), hide it. I'll add a click listener on window to hide message on first mousedown. Also keydown listener to hide on first keydown.
But the message might cover content. I'll position it absolutely top center, with some padding, and fade out.
I'll style it.
Now, ensure no external dependencies. All inline.
Let's write the complete code.
Potential issues:
- Drag and drop may not work well on touch devices, but we assume mouse.
- Console errors: must ensure no undefined, etc.
- CSS must be valid.
- The viewport meta tag is needed.
Let's draft CSS.
```css
/* CSS reset */
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; background: var(--bg-color); color: var(--text-color); transition: background 0.3s, color 0.3s; }
a { color: inherit; text-decoration: none; }
button { cursor: pointer; }
/* Layout */
.columns { display: flex; flex-wrap: wrap; gap: 1rem; max-width: 1200px; margin: 2rem auto; padding: 0 1rem; }
.column { flex: 1 1 280px; min-width: 250px; background: var(--card-bg); border-radius: 8px; padding: 1rem; box-shadow: 0 2px 4px rgba(0,0,0,0.05); position: relative; }
.column.dragover { background: #e3f2fd; } /* light highlight */
.column.dragover.dark { background: #1e3a5f; } /* dark highlight, but we can just change variable */
/* Header */
header { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 1rem; }
header h1 { font-size: 1.5rem; }
.controls { display: flex; gap: 0.5rem; align-items: baseline; }
.controls input { flex: 1; padding: 0.4rem 0.6rem; border: 1px solid #ccc; border-radius: 4px; }
.controls button { padding: 0 0.8rem; }
/* Column header */
.column h2 { font-size: 1rem; margin-bottom: 0.5rem; display: flex; justify-content: space-between; align-items: center; }
.column h2 .count { color: var(--priority-high); font-weight: bold; }
/* Priority badge styles */
.priority-badge { display: inline-block; padding: 2px 6px; border-radius: 10px; font-size: 0.65rem; font-weight: background; color: #fff; margin-left: 0.4rem; }
.priority-low { background: #4caf50; }
.priority-medium { background: #ffc107; color: #000; } /* dark text for visibility */
.priority-high { background: #f44336; }
/* Cards */
.card { padding: 0.8rem 1rem; background: #fff; border-radius: 4px; margin-bottom: 0.6rem; position: relative; cursor: grab; user-select: none; }
.card.dragging { opacity: 0.5; }
.card .title { font-weight: bold; font-size: 0.9rem; }
.card .priority { position: absolute; top: 0.8rem; right: 0.8rem; }
/* Drop indicator */
.drop-indicator { position: absolute; left: 0; right: 0; height: 3px; background: var(--priority-high); opacity: 0; transition: opacity 0.1s; }
.column.dragover .drop-indicator { opacity: 1; }
/* Modal */
.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); backdrop-filter: blur(4px); }
.modal-content { background: var(--card-bg); margin: 10% auto; padding: 2rem; border-radius: 8px; width: 90%; max-width: 400px; }
.modal-content h2 { margin-bottom: 1rem; }
.modal-content form { display: flex; flex-direction: column; gap: 0.8rem; }
.modal-content input, .modal-content select { padding: 0.5rem; border: 1px solid #ddd; border-radius: 4px; }
#close-modal { align-self: flex-end; }
/* Start message */
#start-message { position: fixed; top: 2rem; left: 50%; transform: translateX(-50%); background: var(--priority-high); color: #fff; padding: 0.6rem 1rem; border-radius: 20px; font-size: 0.9rem; z-index: 100; }
```
Wait, the priority-high color is #f44336, but for start message background I'll use a fixed color or variable. I'll use a semi-transparent maybe. But I'll just use a fixed color.
Better: use a CSS variable for theme colors.
I'll restructure CSS variables at root.
```css
:root {
--bg-color: #f5f5f5;
--card-bg: #ffffff;
--text-color: #333333;
--priority-low: #4caf50;
--priority-medium: #ffc107;
--priority-high: #f44336;
--border-color: #ddd;
}
[data-theme="dark"] {
--bg-color: #1e1e1e;
--card-bg: #252526;
--text-color: #e0e0e0;
--priority-low: #66bb6a;
--priority-medium: #ffb300;
--priority-high: #ef5350;
--border-color: #444;
}
```
Then use these variables.
But the .column.dragoover background color should also theme-aware. I'll set it via a class that sets background color using a variable, or I'll just set a fixed color that contrasts. For simplicity, I'll set .column.dragover { background: var(--priority-high) !important; } but that would color the whole column. Maybe better to just change border color. I'll use box-shadow or border.
I'll change: .column.dragoer { border: 2px dashed var(--priority-high); } but need to ensure cards don't cover. Since column has padding and cards inside, a dashed border around column may be visible behind cards if cards have margin. I'll set column position relative, and cards absolute? Too complex. I'll just set a semi-transparent background on drag via JS class, but CSS can use :active? No.
Alternative: I'll not use dragover class for background change; instead, I'll add a drop indicator div that appears. I'll implement that.
Simplify: I'll make the drop indicator a simple element: when dragging over a column, a bar appears at the top of the column's content area, spanning the width, with a contrasting color. I'll position it absolutely within the column. On dragleave, hide it.
I'll do:
HTML inside each column: `<div class="drop-indicator"></div>` after the header or before cards.
CSS:
```css
.drop-indicator { position: absolute; left: 0; right: 0; top: 0; height: 3px; background: var(--priority-high); opacity: 0; transition: opacity 0.1s; z-index: 10; }
.column.dragover .drop-indicator { opacity: 1; }
```
But column needs position: relative for absolute child. I'll add that.
And cards need to be above the indicator or below? The indicator will be at top of column content, above cards. When dropped, card will be appended after the indicator? I'll place indicator after header, before cards container. Cards will be below it in flow. When indicator is visible, it sits above cards. On drop, the card will be appended to cards div, which is below the indicator. That's fine; the indicator just shows where the card will go relative to top. But maybe user expects indicator to be at mouse position. I'll keep it simple: indicator at top of column, shows that dropping will add to top? Hmm.
Maybe better: indicator follows mouse y within column. That's more intuitive but requires mousemove tracking. For a demo, I'll just show a full-width bar at the top when dragging over column, and the card will be prepended to the column. That indicates "drop here at top". I'll add a message "Drop here to add to top". But requirement just says "clear visual drop indicator". A top bar might suffice.
I'll do: drop indicator is a thin bar at the top of the column's content area, visible when dragging over column. Card will be inserted at the top of that column (prepend). That's clear.
Let's adjust: In column HTML: `<div class="column" id="to-do"><h2>...<span class="count">...</span></h2><div class="drop-indicator"></div><div class="cards" id="cards-to-do"></div></div>`
CSS: column { position: relative; } .drop-indicator { position: absolute; left: 0; right: 0; top: 0; height: 3px; background: var(--priority-high); opacity: 0; transition: opacity 0.1s; z-index: 5; } .column.dragover .drop-indicator { opacity: 1; }
Cards have margin-top maybe. I'll set cards padding-top or something.
When card is dropped, I'll prepend it to the cards div, or append? I'll prepend so it appears above the indicator next time. But indicator is at top, so prepending will put card behind indicator? Actually indicator is above cards in CSS? I'll set indicator height 3px, cards below it. If I prepend card to cards div, it will appear below the indicator (since indicator is outside cards div? Wait, structure: column > h2 + .drop-indicator + .cards. So drop-indicator is sibling of .cards, above it in markup. So cards are below indicator. If I prepend card to .cards, it will appear below indicator. That's fine; the indicator shows "drop here", and card appears below it. But user might expect card to go where indicator is. Since indicator is just 3px high, it's just a visual cue. I'll go with this.
On drop, I'll also remove the dragover class and hide indicator.
Now, drag events: need to handle dragstart on each card. I'll add draggable attribute in HTML or via JS. I'll set it in JS after DOM loaded.
I'll also need to stop default drag actions.
Let's write the JS.
```javascript
document.addEventListener('DOMContentLoaded', () => {
'use strict';
// Initial cards data
const cards = [
{ id: 1, title: 'Design mockup', priority: 'high', column: 'to-do' },
{ id: 2, title: 'Write API spec', priority: 'medium', column: 'to-do' },
{ id: 3, title: 'Setup repo', priority: 'low', column: 'to-do' },
{ id: 4, title: 'Implement login', priority: 'high', column: 'in-progress' },
{ id: 5, title: 'Fix bug #42', priority: 'medium', column: 'in-progress' },
{ id: 6, title: 'Refactor navbar', priority: 'low', column: 'done' }
];
// DOM elements
const columns = document.getElementById('columns');
const columnElements = {
'to-do': document.getElementById('to-do'),
'in-progress': document.getElementById('in-progress'),
'done': document.getElementById('done')
};
const cardContainers = {
'to-do': document.getElementById('cards-to-do'),
'in-progress': document.getElementById('cards-in-progress'),
'done': document.getElementById('cards-done')
};
const columnHeaders = {
'to-do': document.querySelector('#to-do h2 .count'),
'in-progress': document.querySelector('#in-progress h2 .count'),
'done': document.querySelector('#done h2 .count')
};
const searchInput = document.getElementById('search');
const themeToggle = document.getElementById('theme-toggle');
const startMessage = document.getElementById('start-message');
const modal = document.getElementById('modal');
const cardForm = document.getElementById('card-form');
const closeModal = document.getElementById('close-modal');
// Theme handling
function setTheme(theme) {
document.body.setAttribute('data-theme', theme);
themeToggle.setAttribute('aria-pressed', theme === 'dark');
themeToggle.textContent = theme === 'dark' ? '☀️ Light' : '🌙 Dark';
}
setTheme('light'); // default
themeToggle.addEventListener('click', () => {
const newTheme = document.body.getAttribute('data-theme') === 'light' ? 'dark' : 'light';
setTheme(newTheme);
});
// Render cards function
function renderCards(filter = '') {
// Clear all card containers
Object.values(cardContainers).forEach(c => c.innerHTML = '');
// Recalculate counts
const counts = { 'to-do': 0, 'in-progress': 0, 'done': 0 };
cards
.filter(card => card.title.toLowerCase().includes(filter.toLowerCase()))
.forEach(card => {
// Create card element
const cardEl = document.createElement('div');
cardEl.className = 'card';
cardEl.draggable = true;
cardEl.id = `card-${card.id}`;
cardEl.dataset.id = card.id;
cardEl.dataset.column = card.column;
// Priority badge
const badge = document.createElement('span');
badge.className = `priority-badge priority-${card.priority}`;
badge.textContent = card.priority.charAt(0).toUpperCase() + card.priority.slice(1);
cardEl.appendChild(badge);
// Title
const title = document.createElement('div');
title.className = 'title';
title.textContent = card.title;
cardEl.appendChild(title);
// Delete button
const delBtn = document.createElement('button');
delBtn.className = 'delete-btn';
delBtn.textContent = '×';
delBtn.title = 'Delete card';
delBtn.addEventListener('click', (e) => {
e.stopPropagation();
deleteCard(card.id);
});
cardEl.appendChild(delBtn);
// Append to column container
const container = cardContainers[card.column];
container.appendChild(cardEl);
counts[card.column]++;
});
// Update column counts
Object.keys(counts).forEach(col => {
if (columnHeaders[col]) columnHeaders[col].textContent = counts[col];
});
// Re-apply drag listeners after render
initDragAndDrop();
}
// Delete card
function deleteCard(id) {
const index = cards.findIndex(c => c.id === id);
if (index > -1) cards.splice(index, 1);
renderCards(searchInput.value);
}
// Delete button styling? I'll add CSS later.
// Modal handling
function openModal() {
modal.style.display = 'block';
}
function closeModalFn() {
modal.style.display = 'none';
cardForm.reset();
}
document.getElementById('add-card-btn').addEventListener('click', openModal);
closeModal.addEventListener('click', closeModalFn);
modal.addEventListener('click', (e) => {
if (e.target === modal) closeModalFn();
});
cardForm.addEventListener('submit', (e) => {
e.preventDefault();
const title = document.getElementById('card-title').value.trim();
const priority = document.getElementById('card-priority').value;
if (!title) return;
const newCard = {
id: Date.now(),
title,
priority,
column: 'to-do' // new cards go to To Do
};
cards.push(newCard);
closeModalFn();
renderCards(searchInput.value);
});
// Search filtering
searchInput.addEventListener('input', (e) => {
renderCards(e.target.value);
});
// Drag and drop initialization
let draggedCard = null;
function initDragAndDrop() {
// Card drag start
document.querySelectorAll('.card:not(.dragging)').forEach(card => {
card.addEventListener('dragstart', (e) => {
draggedCard = card;
setTimeout(() => card.classList.add('dragging'), 0);
e.dataTransfer.setData('text/plain', card.dataset.id);
e.dataTransfer.effectAllowed = 'move';
});
card.addEventListener('dragend', () => {
card.classList.remove('dragging');
// Remove dragover from columns
Object.values(columnElements).forEach(col => col.classList.remove('dragover'));
document.querySelectorAll('.drop-indicator').forEach(ind => ind.style.opacity = '0');
});
});
// Column drag over / enter / leave
Object.values(columnElements).forEach(col => {
col.addEventListener('dragover', (e) => {
e.preventDefault();
col.classList.add('dragover');
// Show drop indicator
const indicator = col.querySelector('.drop-indicator');
if (indicator) indicator.style.opacity = '1';
});
col.addEventListener('dragleave', (e) => {
// Check if leave is to outside or another child
if (!col.contains(e.relatedTarget)) {
col.classList.remove('dragover');
const indicator = col.querySelector('.drop-indicator');
if (indicator) indicator.style.opacity = '0';
}
});
col.addEventListener('drop', (e) => {
e.preventDefault();
col.classList.remove('dragover');
const indicator = col.querySelector('.drop-indicator');
if (indicator) indicator.style.opacity = '0';
const cardId = e.dataTransfer.getData('text/plain');
if (!cardId) return;
const cardData = cards.find(c => c.id.toString() === cardId);
if (!cardData) return;
// Remove card from current column
const currentCol = cardData.column;
const currentContainer = cardContainers[currentCol];
const cardEl = document.getElementById(`card-${cardId}`);
if (currentContainer && cardEl) {
currentContainer.removeChild(cardEl);
}
// Update column data
cardData.column = col.id; // col.id is 'to-do', etc.
// Append to new column
const newContainer = cardContainers[col.id];
newContainer.appendChild(cardEl);
// Re-render to update counts and UI
renderCards(searchInput.value);
});
});
}
// Start message click/key to hide
function hideStartMessage() {
startMessage.style.display = 'none';
}
window.addEventListener('click', hideStartMessage);
window.addEventListener('keydown', hideStartMessage);
// Initial render
renderCards();
});
```
Now CSS adjustments.
I need to ensure column elements have IDs: to-do, in-progress, done. In my HTML I have those IDs.
Also, column elements in JS: I selected by id, but I used `document.getElementById('to-do')` etc. That's fine.
But in the column drag listeners, I used `col.id` which will be the id string. That matches cardData.column which is 'to-do', etc.
Now, the drop indicator: I need to add .drop-indicator inside each column. In HTML, I'll add after h2, before cards div.
Let's adjust HTML inside column:
```html
<div class="column" id="to-do">
<h2>To Do <span class="count">3</span></h2>
<div class="drop-indicator"></div>
<div class="cards" id="cards-to-do"></div>
</div>
```
Same for others.
Now CSS for drop indicator:
```css
.drop-indicator {
position: absolute;
left: 0;
right: 0;
top: 0;
height: 3px;
background: var(--priority-high);
opacity: 0;
transition: opacity 0.1s;
z-index: 10;
}
.column.dragover .drop-indicator { opacity: 1; }
```
But column needs position: relative. I'll add that to .column.
Also, cards need to be below the indicator. Since indicator is absolute positioned at top:0, and cards are in flow below it (since indicator is sibling before cards in markup), cards will start below the 3px bar. That's fine.
But when we prepend card to cards container, it will appear below the indicator. Good.
Now, card CSS: I'll give cards position: relative, and maybe margin-top to account for indicator? Not needed; indicator is 3px, cards will have top margin maybe 0, they'll sit right below the 3px bar, which is fine.
Now, the column dragover class: I'll set background color change using a variable. But I need to theme-aware. I'll just set a fixed semi-transparent color, or use a class that adds a box-shadow. Simpler: I'll not change background; rely on drop indicator. But requirement: "with a clear visual drop indicator." The drop indicator is the 3px bar. That should be enough.
But also, when dragging over, the cursor might change. I'll set cursor: pointer or grab.
I'll add CSS: .column.dragover { cursor: grab; } or move.
Now, the start message: I'll style it as I planned.
Now, ensure no horizontal scroll. I'll set body overflow-x: hidden; max-width: 1280px; margin: 0 auto;.
Also, columns max-width: 1200px, okay.
Now, I need to include the CSS and JS in the fenced code block.
Let's assemble the full HTML.
I'll write the code carefully.
Potential issues:
- In the render function, I clear card containers and re-render. But drag event listeners are added via initDragAndDrop() which is called after render. However, I need to ensure that the card elements have the data attributes set correctly. In my render, I set cardEl.dataset.id = card.id; cardEl.dataset.column = card.column; That's good.
- The delete button: I add event listener inside render, but I also need to style it. I'll add CSS for .delete-btn.
- The modal: I have open on "Add Card" button. I need to add that button in HTML. I'll add a button after header or somewhere. I'll place it in header right side.
I'll add: `<button id="add-card-btn" aria-label="Add Card">➕ Add Card</button>` in header controls.
- The theme toggle button: I'll have it text change.
- The search field: placeholder "Search cards...".
- The start message: I'll hide on load? Requirement: "if the demo uses the keyboard, display a clear 'Click or press a key to start' message." So it should be visible on first paint. I'll set it visible, and hide after user interaction.
But note: "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." So the message should be there, and after user clicks the iframe (or anywhere), it hides. I'll add click and keydown listeners to hide it. But the message might be covered if user clicks elsewhere. I'll make it so that first mousedown or keydown hides it.
I'll set the message initially visible. After user clicks anywhere or presses key, it fades out. I'll just set display none.
Now, check for errors:
- In JS, `document.getElementById('add-card-btn')` but I haven't given that id in HTML yet. I'll add.
- `cardForm` id: I have `#card-form`. Good.
- `close-modal` id: I have `#close-modal`. Good.
- In the drag start, I use `card.dataset.id` which is set as `card.id` which is a number. I set `cardEl.dataset.id = card.id;` where card.id is number, but dataset expects string. It will be coerced to string. When retrieving with `e.dataTransfer.getData('text/plain')`, it will be string. In deleteCard, I compare `c.id.toString() === cardId`. That's fine.
- In initDragAndDrop, I queryAll `.card:not(.dragging)`. But initially no card has .dragging class, so fine.
- The dragend removes dragover from columns and hides indicators. Good.
- The drop event: I get cardId, find cardData, remove from current column's container, update column, append to new container. Then re-render. However, re-render will re-add delete listeners and reset drag states. That's fine.
But there's a potential issue: when I remove cardEl from currentContainer, but cardEl is still in DOM; then I append to newContainer. Then renderCards will clear all containers and re-create cards. That's okay; the card will be recreated with correct data. However, I must ensure that the card's new column is set correctly. I update cardData.column before render, so render will place it in correct column.
But there's a subtle bug: In drop event, I remove cardEl from current container, then append to new container. Then I call renderCards which will clear both containers and recreate all cards. So the manual removal/append is redundant; renderCards will handle it. But I need to update cardData.column before render. I'll just remove the manual DOM manipulation and rely on renderCards after updating data. However, I need to update the column header counts; renderCards does that. So I can simplify: in drop, just update cardData.column = col.id; then renderCards(searchInput.value). No need to touch DOM directly. But wait, the card element's dataset.column might need updating, but render recreates it. So yes, simple.
But I also need to ensure that the dragged card is the one being moved. Since renderCards filters and recreates all cards based on current data, it will work.
However, there's a catch: the drag start sets `draggedCard = card;` but I don't use it later. That's fine for visual.
I'll simplify drop:
```javascript
col.addEventListener('drop', (e) => {
e.preventDefault();
col.classList.remove('dragover');
const indicator = col.querySelector('.drop-indicator');
if (indicator) indicator.style.opacity = '0';
const cardId = e.dataTransfer.getData('text/plain');
if (!cardId) return;
const cardData = cards.find(c => c.id.toString() === cardId);
if (!cardData) return;
// Update column
cardData.column = col.id;
// Re-render
renderCards(searchInput.value);
});
```
That's cleaner.
But I need to make sure that the card's original column is removed from the data array; but since we just change the column property, and renderCards iterates over all cards and places them in cardContainers[card.column], it will automatically move the card to the new column. However, the card might appear duplicated if not removed from old column. But renderCards clears all cardContainers first, so old references are gone. Then it adds cards based on new column. So it's fine.
One issue: the card's id might be duplicated if we don't remove old, but since we clear containers, it's fine.
But what about the delete button on the card? It calls deleteCard(card.id) which removes from cards array and re-renders. That's fine.
Now, the initial cards: I have 6 cards. Counts: To Do should show 3, In Progress 2, Done 1. My initial array has 3 to-do, 2 in-progress, 1 done. Good.
Now, the column count spans: I have `<span class="count">3</span>` etc. In render, I update those spans with counts[column]. I need to make sure the spans exist and I select them correctly. In my JS, I have `columnHeaders` object that selects `.count` inside each column h2. But I need to ensure the HTML structure: `<h2>To Do <span class="count">3</span></h2>`. That's fine.
Now, CSS for .count: I'll style it.
Now, the priority badge: I'll create elements with classes `priority-badge priority-${priority}`. I'll style them.
Now, the delete button: I'll create a button with class `delete-btn`. I need to style it, maybe small '×'.
Now, the modal: I'll have a form with input required. On submit, create card.
Now, the theme toggle: I'll have button with id `theme-toggle`. I'll toggle data-theme.
Now, the start message: I'll have div with id `start-message`. I'll style it.
Now, ensure the iframe receives keyboard events after user clicks: I'll add the message and hide on first click/keydown. But the sandbox might not allow keyboard events until user clicks the iframe. The requirement: "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." So I should show the message, and after user clicks the iframe (or anywhere), the message hides, and keyboard events are received. I'll add a click listener on the document to hide the message, and also a keydown listener. But note: the message might be clicked itself; that's fine.
I'll do:
```javascript
startMessage.addEventListener('click', (e) => {
e.stopPropagation(); // don't hide when clicking message? Actually we want to hide on any click outside maybe. But requirement: "Click or press a key to start". So clicking the message should also hide it, or maybe the message is the prompt. I'll just hide on any mousedown outside, but simpler: hide on first mousedown or keydown anywhere. I'll just hide on any click or keydown, including on the message. That's fine.
});
```
But if I hide on any click, and the user clicks the add button, it will hide the message. That's okay.
I'll just do:
```javascript
window.addEventListener('mousedown', () => { startMessage.style.display = 'none'; });
window.addEventListener('keydown', () => { startMessage.style.display = 'none'; });
```
But I need to make sure the message is visible on first paint. I'll not hide it initially via CSS; I'll set it visible, and the event listeners will hide it on first interaction.
Now, let's write the full code.
I'll put CSS in <style> and JS in <script>.
I need to be careful about the viewport meta tag.
Now, let's construct the HTML string.
I'll write it step by step.
Potential CSS issues: The `.priority-medium` has `color: #000` for dark text on yellow background. That's fine for light theme. For dark theme, yellow #ffb300 might have black text, which is visible. I'll keep it simple.
Now, the card's delete button: I'll style it to be small, maybe position absolute top right.
Card CSS:
```css
.card { position: relative; padding: 0.8rem 1rem 0.8rem 1rem; background: var(--card-bg); border-radius: 4px; margin-bottom: 0.6rem; cursor: grab; user-select: none; transition: opacity 0.2s; }
.card.dragging { opacity: 0.5; }
.card .title { font-weight: bold; font-size: 0.9rem; margin-bottom: 0.2rem; }
.card .priority { position: absolute; top: 0.8rem; right: 0.8rem; padding: 2px 6px; border-radius: 10px; font-size: 0.65rem; font-weight: bold; color: #fff; }
.card .delete-btn { position: absolute; top: 0.8rem; right: 0.8rem; background: none; border: none; color: var(--text-color); font-size: 1rem; cursor: pointer; line-height: 1; }
.card .delete-btn:hover { color: var(--priority-high); }
```
But the priority badge already is at top right. I'll place delete button maybe after badge, or I can combine. I'll put delete button as a separate span/x. I'll use a button inside card, positioned absolute.
But the priority badge is also absolute. I can have both.
I'll structure card inner: title div, then a span for priority and a button for delete, both absolute top right. I'll give them some spacing.
Simplify: I'll put the delete button as a small '×' after the title, not absolute. But then it takes space. I'll use absolute.
Let's design card inner HTML pattern:
```html
<div class="card" draggable="true">
<span class="priority-badge priority-high">High</span>
<div class="title">Card Title</div>
<button class="delete-btn">×</button>
</div>
```
CSS:
```css
.card { position: relative; padding: 0.8rem 1rem 0.8rem 1rem; background: var(--card-bg); border-radius: 4px; margin-bottom: 0.6rem; cursor: grab; user-select: none; }
.card .title { font-weight: bold; font-size: 0.9rem; margin-bottom: 0.2rem; }
.card .priority-badge { position: absolute; top: 0.8rem; left: 0.8rem; padding: 2px 6px; border-radius: 10px; font-size: 0.65rem; font-weight: bold; color: #fff; }
.card .delete-btn { position: absolute; top: 0.8rem; right: 0.8rem; background: none; border: none; color: var(--text-color); font-size: 1rem; cursor: pointer; }
.card .delete-btn:hover { color: var(--priority-high); }
```
But wait, the card's data-column and dataset.id are set via JS, not HTML. I'll set them in JS after creating element. So the HTML skeleton can be minimal.
In render function:
```javascript
const cardEl = document.createElement('div');
cardEl.className = 'card';
cardEl.draggable = true;
cardEl.dataset.id = card.id;
cardEl.dataset.column = card.column;
// priority badge
const badge = document.createElement('span');
badge.className = `priority-badge priority-${card.priority}`;
badge.textContent = card.priority.charAt(0).toUpperCase() + card.priority.slice(1);
cardEl.appendChild(badge);
// title
const title = document.createElement('div');
title.className = 'title';
title.textContent = card.title;
cardEl.appendChild(title);
// delete button
const delBtn = document.createElement('button');
delBtn.className = 'delete-btn';
delBtn.textContent = '×';
delBtn.addEventListener('click', (e) => {
e.stopPropagation();
deleteCard(card.id);
});
cardEl.appendChild(delBtn);
```
That works.
Now, the column drop indicator: I'll add `<div class="drop-indicator"></div>` inside column, after h2.
Now, CSS for drop indicator:
```css
.drop-indicator {
position: absolute;
left: 0;
right: 0;
top: 0;
height: 3px;
background: var(--priority-high);
opacity: 0;
transition: opacity 0.1s;
z-index: 5;
}
.column.dragover .drop-indicator { opacity: 1; }
```
But column needs `position: relative;`. I'll add that.
Now, column header: I'll have `<h2>To Do <span class="count">3</span></h2>`. The count span I'll style.
Now, the start message: I'll add `<div id="start-message">Click or press a key to start</div>`. Style:
```css
#start-message {
position: fixed;
top: 2rem;
left: 50%;
transform: translateX(-50%);
background: var(--priority-high);
color: #fff;
padding: 0.6rem 1rem;
border-radius: 20px;
font-size: 0.9rem;
z-index: 100;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
```
But the priority-high color variable will be set based on theme. For light, #f44336; for dark, #ef5350. That's fine.
Now, the theme toggle button: I'll style it.
Now, the header: I'll have `<header>` with h1, search input, theme toggle, add card button.
Now, let's write the full HTML.
I'll ensure all IDs match.
Let's draft the HTML structure:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Kanban Board</title>
<style>
/* ... all CSS ... */
</style>
</head>
<body data-theme="light">
<div id="start-message">Click or press a key to start</div>
<header>
<h1>Kanban Board</h1>
<div class="controls">
<input type="text" id="search" placeholder="Search cards...">
<button id="theme-toggle" aria-pressed="false">🌙 Dark</button>
<button id="add-card-btn" aria-label="Add Card">➕ Add Card</button>
</div>
</header>
<div class="columns" id="columns">
<div class="column" id="to-do">
<h2>To Do <span class="count">3</span></h2>
<div class="drop-indicator"></div>
<div class="cards" id="cards-to-do"></div>
</div>
<div class="column" id="in-progress">
<h2>In Progress <span class="count">2</span></h2>
<div class="drop-indicator"></div>
<div class="cards" id="cards-in-progress"></div>
</div>
<div class="column" id="done">
<h2>Done <span class="count">1</span></h2>
<div class="drop-indicator"></div>
<div class="cards" id="cards-done"></div>
</div>
</div>
<!-- Modal -->
<div id="modal" class="modal">
<div class="modal-content">
<h2>Add Card</h2>
<form id="card-form">
<input type="text" id="card-title" required placeholder="Title">
<select id="card-priority">
<option value="low">Low</option>
<option value="medium">Medium</option>
<option value="high">High</option>
</select>
<button type="submit">Create</button>
<button type="button" id="close-modal">×</button>
</form>
</div>
</div>
<script>
// JS
</script>
</body>
</html>
```
Now, CSS.
I'll write CSS with variables.
```css
/* Root variables for themes */
:root {
--bg-color: #f5f5f5;
--card-bg: #ffffff;
--text-color: #333333;
--priority-low: #4caf50;
--priority-medium: #ffc107;
--priority-high: #f44336;
--border-color: #ddd;
}
/* Dark theme */
[data-theme="dark"] {
--bg-color: #1e1e1e;
--card-bg: #252526;
--text-color: #e0e0e0;
--priority-low: #66bb6a;
--priority-medium: #ffb300;
--priority-high: #ef5350;
}
/* Basic reset */
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, -apple-system, sans-serif; background: var(--bg-color); color: var(--text-color); transition: background 0.3s, color 0.3s; min-height: 100vh; overflow-x: hidden; }
/* Layout */
.columns { display: flex; flex-wrap: wrap; gap: 1rem; max-width: 1200px; margin: 2rem auto; padding: 0 1rem; }
.column { flex: 1 1 280px; min-width: 250px; background: var(--card-bg); border-radius: 8px; padding: 1rem; position: relative; box-shadow: 0 2px 4px rgba(0,0,0,0.05); }
.column.dragover { cursor: grab; } /* optional */
.drop-indicator {
position: absolute;
left: 0; right: 0; top: 0; height: 3px;
background: var(--priority-high);
opacity: 0;
transition: opacity 0.1s;
z-index: 10;
}
.column.dragover .drop-indicator { opacity: 1; }
/* Header */
header { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 1rem; }
header h1 { font-size: 1.5rem; }
.controls { display: flex; gap: 0.5rem; align-items: baseline; }
.controls input { flex: 1; padding: 0.4rem 0.6rem; border: 1px solid var(--border-color); border-radius: 4px; }
.controls button { padding: 0 0.8rem; }
/* Column header */
.column h2 { font-size: 1rem; margin-bottom: 0.5rem; display: flex; justify-content: space-between; align-items: center; }
.column h2 .count { color: var(--priority-high); font-weight: bold; }
/* Priority badge */
.priority-badge { display: inline-block; padding: 2px 6px; border-radius: 10px; font-size: 0.65rem; font-weight: bold; margin-left: 0.4rem; }
.priority-low { background: var(--priority-low); color: #fff; }
.priority-medium { background: var(--priority-medium); color: #000; } /* dark text for visibility */
.priority-high { background: var(--priority-high); color: #fff; }
/* Cards */
.card { position: relative; padding: 0.8rem 1rem 0.8rem 1rem; background: var(--card-bg); border-radius: 4px; margin-bottom: 0.6rem; cursor: grab; user-select: none; transition: opacity 0.2s; }
.card.dragging { opacity: 0.5; }
.card .title { font-weight: bold; font-size: 0.9rem; margin-bottom: 0.2rem; }
.card .priority-badge { position: absolute; top: 0.8rem; left: 0.8rem; padding: 2px 6px; border-radius: 10px; font-size: 0.65rem; font-weight: bold; color: #fff; }
.card .delete-btn { position: absolute; top: 0.8rem; right: 0.8rem; background: none; border: none; color: var(--text-color); font-size: 1rem; cursor: pointer; line-height: 1; }
.card .delete-btn:hover { color: var(--priority-high); }
/* Modal */
.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); backdrop-filter: blur(4px); }
.modal-content { background: var(--card-bg); margin: 10% auto; padding: 2rem; border-radius: 8px; width: 90%; max-width: 400px; }
.modal-content h2 { margin-bottom: 1rem; }
.modal-content form { display: flex; flex-direction: column; gap: 0.8rem; }
.modal-content input, .modal-content select { padding: 0.5rem; border: 1px solid #ccc; border-radius: 4px; }
#close-modal { align-self: flex-end; }
/* Start message */
#start-message {
position: fixed;
top: 2rem;
left: 50%;
transform: translateX(-50%);
background: var(--priority-high);
color: #fff;
padding: 0.6rem 1rem;
border-radius: 20px;
font-size: 0.9rem;
z-index: 100;
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
}
```
Now JS.
I'll write the script.
```javascript
document.addEventListener('DOMContentLoaded', () => {
'use strict';
// Initial cards data
const cards = [
{ id: 1, title: 'Design mockup', priority: 'high', column: 'to-do' },
{ id: 2, title: 'Write API spec', priority: 'medium', column: 'to-do' },
{ id: 3, title: 'Setup repo', priority: 'low', column: 'to-do' },
{ id: 4, title: 'Implement login', priority: 'high', column: 'in-progress' },
{ id: 5, title: 'Fix bug #42', priority: 'medium', column: 'in-progress' },
{ id: 6, title: 'Refactor navbar', priority: 'low', column: 'done' }
];
// DOM elements
const startMessage = document.getElementById('start-message');
const columnsEl = document.getElementById('columns');
const columnElements = {
'to-do': document.getElementById('to-do'),
'in-progress': document.getElementById('in-progress'),
'done': document.getElementById('done')
};
const cardContainers = {
'to-do': document.getElementById('cards-to-do'),
'in-progress': document.getElementById('cards-in-progress'),
'done': document.getElementById('cards-done')
};
const columnHeaders = {
'to-do': document.querySelector('#to-do h2 .count'),
'in-progress': document.querySelector('#in-progress h2 .count'),
'done': document.querySelector('#done h2 .count')
};
const searchInput = document.getElementById('search');
const themeToggle = document.getElementById('theme-toggle');
const addCardBtn = document.getElementById('add-card-btn');
const cardForm = document.getElementById('card-form');
const modal = document.getElementById('modal');
const closeModal = document.getElementById('close-modal');
// Theme handling
function setTheme(theme) {
document.body.setAttribute('data-theme', theme);
themeToggle.setAttribute('aria-pressed', theme === 'dark');
themeToggle.textContent = theme === 'dark' ? '☀️ Light' : '🌙 Dark';
}
setTheme('light');
themeToggle.addEventListener('click', () => {
const newTheme = document.body.getAttribute('data-theme') === 'light' ? 'dark' : 'light';
setTheme(newTheme);
});
// Render cards
function renderCards(filter = '') {
// Clear all card containers
Object.values(cardContainers).forEach(c => c.innerHTML = '');
// Recalculate counts
const counts = { 'to-do': 0, 'in-progress': 0, 'done': 0 };
cards
.filter(card => card.title.toLowerCase().includes(filter.toLowerCase()))
.forEach(card => {
const cardEl = document.createElement('div');
cardEl.className = 'card';
cardEl.draggable = true;
cardEl.dataset.id = card.id;
cardEl.dataset.column = card.column;
// Priority badge
const badge = document.createElement('span');
badge.className = `priority-badge priority-${card.priority}`;
badge.textContent = card.priority.charAt(0).toUpperCase() + card.priority.slice(1);
cardEl.appendChild(badge);
// Title
const title = document.createElement('div');
title.className = 'title';
title.textContent = card.title;
cardEl.appendChild(title);
// Delete button
const delBtn = document.createElement('button');
delBtn.className = 'delete-btn';
delBtn.textContent = '×';
delBtn.addEventListener('click', (e) => {
e.stopPropagation();
deleteCard(card.id);
});
cardEl.appendChild(delBtn);
// Append to column container
const container = cardContainers[card.column];
container.appendChild(cardEl);
counts[card.column]++;
});
// Update column counts
Object.keys(counts).forEach(col => {
if (columnHeaders[col]) columnHeaders[col].textContent = counts[col];
});
// Initialize drag and drop after render
initDragAndDrop();
}
// Delete card
function deleteCard(id) {
const index = cards.findIndex(c => c.id === id);
if (index > -1) cards.splice(index, 1);
renderCards(searchInput.value);
}
// Modal handling
function openModal() { modal.style.display = 'block'; }
function closeModalFn() { modal.style.display = 'none'; cardForm.reset(); }
addCardBtn.addEventListener('click', openModal);
closeModal.addEventListener('click', closeModalFn);
modal.addEventListener('click', (e) => {
if (e.target === modal) closeModalFn();
});
cardForm.addEventListener('submit', (e) => {
e.preventDefault();
const title = document.getElementById('card-title').value.trim();
const priority = document.getElementById('card-priority').value;
if (!title) return;
const newCard = {
id: Date.now(),
title,
priority,
column: 'to-do'
};
cards.push(newCard);
closeModalFn();
renderCards(searchInput.value);
});
// Search filtering
searchInput.addEventListener('input', (e) => {
renderCards(e.target.value);
});
// Drag and drop
let draggedCard = null;
function initDragAndDrop() {
// Card drag start
document.querySelectorAll('.card:not(.dragging)').forEach(card => {
card.addEventListener('dragstart', (e) => {
draggedCard = card;
setTimeout(() => card.classList.add('dragging'), 0);
e.dataTransfer.setData('text/plain', card.dataset.id);
e.dataTransfer.effectAllowed = 'move';
});
card.addEventListener('dragend', () => {
card.classList.remove('dragging');
// Remove dragover from columns and hide indicators
Object.values(columnElements).forEach(col => col.classList.remove('dragover'));
document.querySelectorAll('.drop-indicator').forEach(ind => ind.style.opacity = '0');
});
});
// Column drag over / enter / leave / drop
Object.values(columnElements).forEach(col => {
col.addEventListener('dragover', (e) => {
e.preventDefault();
col.classList.add('dragover');
// Show drop indicator (already handled by CSS, but ensure opacity)
const indicator = col.querySelector('.drop-indicator');
if (indicator) indicator.style.opacity = '1';
});
col.addEventListener('dragleave', (e) => {
if (!col.contains(e.relatedTarget)) {
col.classList.remove('dragover');
const indicator = col.querySelector('.drop-indicator');
if (indicator) indicator.style.opacity = '0';
}
});
col.addEventListener('drop', (e) => {
e.preventDefault();
col.classList.remove('dragover');
const indicator = col.querySelector('.drop-indicator');
if (indicator) indicator.style.opacity = '0';
const cardId = e.dataTransfer.getData('text/plain');
if (!cardId) return;
const cardData = cards.find(c => c.id.toString() === cardId);
if (!cardData) return;
// Update column
cardData.column = col.id;
// Re-render to move card and update counts
renderCards(searchInput.value);
});
});
}
// Start message interaction
function hideStartMessage() {
startMessage.style.display = 'none';
}
// Hide on first mouse click or key press
window.addEventListener('mousedown', hideStartMessage);
window.addEventListener('keydown', hideStartMessage);
// Initial render
renderCards();
});
```
Now, check for any errors.
- In CSS, I have `.column.dragover { cursor: grab; }` but maybe better to set cursor on cards. I'll leave.
- The `.drop-indicator` opacity set in JS might be redundant with CSS, but fine.
- In `initDragAndDrop`, I query `.card:not(.dragging)` but after render, cards don't have `.dragging` class, so all cards get listeners. Good.
- The `dragleave` check `if (!col.contains(e.relatedTarget))` might fire when dragging from one column to another; relatedTarget might be the new column, so it won't hide. That's correct.
- The `drop` event: I update `cardData.column = col.id;` then `renderCards`. But `renderCards` will clear all containers and re-add cards based on new column. However, the `cardData` object is the same reference in the `cards` array, so its `column` property changes. Good.
- But there's a potential issue: when `renderCards` is called, it filters cards by search value, then creates new card elements. The deleted card's old DOM elements are removed because containers are cleared. So it's fine.
- The `deleteCard` function removes from `cards` array and re-renders. Good.
- The modal: `openModal` sets display block. `closeModalFn` sets none and resets form. I also have `modal.addEventListener('click', (e) => { if (e.target === modal) closeModalFn(); })`. That closes modal when clicking outside the content. Good.
- The `add-card-btn` click opens modal. Good.
- The start message: `window.addEventListener('mousedown', hideStartMessage);` and `window.addEventListener('keydown', hideStartMessage);` will hide message on any mousedown or keydown. But note: the sandbox might not send keydown events until user clicks the iframe. The requirement: "The iframe only receives keyboard events after the user clicks it". So the keydown listener on window might not fire until user clicks iframe. That's fine; the message will stay until user clicks iframe, then hide. But also, the message might be covered by other elements; but it's fixed position. Should be okay.
- However, I need to ensure that the message is visible on first paint. I'll not hide it initially. The event listeners will be added after DOMContentLoaded, but the message is already in HTML. The listeners are added after load, but the message is visible until user interaction. That's fine.
- One thing: the `mousedown` listener on window will hide the message even if user clicks the add button or anywhere. That's intended.
- But there's a caveat: the `keydown` listener on window might fire when user presses a key inside the search input, which is fine; it will hide the message. But the requirement says keyboard events only received after user clicks the iframe. In the sandbox, the iframe might need a click to focus. The message hiding on keydown before click might be okay; it just hides the message. But if the user presses a key before clicking, the message hides, but keyboard events might not be received later? The requirement is just to display the message; after user clicks, it's hidden. I think it's fine.
- I should maybe only hide the message on click/keydown after the iframe is clicked, but I can't detect iframe click easily. I'll just hide on any mousedown/keydown, as it's simpler and likely acceptable.
- Alternatively, I could hide the message when the user clicks the anywhere inside the demo, but the note says "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." This suggests the message should be displayed, and after user interacts (click), the message goes away. I'll keep as is.
Now, check for any console errors potential:
- In CSS, I use `var(--priority-high)` etc. These are defined in `:root` and `[data-theme="dark"]`. Good.
- In JS, `document.querySelector('#to-do h2 .count')` might return null if the structure changes, but it's consistent.
- The `columnHeaders` object might have `null` values if elements not found, but they are found.
- In `renderCards`, I do `Object.values(cardContainers).forEach(c => c.innerHTML = '');` That's fine.
- The `cards` array initial length 6. Counts: to-do 3, in-progress 2, done 1. Good.
- The `priority-badge` classes: `priority-${card.priority}` will be `priority-low`, `priority-medium`, `priority-high`. CSS has those.
- The `deleteBtn` event stops propagation, good.
- The `dragstart` sets `e.dataTransfer.setData('text/plain', card.dataset.id);` where `card.dataset.id` is string of number. Good.
- The `dragend` removes `dragging` class and removes dragover from columns and hides indicators. Good.
- The `dragover` on column adds class and shows indicator opacity. Good.
- The `dragleave` checks `if (!col.contains(e.relatedTarget))`; if dragging from one column to another, relatedTarget might be the new column, so it won't remove dragoor. Good.
- The `drop` updates card column and re-renders. Good.
Now, test responsive: columns use flex-wrap: wrap, so on narrow screens, columns will stack vertically. No horizontal scroll because body has overflow-x: hidden, and columns max-width 1200 with margin auto, and gap. On very narrow, each column will take full width due to flex-basis 280px but flex-wrap will wrap, but if screen width less than 280px, columns might shrink below min-width 250px? I set min-width: 250px on column, but flex-basis 280px. On 375px width, maybe 1 column per row? 375px - gaps maybe fit one column with some space. It should adapt. I'll ensure columns have `min-width: 250px;` but if screen smaller, they might overflow. I'll set `column { flex: 1 1 250px; }` and remove min-width, or keep min-width and let flex shrink. I'll set `flex: 1 1 250px;` and remove min-width property. Actually, I had `flex: 1 1 280px; min-width: 250px;`. That's okay; on 375px, one column will be 280px, leaving some space, maybe two columns? 280*2 = 560 > 375, so only one column per row. That's fine; no horizontal scroll.
But I also have `max-width: 1200px;` on columns container, centered. Good.
Now, the header: search input with flex:1, button fixed width. On narrow, might overflow; but with flex-wrap? Header is not wrapped. I might need to make search input shrink. I'll set `flex: 1 1 200px;` or use `minmax`. I'll just set `flex: 1;` which will grow and shrink. Good.
Now, the theme toggle and add button: they have fixed padding, will shrink to fit.
Now, the start message: fixed top, centered. On narrow, might be below some content, but fine.
Now, check for any use of localStorage, etc. No.
No alert, confirm, prompt, window.open.
No external files.
Now, one thing: the CSS variable `--priority-medium` for light is `#ffc107`, and I set `.priority-medium { color: #000; }` for dark text. For dark theme, `--priority-medium` is `#ffb300`, and I didn't set color override, so it will inherit `var(--text-color)` which is `#e0e0e0`, which is light, might be visible on yellow? `#ffb300` on `#252526` background with `#e0e0e0` text might be okay but could be low contrast. I'll add a rule for dark theme: `[data-theme="dark"] .priority-medium { color: #000; }` or I can set color in the variable? Better to add specific CSS.
I'll add:
```css
[data-theme="dark"] .priority-medium { color: #000; }
[data-theme="dark"] .priority-low { color: #fff; } /* already set via class? Actually class sets background but color inherits? I set color: #fff in the class. That's for light. For dark, I need to override. I'll set in [data-theme="dark"] rules. */
```
But my `.priority-low` class has `color: #fff;` always. For dark theme, white on maybe darker yellow? `--priority-low` is `#66bb6a`, which is green, white text is fine. `.priority-high` class has `color: #fff;`; for dark theme, `--priority-high` is `#ef5350`, red, white text fine. `.priority-medium` yellow, need dark text for dark theme. I'll add:
```css
[data-theme="dark"] .priority-medium { color: #000; }
```
I'll include that in CSS.
Also, the start message background uses `var(--priority-high)`, which for dark is `#ef5350`, red, on white text? The message has `color: #fff;` so white text on red background. For dark theme, that's fine. For light, `#f44336` red, white text fine.
Now, the column dragover: I set `cursor: grab;` but maybe better to set `cursor: grabbing;` when dragging? Not necessary.
Now, I think it's ready.
Let's write the final fenced code block.
I need to output exactly one fenced code block with ```html (or ```svg). I'll output ```html.
Make sure no text before or after.
I'll combine all.
One more thing: The requirement: "Pre-populate the board with 6 realistic cards spread across the columns so the first screenshot looks alive." My initial cards are realistic.
"Each card has a title and a priority (low / medium / high) shown as a colored badge." Yes.
"Add a card through a small modal form (title required), and delete a card with a button on it." Yes.
"Drag and drop cards between columns with the mouse, with a clear visual drop indicator." Yes, the 3px bar.
"A search field that filters cards live by title." Yes.
"A light / dark theme toggle." Yes.
"State lives in memory only (no storage APIs)." Yes.
Now, I'll output the code.
But wait: The requirement also says: "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." This is for games, not Kanban. I think it's not applicable. The note about "if the demo uses the keyboard, display a clear 'Click or press a key to start' message" applies. I have that.
Also: "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." I have the message and hide on interaction.
Now, final code.
I'll put the HTML, CSS, JS as described.
Let's produce the final answer.