Models 44

Xing4.0 29B A4B (free)

by XingChenreasoning defaultClean
Open in new window

Running live in a sandboxed frame. Click inside it before using the keyboard.

How it went

Total time
2 min 23 s
Cost
$0
Output tokens
20,975
Spent thinking
13,410 tokens
Speed
149 tok/s
Console errors
0
First token after
2.4 s
First visible text after
1 min 45 s
Prompt tokens
520
Code size
20.2 KB
Finish reason
stop
Served by
Phala
Model id
xingchen-agi/xing4.0-29b-a4b: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 · 700 lines

<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Interactive Kanban Board</title>
<style>
  * { box-sizing: border-box; margin: 0; padding: 0; }

  :root {
    --bg: #f4f5f7;
    --column-bg: #ebecf0;
    --column-header: #dfe1e6;
    --card-bg: #ffffff;
    --text: #172b4d;
    --muted: #5e6c84;
    --border: #cfd8e3;
    --accent: #0052cc;
    --accent-soft: rgba(0, 82, 204, 0.10);
    --low: #5e6c84;
    --medium: #b58900;
    --high: #de350b;
    --shadow: 0 1px 2px rgba(9, 30, 66, 0.1), 0 0 1px rgba(9, 30, 66, 0.2);
    --hover: rgba(9, 30, 66, 0.04);
  }

  [data-theme="dark"] {
    --bg: #0f1117;
    --column-bg: #1c1f2b;
    --column-header: #232634;
    --card-bg: #2a2e3d;
    --text: #e6e8ee;
    --muted: #9aa3b2;
    --border: #3a3f52;
    --accent: #4d8dff;
    --accent-soft: rgba(77, 141, 255, 0.14);
    --low: #8b93a5;
    --medium: #d9a800;
    --high: #ff6b5e;
    --shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 0 1px rgba(0, 0, 0, 0.5);
    --hover: rgba(255, 255, 255, 0.06);
  }

  body {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
    background: var(--bg);
    color: var(--text);
    min-height: 100vh;
    padding: 20px;
    transition: background 0.2s, color 0.2s;
  }

  .start-banner {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
    margin-bottom: 16px;
    padding: 10px 16px;
    background: linear-gradient(90deg, rgba(0, 82, 204, 0.10), rgba(0, 82, 204, 0.02));
    border: 1px solid var(--accent);
    border-radius: 8px;
    font-size: 13px;
    font-weight: 600;
    color: var(--accent);
    animation: slideDown 0.35s ease;
    user-select: none;
  }
  .start-banner.hidden { display: none; }

  @keyframes slideDown {
    from { opacity: 0; transform: translateY(-10px); }
    to { opacity: 1; transform: translateY(0); }
  }

  .app-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    margin-bottom: 18px;
    flex-wrap: wrap;
  }

  .app-title { font-size: 22px; font-weight: 700; letter-spacing: -0.3px; }
  .app-title span { color: var(--accent); }

  .toolbar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }

  .search-box { position: relative; }
  .search-box input {
    width: 210px;
    padding: 8px 12px 8px 32px;
    border: 1px solid var(--border);
    border-radius: 6px;
    background: var(--card-bg);
    color: var(--text);
    font-size: 14px;
    outline: none;
    transition: border-color 0.15s, box-shadow 0.15s;
  }
  .search-box input:focus {
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--accent-soft);
  }
  .search-icon {
    position: absolute;
    left: 10px;
    top: 50%;
    transform: translateY(-50%);
    color: var(--muted);
    pointer-events: none;
    font-size: 16px;
  }

  .btn {
    padding: 8px 14px;
    border: none;
    border-radius: 6px;
    font-size: 14px;
    font-weight: 600;
    cursor: pointer;
    transition: background 0.15s, transform 0.05s;
    font-family: inherit;
  }
  .btn:active { transform: translateY(1px); }
  .btn-primary { background: var(--accent); color: #fff; }
  .btn-primary:hover { filter: brightness(1.08); }
  .btn-ghost {
    background: transparent;
    color: var(--text);
    border: 1px solid var(--border);
  }
  .btn-ghost:hover { background: var(--hover); }

  .btn-icon {
    width: 38px;
    height: 38px;
    padding: 0;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    border: 1px solid var(--border);
    background: var(--card-bg);
    color: var(--text);
    border-radius: 6px;
    cursor: pointer;
    transition: background 0.15s;
  }
  .btn-icon:hover { background: var(--hover); }
  .theme-toggle .icon { transition: opacity 0.2s, transform 0.3s; }
  #iconSun { display: none; }

  .board {
    display: grid;
    grid-template-columns: repeat(3, minmax(240px, 1fr));
    gap: 16px;
    align-items: start;
  }
  @media (max-width: 760px) { .board { grid-template-columns: 1fr; } }

  .column {
    background: var(--column-bg);
    border-radius: 8px;
    padding: 12px;
    min-height: 200px;
    display: flex;
    flex-direction: column;
    transition: background 0.15s, outline-color 0.1s;
    outline: 2px dashed transparent;
    outline-offset: -2px;
  }
  .column.drop-active {
    background: var(--accent-soft);
    outline-color: var(--accent);
    box-shadow: 0 0 0 2px var(--accent);
  }

  .column-header {
    display: flex;
    align-items: center;
    justify-content: space-between;
    margin-bottom: 12px;
    padding: 0 4px;
  }
  .column-title {
    font-size: 12px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.6px;
    color: var(--muted);
  }
  .column-count {
    background: var(--column-header);
    border-radius: 10px;
    padding: 2px 8px;
    font-size: 12px;
    font-weight: 700;
    color: var(--muted);
    min-width: 22px;
    text-align: center;
  }

  .cards-list {
    display: flex;
    flex-direction: column;
    gap: 8px;
    min-height: 40px;
    flex: 1;
  }

  .card {
    background: var(--card-bg);
    border-radius: 6px;
    padding: 10px 12px;
    box-shadow: var(--shadow);
    cursor: grab;
    border-left: 3px solid transparent;
    transition: box-shadow 0.15s, transform 0.05s;
    animation: cardFade 0.2s ease;
  }
  @keyframes cardFade {
    from { opacity: 0; transform: translateY(4px); }
    to { opacity: 1; transform: translateY(0); }
  }
  .card:hover {
    box-shadow: 0 2px 8px rgba(9, 30, 66, 0.18), 0 0 1px rgba(9, 30, 66, 0.35);
  }
  .card:hover { border-left-color: var(--accent); }
  .card.dragging { opacity: 0.4; cursor: grabbing; }

  .card-title {
    font-size: 14px;
    font-weight: 500;
    margin-bottom: 8px;
    line-height: 1.35;
    word-break: break-word;
  }
  .card-meta {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
  }
  .priority {
    font-size: 11px;
    font-weight: 700;
    padding: 2px 8px;
    border-radius: 10px;
    text-transform: uppercase;
    letter-spacing: 0.4px;
    white-space: nowrap;
  }
  .priority.low { background: rgba(94, 108, 132, 0.15); color: var(--low); }
  .priority.medium { background: rgba(181, 137, 0, 0.15); color: var(--medium); }
  .priority.high { background: rgba(222, 53, 11, 0.15); color: var(--high); }

  .card-delete {
    background: none;
    border: none;
    color: var(--muted);
    font-size: 17px;
    padding: 2px 7px;
    border-radius: 4px;
    cursor: pointer;
    opacity: 0;
    transition: opacity 0.15s, color 0.15s, background 0.15s;
    font-family: inherit;
    line-height: 1;
  }
  .card:hover .card-delete { opacity: 1; }
  .card-delete:hover {
    background: rgba(220, 53, 69, 0.12);
    color: var(--high);
  }

  .empty-hint {
    color: var(--muted);
    font-size: 12px;
    font-style: italic;
    padding: 10px 4px;
    text-align: center;
  }

  .modal-backdrop {
    position: fixed;
    inset: 0;
    background: rgba(9, 30, 66, 0.5);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 200;
    animation: fadeIn 0.2s ease;
  }
  .modal-backdrop.hidden { display: none; }
  @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }

  .modal {
    background: var(--card-bg);
    border-radius: 10px;
    padding: 20px;
    width: min(400px, 92vw);
    box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
    animation: popIn 0.2s ease;
  }
  @keyframes popIn {
    from { opacity: 0; transform: scale(0.95) translateY(8px); }
    to { opacity: 1; transform: scale(1) translateY(0); }
  }
  .modal h3 { margin-bottom: 16px; font-size: 17px; }
  .form-group { margin-bottom: 14px; }
  .form-group label {
    display: block;
    font-size: 13px;
    font-weight: 600;
    margin-bottom: 6px;
    color: var(--muted);
  }
  .form-group input, .form-group select {
    width: 100%;
    padding: 9px 11px;
    border: 1px solid var(--border);
    border-radius: 6px;
    background: var(--bg);
    color: var(--text);
    font-size: 14px;
    outline: none;
    font-family: inherit;
  }
  .form-group input:focus, .form-group select:focus {
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--accent-soft);
  }
  .modal-actions {
    display: flex;
    justify-content: flex-end;
    gap: 10px;
    margin-top: 18px;
  }
  .btn-danger {
    background: var(--high);
    color: #fff;
  }
  .btn-danger:hover { background: #bf2600; }

  .toast {
    position: fixed;
    bottom: 22px;
    left: 50%;
    transform: translateX(-50%) translateY(20px);
    background: var(--text);
    color: var(--card-bg);
    padding: 10px 18px;
    border-radius: 6px;
    font-size: 13px;
    font-weight: 600;
    opacity: 0;
    transition: opacity 0.25s, transform 0.25s;
    z-index: 300;
    pointer-events: none;
    max-width: 80vw;
    text-align: center;
  }
  .toast.show {
    opacity: 1;
    transform: translateX(-50%) translateY(0);
  }
</style>
</head>
<body>
  <div class="start-banner" id="startBanner">Click or press a key to start the demo</div>

  <header class="app-header">
    <h1 class="app-title">Interactive <span>Kanban</span></h1>
    <div class="toolbar">
      <div class="search-box">
        <span class="search-icon">&#8593;</span>
        <input type="text" id="searchInput" placeholder="Filter cards by title..." autocomplete="off">
      </div>
      <button class="btn btn-primary" id="addBtn">+ New card</button>
      <button class="btn-icon theme-toggle" id="themeToggle" title="Toggle light / dark theme" aria-label="Toggle theme">
        <svg class="icon" id="iconSun" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
        <svg class="icon" id="iconMoon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
      </button>
    </div>
  </header>

  <main class="board" id="board"></main>

  <div class="modal-backdrop hidden" id="modalBackdrop">
    <div class="modal">
      <h3>Add new card</h3>
      <div class="form-group">
        <label for="cardTitleInput">Title *</label>
        <input type="text" id="cardTitleInput" placeholder="What needs doing?" maxlength="80" autocomplete="off">
      </div>
      <div class="form-group">
        <label for="cardPrioritySelect">Priority</label>
        <select id="cardPrioritySelect">
          <option value="low">Low</option>
          <option value="medium" selected>Medium</option>
          <option value="high">High</option>
        </select>
      </div>
      <div class="modal-actions">
        <button class="btn btn-ghost" id="cancelBtn">Cancel</button>
        <button class="btn btn-primary" id="submitBtn">Add card</button>
      </div>
    </div>
  </div>

  <div class="toast" id="toast"></div>

<script>
(function () {
  'use strict';

  var COLUMNS = ['todo', 'inProgress', 'done'];
  var COLUMN_LABELS = { todo: 'To do', inProgress: 'In progress', done: 'Done' };

  var nextId = 1;
  var state = {
    columns: {
      todo: [
        { id: nextId++, title: 'Design landing page mockup', priority: 'high' },
        { id: nextId++, title: 'Set up CI pipeline', priority: 'medium' },
        { id: nextId++, title: 'Write API documentation', priority: 'low' }
      ],
      inProgress: [
        { id: nextId++, title: 'Implement authentication module', priority: 'high' },
        { id: nextId++, title: 'Refactor database schema', priority: 'medium' }
      ],
      done: [
        { id: nextId++, title: 'Configure development environment', priority: 'low' },
        { id: nextId++, title: 'Draft project README', priority: 'low' }
      ]
    },
    theme: 'light',
    started: false
  };

  var DEMO_INTERVAL = 2200;

  var boardEl = document.getElementById('board');
  var searchInput = document.getElementById('searchInput');
  var addBtn = document.getElementById('addBtn');
  var themeToggle = document.getElementById('themeToggle');
  var startBanner = document.getElementById('startBanner');
  var modalBackdrop = document.getElementById('modalBackdrop');
  var cardTitleInput = document.getElementById('cardTitleInput');
  var cardPrioritySelect = document.getElementById('cardPrioritySelect');
  var submitBtn = document.getElementById('submitBtn');
  var cancelBtn = document.getElementById('cancelBtn');
  var toastEl = document.getElementById('toast');

  function applyTheme() {
    document.documentElement.setAttribute('data-theme', state.theme);
    document.getElementById('iconSun').style.display = state.theme === 'dark' ? 'block' : 'none';
    document.getElementById('iconMoon').style.display = state.theme === 'dark' ? 'none' : 'block';
  }

  themeToggle.addEventListener('click', function () {
    state.theme = state.theme === 'light' ? 'dark' : 'light';
    applyTheme();
  });

  function escapeHtml(str) {
    return str.replace(/[&<>"']/g, function (c) {
      return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c];
    });
  }

  function getFiltered() {
    var q = searchInput.value.trim().toLowerCase();
    if (!q) return state.columns;
    var out = {};
    COLUMNS.forEach(function (col) {
      out[col] = state.columns[col].filter(function (c) {
        return c.title.toLowerCase().indexOf(q) !== -1;
      });
    });
    return out;
  }

  function cardTemplate(card) {
    return '<article class="card" draggable="true" data-id="' + card.id + '">' +
      '<div class="card-title">' + escapeHtml(card.title) + '</div>' +
      '<div class="card-meta">' +
      '<span class="priority ' + card.priority + '">' + card.priority + '</span>' +
      '<button class="card-delete" data-action="delete" data-id="' + card.id + '" title="Delete card" aria-label="Delete card">&times;</button>' +
      '</div></article>';
  }

  function render() {
    boardEl.innerHTML = '';
    var filtered = getFiltered();

    COLUMNS.forEach(function (col) {
      var colEl = document.createElement('section');
      colEl.className = 'column';
      colEl.dataset.column = col;

      var headerEl = document.createElement('div');
      headerEl.className = 'column-header';

      var titleSpan = document.createElement('span');
      titleSpan.className = 'column-title';
      titleSpan.textContent = COLUMN_LABELS[col];

      var countSpan = document.createElement('span');
      countSpan.className = 'column-count';
      countSpan.textContent = (filtered[col] || []).length;

      headerEl.append(titleSpan, countSpan);

      var listEl = document.createElement('div');
      listEl.className = 'cards-list';

      var items = filtered[col] || [];
      if (items.length === 0 && state.started) {
        var hint = document.createElement('div');
        hint.className = 'empty-hint';
        hint.textContent = 'No cards here';
        listEl.appendChild(hint);
      }
      items.forEach(function (c) {
        listEl.insertAdjacentHTML('beforeend', cardTemplate(c));
      });

      colEl.append(headerEl, listEl);
      boardEl.appendChild(colEl);
    });
  }

  var dragData = null;

  boardEl.addEventListener('dragstart', function (e) {
    if (!state.started) { e.preventDefault(); return; }
    var card = e.target.closest('.card');
    if (!card) return;
    dragData = { id: Number(card.dataset.id), from: card.closest('.column').dataset.column };
    card.classList.add('dragging');
    e.dataTransfer.effectAllowed = 'move';
    try { e.dataTransfer.setData('text/plain', String(dragData.id)); } catch (err) {}
  });

  boardEl.addEventListener('dragend', function () {
    document.querySelectorAll('.column.drop-active').forEach(function (el) {
      el.classList.remove('drop-active');
    });
    var card = boardEl.querySelector('.card.dragging');
    if (card) card.classList.remove('dragging');
    dragData = null;
  });

  boardEl.addEventListener('dragover', function (e) {
    if (!state.started || !dragData) return;
    e.preventDefault();
    var col = e.target.closest('.column');
    if (!col) return;
    document.querySelectorAll('.column.drop-active').forEach(function (el) {
      el.classList.remove('drop-active');
    });
    col.classList.add('drop-active');
  });

  boardEl.addEventListener('dragleave', function (e) {
    if (!e.relatedTarget || !boardEl.contains(e.relatedTarget)) {
      document.querySelectorAll('.column.drop-active').forEach(function (el) {
        el.classList.remove('drop-active');
      });
    }
  });

  boardEl.addEventListener('drop', function (e) {
    if (!state.started || !dragData) return;
    e.preventDefault();
    var col = e.target.closest('.column');
    if (!col || !dragData) { dragData = null; return; }
    var targetCol = col.dataset.column;
    if (targetCol === dragData.from) { dragData = null; render(); return; }
    moveCard(dragData.id, targetCol);
    dragData = null;
    render();
  });

  function moveCard(id, targetCol) {
    for (var i = 0; i < COLUMNS.length; i++) {
      var col = COLUMNS[i];
      var idx = state.columns[col].findIndex(function (c) { return c.id === id; });
      if (idx >= 0) {
        var card = state.columns[col].splice(idx, 1)[0];
        state.columns[targetCol].push(card);
        return;
      }
    }
  }

  searchInput.addEventListener('input', function () {
    if (!state.started) return;
    render();
  });

  function openModal() {
    modalBackdrop.classList.remove('hidden');
    cardTitleInput.value = '';
    cardPrioritySelect.value = 'medium';
    setTimeout(function () { cardTitleInput.focus(); }, 60);
  }

  function closeModal() {
    modalBackdrop.classList.add('hidden');
  }

  addBtn.addEventListener('click', openModal);
  cancelBtn.addEventListener('click', closeModal);
  modalBackdrop.addEventListener('click', function (e) {
    if (e.target === modalBackdrop) closeModal();
  });

  submitBtn.addEventListener('click', function () {
    var title = cardTitleInput.value.trim();
    if (!title) {
      showToast('Title is required');
      cardTitleInput.focus();
      return;
    }
    state.columns.todo.push({ id: nextId++, title: title, priority: cardPrioritySelect.value });
    closeModal();
    if (state.started) render();
    else showToast('Click or press a key to start the demo');
  });

  cardTitleInput.addEventListener('keydown', function (e) {
    if (e.key === 'Enter') submitBtn.click();
  });

  boardEl.addEventListener('click', function (e) {
    var delBtn = e.target.closest('.card-delete');
    if (delBtn) {
      e.stopPropagation();
      deleteCard(Number(delBtn.dataset.id));
      return;
    }
    if (!state.started) startDemo();
  });

  function deleteCard(id) {
    for (var i = 0; i < COLUMNS.length; i++) {
      var col = COLUMNS[i];
      var idx = state.columns[col].findIndex(function (c) { return c.id === id; });
      if (idx >= 0) {
        state.columns[col].splice(idx, 1);
        break;
      }
    }
    if (state.started) render();
    else showToast('Click or press a key to start the demo');
  }

  var demoTimer = null;
  function startDemo() {
    if (state.started) return;
    state.started = true;
    startBanner.classList.add('hidden');
    render();

    demoTimer = setInterval(function () {
      var sources = ['todo', 'inProgress'];
      var sourceCol = sources[Math.floor(Math.random() * sources.length)];
      var arr = state.columns[sourceCol];
      if (arr.length === 0) return;
      var card = arr[Math.floor(Math.random() * arr.length)];
      var dest;
      if (Math.random() < 0.7) dest = 'done';
      else dest = sources[(sources.indexOf(sourceCol) + 1) % 2];
      moveCard(card.id, dest);
      render();
    }, DEMO_INTERVAL);
  }

  document.addEventListener('keydown', function () {
    startDemo();
  });

  function showToast(msg) {
    toastEl.textContent = msg;
    toastEl.classList.add('show');
    clearTimeout(showToast._timer);
    showToast._timer = setTimeout(function () {
      toastEl.classList.remove('show');
    }, 2200);
  }

  applyTheme();
  render();
})();
</script>
</body>
</html>
Reasoning trace (13,410 tokens)
Let me build an interactive Kanban board with all the requirements. I need to:

1. Three columns with card counts
2. 6 pre-populated cards across columns
3. Each card has title + priority badge (low/medium/high)
4. Add card via modal (title required)
5. Delete card via button on card
6. Drag and drop with visual drop indicator
7. Search field that filters by title live
8. Light/dark theme toggle
9. State in memory only

Let me write clean, vanilla HTML/CSS/JS. The drop indicator should be clear - I'll show a highlighted border/line when dragging over a column.

Let me make this visually appealing and ensure it works without any external dependencies.

Key considerations for the sandbox:
- No localStorage etc
- No alerts/confirm
- Works in 1280x800, adapts down
- Visible in first seconds
- Demo mode auto-plays behind a "Click or press a key to start" message
- Only keyboard events after user clicks iframe
- Zero console errors in latest Chrome

Since the requirement says "The demo must not use storage APIs (localStorage, sessionStorage, cookies, IndexedDB): state lives in-memory only" - I'll use a JS object for state.

For drag and drop, I'll implement native HTML5 drag and drop or mouse-based drag. Let me use HTML5 drag and drop API which is simpler. Actually, for better visual feedback and control, let me implement custom mouse-based dragging so I can have a clear drop indicator.

Actually, HTML5 drag and drop is fine and simpler. Let me use it with good visual feedback.

For the "self-playing demo mode" - I need to add a simple AI that plays behind a "Click or press a key to start" message. But also keep the manual add/delete/search functionality working.

Hmm, this is a bit tricky. Let me think about this. The requirement 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 when you click the board or press a key, the demo starts playing automatically. The search field filtering and other interactions should still work. Let me add a simple demo AI that moves cards around automatically.

Let me design this carefully:

1. Initial state: Show a "Click or press a key to start" overlay/message. Cards are visible behind but maybe slightly dimmed.

2. When user clicks or presses a key:
   - Start a timer that auto-moves cards between columns every ~3 seconds (randomly picks a card and moves it to another column)
   - The search/filter, add/delete still work

3. The drag and drop should work normally too.

Let me implement this. I'll make the demo play: every few seconds, pick a random card from any column and move it to a different column (preferably toward "done" then back). This simulates activity.

Let me code this up:

```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Kanban Board</title>
<style>
/* CSS variables for theming */
:root {
  --bg: #f4f5f7;
  --column-bg: #ebecf0;
  --card-bg: #ffffff;
  --text: #172b4d;
  --muted: #6b778c;
  --border: #d0d4dc;
  --accent: #0052cc;
  --low: #65778c;
  --medium: #ffab00;
  --high: #de350b;
  --drop-indicator: rgba(0, 82, 204, 0.15);
}
[data-theme="dark"] {
  --bg: #1e1e2e;
  --column-bg: #2a2a3a;
  --card-bg: #33334a;
  --text: #e0e0e0;
  --muted: #9ca3af;
  --border: #44446a;
  --accent: #3b82f6;
  --low: #6b7280;
  --medium: #f59e0b;
  --high: #ef4444;
  --drop-indicator: rgba(59, 130, 246, 0.2);
}
...
</style>
```

OK let me just write the full thing now. I'll be thorough.

Let me think about the layout:
- Header bar with title, search box, theme toggle, add button
- Board area with three columns
- Each column header shows "To do", count, and column bg
- Cards draggable
- Modal overlay for adding cards

For drag and drop, I'll use the HTML5 DnD API. When dragging starts, I track which card. When over a column, I show the drop indicator. On drop, move the card.

Actually, for the drop indicator, I want a clear visual. Let me add a class to the column being hovered during drag.

Let me also make sure clicking the add button or pressing a key starts the demo.

Here's my plan for the JS:

```js
const state = {
  columns: {
    todo: [{id:1, title:'...', priority:'medium'}, ...],
    inprogress: [...],
    done: [...]
  },
  theme: 'light',
  demoPlaying: false,
  demoTimer: null,
};

// Render function rebuilds the DOM from state
function render() {...}

// Add card modal
function openAddModal() {...}
function closeAddModal() {...}
function handleAddSubmit() {...}

// Delete card
function deleteCard(id) {...}

// Drag and drop
let draggedCard = null;
function onDragStart(e) {...}
function onDragOver(e) {...} // preventDefault + highlight column
function onDragLeave(e) {...}
function onDrop(e) {...}

// Search filter
const searchQuery = '';
function applySearch() {...} // re-render with filtered cards

// Theme toggle
function toggleTheme() {...}

// Demo mode
function startDemo() {...}
function stopDemo() {...}

// Init
render();
showStartOverlay();
document.addEventListener('keydown', ...)
boardElement.addEventListener('click', ...)
```

Wait, the requirement: "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."

So the demo should auto-start when you click/press key. And it should play behind the message - meaning the message stays until you click, but while waiting, nothing dramatic happens? Or cards move slowly so you see movement but maybe not obvious at first?

I think: show the overlay. When click/key pressed, hide overlay and start the demo timer. Cards start moving every ~2.5-3 seconds to simulate activity.

But the search field is visible from the start, and add/delete buttons are on cards. Hmm. Actually, maybe the cards shouldn't move until you start. The cards are visible though.

Let me reconsider. Maybe:
- On load: show the board with cards, dimmed slightly, with a "Click or press a key to start" banner.
- After click/key: banner disappears, demo starts (cards shuffle around), interactions enabled.

Actually that works well. The screenshot is taken at 3 seconds, so it needs to look alive at first paint. So even before clicking, cards should be visible and the board should look decent. The demo play could just begin after click.

Hmm, but "first screenshot looks alive" - if cards just sit there statically, is that "alive"? I think the initial pre-populated cards already provide life. Then the demo plays after you click.

Actually wait - maybe the demo should auto-start after like 1 second? No, requirement says "behind a 'Click or press a key to start' message". So the message blocks interaction until you click. But the cards are still visible behind the message.

Let me interpret: The message appears initially. Cards are visible. The search and add are available but blocked. Actually, if it's a "game", then maybe the message blocks everything until you start. Let me make the message an overlay that captures clicks, but shows the board behind it. Then when clicked, it removes itself and the demo begins.

Hmm, but actually reading again: "The demo uses the keyboard, display a clear 'Click or press a key to start' message." and "The iframe only receives keyboard events after the user clicks it".

So before clicking: keyboard events blocked. After clicking: keyboard events flow. Mouse clicks should also start the demo.

OK here's my final interpretation:
- On load, show an overlay banner "Click or press a key to start" centered on the board.
- The board is rendered behind it with all 6 cards visible (alive looking).
- Click anywhere on the board OR press a key → overlay disappears, demo timer starts.
- During demo, cards shuffle periodically (every 2.5s pick random card from non-done columns and move toward done, or just random move).
- All interactions (drag, search, add, delete, theme) work once started.

This makes the first screenshot (at 3s after load) show the board with the overlay still there potentially... hmm, the screenshot is taken at 3 seconds automatically. If the overlay is still showing at 3 seconds, the screenshot would show the overlay on top of the board.

Wait, the screenshot is "taken automatically about 3 seconds after load". So at t=3s, what do we see? If overlay is there, we see overlay. If not, we see board.

The requirement says "Something meaningful must be visible on first paint" and "the first screenshot looks alive". I think both conditions are satisfied if:
- First paint shows the board with cards (meaningful, alive looking)
- At 3s later, either still board (if demo started instantly) or board + some movement.

To be safe, let me NOT show an overlay blocking view. Instead, show the overlay only if needed for keyboard. Like: no visual overlay shown initially, just the board with cards. The "Click or press a key to start" message could be shown but... no, that contradicts "the first screenshot looks alive".

Compromise: Show a small banner at top of screen "Click or press a key to start" - this doesn't block view much. Or show it as a subtle hint at the bottom. Hmm.

Actually, let me re-read one more time: "Games must start in a self-playing demo mode (a simple AI plays) behind a 'Click or pretty much any interaction-blocking overlay, but it should not fully hide the board."

OK let me go with this approach:
- Show a semi-transparent overlay banner centered, with text "Click or press a key to start".
- It does block clicks on the board area underneath but the board is still visible through it (overlay is mostly transparent).
- Actually no - let me make it not block. Make it a small pill/banner at top that says "Click or press a key to start" - this doesn't interfere with the board. When clicked, it disappears.

That way the board is fully visible at first paint. The banner is just a hint at the top. When you click, banner hides and demo begins.

Hmm, but if it doesn't block anything, then why say "only receives keyboard events after the user clicks it"? Because they want to ensure the demo doesn't accidentally trigger something on load. So the keydown handler checks a `started` flag.

Let me go with: small non-blocking banner at top center "Click or press a key to start" initially. After click/key, remove banner, start demo. Keyboard blocked otherwise. This satisfies everything.

Actually wait, I want to make sure the screenshot at 3 seconds shows something alive. If demo hasn't started yet (user hasn't clicked within 3 seconds), at 3 seconds we'd see banner + board. That's fine - board looks alive with 6 cards.

If demo starts at 2 seconds, by 3 seconds we see moved cards. Either way looks alive.

Good. Let me code this.

Now for the actual card content. Let me create realistic cards:

To do:
- "Design landing page mockup" - high
- "Set up CI pipeline" - medium
- "Write API docs" - low

In progress:
- "Implement auth module" - high
- "Refactor database schema" - medium

Done:
- "Setup dev environment" - low
- "Create project README" - low

Let me also make the demo AI do something reasonable: every 2.5s, pick a random card from todo or inprogress, and move it toward done (with some probability). This simulates progress.

Let me write the code now.

For the drop indicator during drag, I'll highlight the column with a dashed outline border when dragging over it.

Let me write clean, organized code:

```html
<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Interactive Kanban Board</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }

:root {
  --bg: #f4f5f7;
  --column-bg: #ebecf0;
  --column-header: #dfe1e6;
  --card-bg: #ffffff;
  --text: #172b4d;
  --muted: #5e6c84;
  --border: #cfd8e3;
  --accent: #0052cc;
  --accent-soft: rgba(0, 82, 204, 0.1);
  --low: #5e6c84;
  --medium: #e6a700;
  --high: #d13212;
  --shadow: 0 1px 2px rgba(9, 30, 66, 0.1), 0 0 1px rgba(9, 30, 66, 0.2);
  --hover: rgba(9, 30, 66, 0.04);
}

[data-theme="dark"] {
  --bg: #0f1117;
  --column-bg: #1c1f2b;
  --column-header: #232634;
  --card-bg: #2a2e3d;
  --text: #e6e8ee;
  --muted: #9aa3b2;
  --border: #3a3f52;
  --accent: #4d8dff;
  --accent-soft: rgba(77, 141, 255, 0.12);
  --low: #8b93a5;
  --medium: #d9a800;
  --high: #ff6b5e;
  --shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 0 1px rgba(0, 0, 0, 0.5);
  --hover: rgba(255, 255, 255, 0.05);
}

body {
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  background: var(--bg);
  color: var(--text);
  min-height: 100vh;
  padding: 24px;
  transition: background 0.2s, color 0.2s;
}

.app-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  margin-bottom: 20px;
  flex-wrap: wrap;
}

.app-title {
  font-size: 22px;
  font-weight: 700;
  letter-spacing: -0.3px;
}

.app-title span { color: var(--accent); }

.toolbar {
  display: flex;
  align-items: center;
  gap: 10px;
}

.search-box {
  position: relative;
}

.search-box input {
  width: 220px;
  padding: 8px 12px 8px 34px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--card-bg);
  color: var(--text);
  font-size: 14px;
  outline: none;
  transition: border-color 0.15s, box-shadow 0.15s;
}

.search-box input:focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}

.search-icon {
  position: absolute;
  left: 10px;
  top: 50%;
  transform: translateY(-50%);
  color: var(--muted);
  pointer-events: none;
}

.btn {
  padding: 8px 14px;
  border: none;
  border-radius: 6px;
  font-size: 14px;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.15s, transform 0.05s;
}

.btn:active { transform: translateY(1px); }

.btn-primary {
  background: var(--accent);
  color: #fff;
}
.btn-primary:hover { background: var(--accent); }

.btn-ghost {
  background: transparent;
  color: var(--text);
  border: 1px solid var(--border);
}
.btn-ghost:hover { background: var(--hover); }

.btn-icon {
  width: 36px;
  height: 36px;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--border);
  background: var(--card-bg);
  color: var(--text);
  border-radius: 6px;
  cursor: pointer;
}
.btn-icon:hover { background: var(--hover); }

.theme-toggle .icon { transition: transform 0.3s; }

.board {
  display: grid;
  grid-template-columns: repeat(3, minmax(240px, 1fr));
  gap: 16px;
  align-items: start;
}

@media (max-width: 760px) {
  .board { grid-template-columns: 1fr; }
}

.column {
  background: var(--column-bg);
  border-radius: 8px;
  padding: 12px;
  min-height: 200px;
  display: flex;
  flex-direction: column;
  transition: background 0.15s, outline 0.1s;
  outline: 2px dashed transparent;
  outline-offset: -2px;
}

.column.drop-active {
  background: var(--accent-soft);
  outline-color: var(--accent);
}

.column-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 12px;
  padding: 0 4px;
}

.column-title {
  font-size: 13px;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.6px;
  color: var(--muted);
}

.column-count {
  background: var(--column-header);
  border-radius: 10px;
  padding: 2px 8px;
  font-size: 12px;
  font-weight: 700;
  color: var(--muted);
  min-width: 22px;
  text-align: center;
}

.cards-list {
  display: flex;
  flex-direction: column;
  gap: 8px;
  min-height: 40px;
  flex: 1;
}

.card {
  background: var(--card-bg);
  border-radius: 6px;
  padding: 10px 12px;
  box-shadow: var(--shadow);
  cursor: grab;
  border-left: 3px solid transparent;
  transition: box-shadow 0.15s, transform 0.05s;
  animation: cardFade 0.2s ease;
}

@keyframes cardFade {
  from { opacity: 0; transform: translateY(4px); }
  to { opacity: 1; transform: translateY(0); }
}

.card:hover {
  box-shadow: 0 2px 6px rgba(9, 30, 66, 0.15), 0 0 1px rgba(9, 30, 66, 0.3);
}

.card.dragging {
  opacity: 0.4;
  cursor: grabbing;
}

.card-title {
  font-size: 14px;
  font-weight: 500;
  margin-bottom: 8px;
  line-height: 1.35;
  word-break: break-word;
}

.card-meta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
}

.priority {
  font-size: 11px;
  font-weight: 700;
  padding: 2px 8px;
  border-radius: 10px;
  text-transform: uppercase;
  letter-spacing: 0.4px;
  white-space: nowrap;
}

.priority.low { background: rgba(94, 108, 132, 0.15); color: var(--low); }
.priority.medium { background: rgba(230, 167, 0, 0.15); color: var(--medium); }
.priority.high { background: rgba(209, 50, 18, 0.15); color: var(--high); }

.card-delete {
  background: none;
  border: none;
  color: var(--muted);
  font-size: 16px;
  padding: 2px 6px;
  border-radius: 4px;
  cursor: pointer;
  opacity: 0;
  transition: opacity 0.15s, color 0.15s, background 0.15s;
  font-family: inherit;
}

.card:hover .card-delete { opacity: 1; }
.card-delete:hover { background: rgba(220, 53, 69, 0.1); color: #de350b; }

/* empty state */
.empty-hint {
  color: var(--muted);
  font-size: 12px;
  font-style: italic;
  padding: 8px;
  text-align: center;
}

/* start overlay */
.start-overlay {
  position: fixed;
  inset: 0;
  background: rgba(9, 30, 66, 0.45);
  backdrop-filter: blur(4px);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 100;
  animation: fadeIn 0.3s ease;
}

.start-overlay.hidden { display: none; }

@keyframes fadeIn {
  from { opacity: 0; }
  to { opacity: 1; }
}

.start-message {
  background: var(--card-bg);
  color: var(--text);
  padding: 18px 24px;
  border-radius: 10px;
  font-size: 15px;
  font-weight: 600;
  text-align: center;
  box-shadow: 0 8px 30px rgba(0, 0, 0, 0.3);
  max-width: 320px;
}

.start-message small {
  display: block;
  margin-top: 8px;
  font-weight: 400;
  color: var(--muted);
  font-size: 12px;
}

/* modal */
.modal-backdrop {
  position: fixed;
  inset: 0;
  background: rgba(9, 30, 66, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 200;
  animation: fadeIn 0.2s ease;
}

.modal-backdrop.hidden { display: none; }

.modal {
  background: var(--card-bg);
  border-radius: 10px;
  padding: 20px;
  width: min(400px, 90vw);
  box-shadow: 0 20px 60px rgba(0,0,0,0.3);
  animation: popIn 0.2s ease;
}

@keyframes popIn {
  from { opacity: 0; transform: scale(0.95) translateY(8px); }
  to { opacity: 1; transform: scale(1) translateY(0); }
}

.modal h3 {
  margin-bottom: 16px;
  font-size: 17px;
}

.form-group { margin-bottom: 14px; }

.form-group label {
  display: block;
  font-size: 13px;
  font-weight: 600;
  margin-bottom: 6px;
  color: var(--muted);
}

.form-group input {
  width: 100%;
  padding: 9px 11px;
  border: 1px solid var(--border);
  border-radius: 6px;
  background: var(--bg);
  color: var(--text);
  font-size: 14px;
  outline: none;
}

.form-group input:focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}

.modal-actions {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
  margin-top: 18px;
}

.btn-danger {
  background: #de350b;
  color: #fff;
}

.btn-danger:hover { background: #bf2600; }

/* toast */
.toast {
  position: fixed;
  bottom: 20px;
  left: 50%;
  transform: translateX(-50%) translateY(20px);
  background: var(--text);
  color: var(--card-bg);
  padding: 10px 18px;
  border-radius: 6px;
  font-size: 13px;
  font-weight: 600;
  opacity: 0;
  transition: opacity 0.25s, transform 0.25s;
  z-index: 300;
  pointer-events: none;
}

.toast.show {
  opacity: 1;
  transform: translateX(-50%) translateY(0);
}
</style>
</head>
<body>
  <div class="start-overlay" id="startOverlay">
    <div class="start-message">
      Ready to play
      <small>Click or press any key to start the demo. Drag, search, add & delete cards anytime afterwards.</small>
    </div>
  </div>

  <header class="app-header">
    <h1 class="app-title">Interactive <span>Kanban</span></h1>
    <div class="toolbar">
      <div class="search-box">
        <span class="search-icon">⌕</span>
        <input type="text" id="searchInput" placeholder="Filter cards..." autocomplete="off">
      </div>
      <button class="btn btn-primary" id="addBtn">+ New card</button>
      <button class="btn-icon theme-toggle" id="themeToggle" title="Toggle theme">
        <svg class="icon" id="iconSun" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
        <svg class="icon" id="iconMoon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
      </button>
    </div>
  </header>

  <main class="board" id="board">
    <!-- columns rendered by JS -->
  </main>

  <!-- Add card modal -->
  <div class="modal-backdrop hidden" id="modalBackdrop">
    <div class="modal">
      <h3>Add new card</h3>
      <div class="form-group">
        <label for="cardTitleInput">Title *</label>
        <input type="text" id="cardTitleInput" placeholder="What needs doing?" maxlength="80">
      </div>
      <div class="form-group">
        <label for="cardPrioritySelect">Priority</label>
        <select id="cardPrioritySelect" style="width:100%; padding:9px 11px; border:1px solid var(--border); border-radius:6px; background:var(--bg); color:var(--text); font-size:14px; outline:none;">
          <option value="low">Low</option>
          <option value="medium">Medium</option>
          <option value="high">High</option>
        </select>
      </div>
      <div class="modal-actions">
        <button class="btn btn-ghost" id="cancelBtn">Cancel</button>
        <button class="btn btn-primary" id="submitBtn">Add card</button>
      </div>
    </div>
  </div>

  <div class="toast" id="toast"></div>

<script>
(function() {
  'use strict';

  // ---- State ----
  const COLUMNS = ['todo', 'inProgress', 'done'];
  const COLUMN_LABELS = { todo: 'To do', inProgress: 'In progress', done: 'Done' };

  let nextId = 1;
  let state = {
    columns: {
      todo: [
        { id: nextId++, title: 'Design landing page mockup', priority: 'high' },
        { id: nextId++, title: 'Set up CI pipeline', priority: 'medium' },
        { id: nextId++, title: 'Write API documentation', priority: 'low' },
      ],
      inProgress: [
        { id: nextId++, title: 'Implement authentication module', priority: 'high' },
        { id: nextId++, title: 'Refactor database schema', priority: 'medium' },
      ],
      done: [
        { id: nextId++, title: 'Configure development environment', priority: 'low' },
        { id: nextId++, title: 'Draft project README', priority: 'low' },
      ],
    },
    theme: 'light',
    started: false,
  };

  const DEMO_INTERVAL = 2200; // ms

  // ---- Elements ----
  const boardEl = document.getElementById('board');
  const searchInput = document.getElementById('searchInput');
  const addBtn = document.getElementById('addBtn');
  const themeToggle = document.getElementById('themeToggle');
  const startOverlay = document.getElementById('startOverlay');
  const modalBackdrop = document.getElementById('modalBackdrop');
  const cardTitleInput = document.getElementById('cardTitleInput');
  const cardPrioritySelect = document.getElementById('cardPrioritySelect');
  const submitBtn = document.getElementById('submitBtn');
  const cancelBtn = document.getElementById('cancelBtn');
  const toastEl = document.getElementById('toast');

  // ---- Theme ----
  function applyTheme() {
    document.documentElement.setAttribute('data-theme', state.theme);
    document.getElementById('iconSun').style.display = state.theme === 'dark' ? 'block' : 'none';
    document.getElementById('iconMoon').style.display = state.theme === 'dark' ? 'none' : 'block';
  }
  themeToggle.addEventListener('click', () => {
    state.theme = state.theme === 'light' ? 'dark' : 'light';
    applyTheme();
  });

  // ---- Rendering ----
  function escapeHtml(str) {
    return str.replace(/[&<>"']/g, c => ({
      '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'
    }[c]));
  }

  function getFiltered() {
    const q = searchInput.value.trim().toLowerCase();
    if (!q) return state.columns;
    const out = {};
    COLUMNS.forEach(col => {
      out[col] = state.columns[col].filter(c => c.title.toLowerCase().includes(q));
    });
    return out;
  }

  function cardTemplate(card) {
    return `
      <article class="card" draggable="true" data-id="${card.id}">
        <div class="card-title">${escapeHtml(card.title)}</div>
        <div class="card-meta">
          <span class="priority ${card.priority}">${card.priority}</span>
          <button class="card-delete" data-action="delete" data-id="${card.id}" title="Delete card" aria-label="Delete card">×</button>
        </div>
      </article>`;
  }

  function render() {
    boardEl.innerHTML = '';
    const filtered = getFiltered();

    COLUMNS.forEach(col => {
      const colEl = document.createElement('section');
      colEl.className = 'column';
      colEl.dataset.column = col;
      const listEl = document.createElement('div');
      listEl.className = 'cards-list';

      const items = filtered[col] || [];
      if (items.length === 0 && state.started) {
        const hint = document.createElement('div');
        hint.className = 'empty-hint';
        hint.textContent = 'No cards here';
        listEl.appendChild(hint);
      }
      items.forEach(c => listEl.insertAdjacentHTML('beforeend', cardTemplate(c)));

      colEl.innerHTML = `
        <div class="column-header">
          <span class="column-title">${COLUMN_LABELS[col]}</span>
          <span class="column-count">${(filtered[col] || []).length}</span>
        </div>
        <div class="cards-list"></div>`;
      listEl.append(...Array.from(colEl.querySelectorAll('.card')));

      boardEl.appendChild(colEl);
    });
  }

  // Wait — I need to append cards to listEl, not colEl. Let me redo this.
```

Oops, I made an error. Let me fix the render function properly. I'll build column elements step by step.

Let me rewrite:

```js
function render() {
  boardEl.innerHTML = '';
  const filtered = getFiltered();

  COLUMNS.forEach(col => {
    const colEl = document.createElement('section');
    colEl.className = 'column';
    colEl.dataset.column = col;

    const headerEl = document.createElement('div');
    headerEl.className = 'column-header';
    const titleSpan = document.createElement('span');
    titleSpan.className = 'column-title';
    titleSpan.textContent = COLUMN_LABELS[col];
    const countSpan = document.createElement('span');
    countSpan.className = 'column-count';
    countSpan.textContent = (filtered[col] || []).length;
    headerEl.append(titleSpan, countSpan);

    const listEl = document.createElement('div');
    listEl.className = 'cards-list';

    const items = filtered[col] || [];
    if (items.length === 0 && state.started) {
      const hint = document.createElement('div');
      hint.className = 'empty-hint';
      hint.textContent = 'No cards here';
      listEl.appendChild(hint);
    }
    items.forEach(c => listEl.insertAdjacentHTML('beforeend', cardTemplate(c)));

    colEl.append(headerEl, listEl);
    boardEl.appendChild(colEl);
  });
}
```

Good. Now drag and drop:

```js
let dragData = null;

boardEl.addEventListener('dragstart', e => {
  if (!state.started) { e.preventDefault(); return; }
  const card = e.target.closest('.card');
  if (!card) return;
  dragData = { id: Number(card.dataset.id), from: currentColumnOf(card) };
  card.classList.add('dragging');
  e.dataTransfer.effectAllowed = 'move';
  e.dataTransfer.setData('text/plain', String(dragData.id));
});

boardEl.addEventListener('dragend', e => {
  const card = e.target.closest('.card');
  if (card) card.classList.remove('dragging');
  document.querySelectorAll('.column.drop-active').forEach(el => el.classList.remove('drop-active'));
  dragData = null;
});

function currentColumnOf(cardEl) {
  const col = cardEl.closest('.column');
  return col.dataset.column;
}

boardEl.addEventListener('dragover', e => {
  if (!state.started || !dragData) return;
  e.preventDefault();
  const col = e.target.closest('.column');
  if (!col) return;
  document.querySelectorAll('.column.drop-active').forEach(el => el.classList.remove('drop-active'));
  col.classList.add('drop-active');
});

boardEl.addEventListener('drop', e => {
  if (!state.started || !dragData) return;
  e.preventDefault();
  const col = e.target.closest('.column');
  if (!col || !dragData) return;
  const targetCol = col.dataset.column;
  if (targetCol === dragData.from) { dragData = null; render(); return; }
  moveCard(dragData.id, targetCol);
  dragData = null;
  render();
});

function moveCard(id, targetCol) {
  // find and remove from source
  for (const col of COLUMNS) {
    const idx = state.columns[col].findIndex(c => c.id === id);
    if (idx >= 0) {
      const [card] = state.columns[col].splice(idx, 1);
      state.columns[targetCol].push(card);
      break;
    }
  }
}
```

Search input:
```js
searchInput.addEventListener('input', () => {
  if (!state.started) return;
  render();
});
```

Hmm, I said before clicking, search is blocked. But I also want the board visible with cards. The search input should work after start. But wait, during preview, maybe I want search to NOT filter (so the first screenshot shows all cards). Yes, that's what I did.

Add card modal:
```js
function openModal() {
  modalBackdrop.classList.remove('hidden');
  cardTitleInput.value = '';
  cardPrioritySelect.value = 'medium';
  setTimeout(() => cardTitleInput.focus(), 50);
}

function closeModal() {
  modalBackdrop.classList.add('hidden');
}

addBtn.addEventListener('click', openModal);
cancelBtn.addEventListener('click', closeModal);
modalBackdrop.addEventListener('click', e => {
  if (e.target === modalBackdrop) closeModal();
});

submitBtn.addEventListener('click', () => {
  const title = cardTitleInput.value.trim();
  if (!title) {
    showToast('Title is required');
    cardTitleInput.focus();
    return;
  }
  state.columns.todo.push({ id: nextId++, title, priority: cardPrioritySelect.value });
  closeModal();
  if (state.started) render();
  else showToast('Click or press a key to start the demo');
});

cardTitleInput.addEventListener('keydown', e => {
  if (e.key === 'Enter') submitBtn.click();
});

// Delete card
boardEl.addEventListener('click', e => {
  const delBtn = e.target.closest('.card-delete');
  if (!delBtn) return;
  const id = Number(delBtn.dataset.id);
  deleteCard(id);
});

function deleteCard(id) {
  for (const col of COLUMNS) {
    const idx = state.columns[col].findIndex(c => c.id === id);
    if (idx >= 0) {
      state.columns[col].splice(idx, 1);
      break;
    }
  }
  if (state.started) render();
  else showToast('Click or press a key to start the demo');
}
```

Wait - during preview (not started), the delete button should not do anything meaningful? Actually, the requirement says delete is available via a button on the card. If we disable it during preview, that might be confusing. Hmm.

Actually, the requirement says "Click or press a key to start" blocks keyboard events. Mouse clicks... the requirement says "The iframe only receives keyboard events after the user clicks it". For mouse, I think both should work after click. Let me allow click interactions (drag, add, delete, search) after clicking. During preview, I'll disable everything except showing the board.

But wait, that's a bit harsh. Maybe make it so the delete button doesn't work during preview but still exists. Actually let me just not render delete buttons when not started? That changes appearance. Hmm.

Alternative: During preview, allow clicks but ignore them. Simplest: check `state.started` and if not started, don't respond.

Actually let me reconsider the whole design. The "click or press a key to start" seems mainly about starting the demo. But the interactions (add, delete, search) should probably also require starting. Otherwise the screenshot might show a deleted card.

OK my approach is fine. Let me keep it. Delete won't work until started. That matches "games must start in demo mode".

Hmm wait, but the screenshot at 3 seconds - if the user hasn't clicked by then, we show the board with cards, no deletions. That's fine.

Demo mode:
```js
let demoTimer = null;

function startDemo() {
  if (state.started) return;
  state.started = true;
  startOverlay.classList.add('hidden');
  render();

  demoTimer = setInterval(() => {
    // pick a random card from todo or inprogress, prefer moving toward done
    const sources = ['todo', 'inProgress'];
    const pool = sources.map(s => state.columns[s]).filter(arr => arr.length > 0);
    if (pool.length === 0) return;
    const srcArr = pool[Math.floor(Math.random() * pool.length)];
    const idx = Math.floor(Math.random() * srcArr.length);
    const card = srcArr[idx];
    // 70% chance to move toward done
    const target = Math.random() < 0.7 ? 'done' : (sources.indexOf(srcArr === state.columns.todo ? 'todo' : 'inProgress') > -1 ? 'inProgress' : srcArr === state.columns.todo ? 'inProgress' : 'todo');
    // Simpler: 70% -> done, else -> pick opposite of source
    let dest;
    if (Math.random() < 0.7) dest = 'done';
    else {
      dest = srcArr === state.columns.todo ? 'inProgress' : 'todo';
    }
    moveCard(card.id, dest);
    render();
  }, DEMO_INTERVAL);
}

boardEl.addEventListener('click', e => {
  if (!state.started) startDemo();
});

document.addEventListener('keydown', e => {
  if (!state.started) startDemo();
});
```

Hmm wait, there's an issue. If I attach a 'click' listener to boardEl, that fires before the click listener on individual elements? Event bubbles up, so boardEl click handler fires on the board level - yes it will fire. Good.

But there's a subtlety: the startOverlay covers the board. Clicks on the overlay go to the overlay, not the board. Once overlay is hidden, clicks reach board. Good.

Also pressing a key: `document` listener fires. Need to check key is not part of form. Simple approach: any key starts demo. That's fine for this demo.

Toast:
```js
let toastTimer = null;
function showToast(msg) {
  toastEl.textContent = msg;
  toastEl.classList.add('show');
  clearTimeout(toastTimer);
  toastTimer = setTimeout(() => toastEl.classList.remove('show'), 2000);
}
```

Init:
```js
applyTheme();
render();
```

Wait, during preview, should render show all cards? Yes. Should show counts. Yes.

Let me also double check the drag flow. When I drag a card and drop it, I call moveCard then render(). Good. When dropping outside any column (e.g., on the board but not on a column), nothing happens. Good.

Edge case: dropping back on same column - my handler returns early after `dragData = null` without rendering. That leaves the card in place. Good. Actually I set dragData = null and return without render. Fine.

Another consideration: the dragover handler calls preventDefault which allows drop. Good.

One more thing: the `cards-list` inside each column has `flex: 1` but also `min-height`. The column has `min-height: 200px`. Cards stack vertically. Good.

Let me also add a nice touch: when dragging, show a small floating element following the cursor. That's complex; let me skip and just use opacity.

Alright, let me also handle the case where the demo might move cards during drag - unlikely since clicks happen fast. Fine.

Now, one concern: The start overlay covers the whole viewport. At 3 second screenshot, if user hasn't clicked, we see the overlay. The overlay has text "Ready to play / Click or press a key to start the demo". That's visible but somewhat busy. Is that OK? The requirement says "Something meaningful must be visible on first paint". The board cards ARE visible through the overlay (overlay is semi-transparent). The message is also visible. That's fine - it's meaningful.

Actually, maybe I should make the overlay less intrusive - like a smaller banner at top, or a message card at bottom. Let me make it a centered card on the board area instead of fullscreen overlay. Hmm, but then it might cover the board visually.

Let me make it fullscreen overlay with backdrop blur but the message card visible. It's the clearest UX pattern. And it satisfies "visible on first paint" because the board is behind it.

Alternatively: no overlay at all initially, just show the banner as a subtle element. Hmm.

You know what, let me go with a cleaner approach: a banner strip at the top of the page (not covering the board), showing "Click or press a key to start". This is visible, doesn't block the board. Then the board is immediately fully visible. When clicked, the banner fades away and demo starts.

Yes! This is better. Let me do that.

```css
.start-banner {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  margin-bottom: 16px;
  padding: 10px 16px;
  background: linear-gradient(90deg, rgba(0,82,204,0.08), rgba(0,82,204,0.02));
  border: 1px solid var(--accent);
  border-radius: 8px;
  font-size: 13px;
  font-weight: 600;
  color: var(--accent);
  animation: slideDown 0.3s ease;
}

.start-banner.hidden { display: none; }

@keyframes slideDown {
  from { opacity: 0; transform: translateY(-10px); }
  to { opacity: 1; transform: translateY(0); }
}
```

And:
```html
<div class="start-banner" id="startBanner">Click or press a key to start</div>
```

When started, add `.hidden`. Banner at top, board below fully visible. 

Now also consider: what if the user just wants to interact without the demo? The requirement says "Games must start in a self-playing demo mode". So starting the demo is mandatory. The demo plays automatically. User can still do add/delete/search/drag after. So that's consistent.

Alright, let me finalize and write the complete file.

One more thought on responsiveness: at 375px width, the header wraps, board becomes single column. Columns get narrow. Cards get cramped but readable. The search box shrinks. Should be OK.

Let me also make sure the modal fits. At 375px, width: min(400px, 90vw) = 337px. Fine.

Also the `+ New card` button at 375px wraps to two lines. Fine.

Alright, writing final code now.

Oh wait, one issue with drag and drop: on some browsers, dragging works fine. But on mobile it doesn't. Not required though. Fine.

Also, `dataTransfer.setData` is needed for Firefox. Done.

Let me also consider: the `cardTemplate` uses template literals with `escapeHtml`. Good.

Let me also verify the priority badge shows "low"/"medium"/"high" text. The requirement: "Each card has a title and a priority (low / medium / high) shown as a colored badge." My badge shows the word (e.g., "High"). That's colored. Could also be icon-only. Text is clearer. Keep text.

Final check on the demo: the demo moves cards every 2.2 seconds. Over 3 seconds, at least one move likely happens. At t=3s, we see moved cards. Looks alive. 

But wait - the screenshot is taken automatically at 3 seconds. If the demo starts at t=0 (immediate), by t=3s we've had ~1.3 intervals = ~1.4 moves possible (each interval 2.2s). So likely 1-2 cards moved. Good - alive looking.

If demo starts later (user takes 1s to click), fewer moves. Still shows cards. Fine.

Alright, writing the complete HTML file now.

Actually, let me reconsider: should the initial screenshot possibly be taken right at load (before any render)? The requirement says "about 3 seconds after load". So it's 3 seconds after page load. By then, render has happened. If overlay is shown, screenshot shows overlay + board behind. If not, screenshot shows board.

With my banner approach: at t=0, banner shows, board renders. At t=3s, screenshot. Banner still there (unless user clicked). Board fully visible behind banner. Meaningful + alive. 

Now let me write it all out.

Additional polish: add a subtle entrance animation for columns. Already have cardFade animation. Good enough.

Let me write the final file. I'll keep it clean and well-organized.

Actually, I realize there might be an issue with the demo logic. Let me simplify:

```js
demoTimer = setInterval(() => {
  // pick a card from todo or inprogress
  const sources = ['todo', 'inProgress'];
  const sourceCol = sources[Math.floor(Math.random() * sources.length)];
  const arr = state.columns[sourceCol];
  if (arr.length === 0) return;
  const card = arr[Math.floor(Math.random() * arr.length)];
  let dest;
  if (Math.random() < 0.65) dest = 'done';
  else dest = sources[(sources.indexOf(sourceCol) + 1) % 2];
  moveCard(card.id, dest);
  render();
}, DEMO_INTERVAL);
```

Cleaner. Let me finalize.

Let me also make sure when the demo is running and the user drags a card, that still works. Since state.started is true, drag handlers proceed. Good.

Edge case: what if user drags card while demo moves another? Race conditions are minor; acceptable.

Let me write the final code now. I'll aim for clean, readable, well-commented (minimal comments since natural language isn't needed, but a few comments help readability... the instructions say no commentary, just the code. Comments are fine I think, but let me minimize). Actually the instruction "No text before or after the code block" refers to the reply itself, not code comments. Code comments inside are OK. But let me keep them minimal for cleanliness.

Writing now.