Models 44

MiniMax M3.1 Flash

by MiniMaxReasoningClean
Open in new window

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

How it went

Total time
43.6 s
Cost
$0.0065
Output tokens
10,858
Spent thinking
2,416 tokens
Speed
250.6 tok/s
Console errors
0
First token after
269 ms
First visible text after
14.8 s
Prompt tokens
678
Code size
27.6 KB
Finish reason
stop
Served by
Nebius
Model id
minimax/minimax-m3.1-flash
Reasoning sent
{"effort":"medium"}
Max output
64,000 tokens
Generated
Nov 11, 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 · 717 lines

<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Kanban Board</title>
<style>
  *{box-sizing:border-box;}
  :root{
    --bg:#eef1f7;
    --bg-grad-1:#e8ecf6;
    --bg-grad-2:#f5f2fb;
    --surface:#ffffff;
    --surface-2:#f8fafc;
    --text:#172033;
    --muted:#6b7793;
    --border:#e3e8f0;
    --border-strong:#d3dae6;
    --accent:#5b5bd6;
    --accent-soft:#ececff;
    --shadow-sm:0 1px 2px rgba(16,24,40,.06),0 1px 3px rgba(16,24,40,.08);
    --shadow-md:0 8px 24px rgba(16,24,40,.10);
    --shadow-lg:0 24px 60px rgba(16,24,40,.22);
    --c-todo:#64748b;
    --c-doing:#e08b12;
    --c-done:#16a34a;
    --low:#0f9d58;
    --med:#d97706;
    --high:#dc2626;
  }
  html[data-theme="dark"]{
    --bg:#0d1117;
    --bg-grad-1:#111823;
    --bg-grad-2:#0c1119;
    --surface:#161c26;
    --surface-2:#1c2431;
    --text:#e6ecf5;
    --muted:#8b9bb4;
    --border:#242e3d;
    --border-strong:#32405380;
    --accent:#8b8bf5;
    --accent-soft:#262a4a;
    --shadow-sm:0 1px 2px rgba(0,0,0,.4);
    --shadow-md:0 10px 26px rgba(0,0,0,.45);
    --shadow-lg:0 28px 70px rgba(0,0,0,.6);
    --c-todo:#94a3b8;
    --c-doing:#f0a63a;
    --c-done:#34d399;
    --low:#34d399;
    --med:#fbbf24;
    --high:#f87171;
  }
  html,body{height:100%;}
  body{
    margin:0;
    font-family:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;
    background:radial-gradient(1200px 600px at 10% -10%,var(--bg-grad-1),transparent 60%),
               radial-gradient(900px 500px at 100% 0%,var(--bg-grad-2),transparent 55%),
               var(--bg);
    color:var(--text);
    -webkit-font-smoothing:antialiased;
    transition:background .3s ease,color .25s ease;
  }
  .wrap{max-width:1180px;margin:0 auto;padding:22px 20px 60px;}

  /* ---------- Header ---------- */
  header.top{
    display:flex;align-items:center;gap:16px;flex-wrap:wrap;
    background:color-mix(in srgb,var(--surface) 82%,transparent);
    backdrop-filter:blur(10px);
    border:1px solid var(--border);
    border-radius:18px;
    padding:14px 18px;
    box-shadow:var(--shadow-sm);
    margin-bottom:20px;
  }
  .brand{display:flex;align-items:center;gap:12px;margin-right:auto;}
  .logo{
    width:40px;height:40px;border-radius:12px;flex:none;
    background:linear-gradient(140deg,var(--accent),#c084fc);
    display:grid;place-items:center;box-shadow:0 6px 16px rgba(91,91,214,.35);
  }
  .logo svg{width:22px;height:22px;}
  h1{font-size:19px;margin:0;letter-spacing:-.02em;font-weight:700;}
  .sub{margin:2px 0 0;font-size:12.5px;color:var(--muted);}
  .search{position:relative;flex:1 1 230px;max-width:340px;min-width:180px;}
  .search svg{position:absolute;left:11px;top:50%;transform:translateY(-50%);width:16px;height:16px;opacity:.55;}
  .search input{
    width:100%;padding:10px 32px 10px 34px;border-radius:11px;
    border:1px solid var(--border);background:var(--surface-2);color:var(--text);
    font-size:14px;outline:none;transition:border-color .15s,box-shadow .15s;
  }
  .search input:focus{border-color:var(--accent);box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 18%,transparent);}
  .clear-x{
    position:absolute;right:8px;top:50%;transform:translateY(-50%);
    border:0;background:transparent;color:var(--muted);cursor:pointer;font-size:16px;
    display:none;line-height:1;padding:2px 5px;border-radius:6px;
  }
  .clear-x:hover{background:var(--border);color:var(--text);}
  .search.has-query .clear-x{display:block;}

  .btn{
    font:inherit;font-size:14px;font-weight:600;cursor:pointer;border-radius:11px;
    padding:10px 15px;border:1px solid transparent;transition:transform .12s ease,background .15s,box-shadow .15s,border-color .15s;
    display:inline-flex;align-items:center;gap:7px;white-space:nowrap;
  }
  .btn:active{transform:translateY(1px);}
  .btn.primary{background:var(--accent);color:#fff;box-shadow:0 6px 16px color-mix(in srgb,var(--accent) 35%,transparent);}
  .btn.primary:hover{filter:brightness(1.06);}
  .btn.ghost{background:var(--surface-2);color:var(--text);border-color:var(--border);}
  .btn.ghost:hover{background:var(--surface);border-color:var(--border-strong);}
  .btn svg{width:16px;height:16px;}
  .icon-btn{
    border:1px solid transparent;background:transparent;color:var(--muted);
    width:26px;height:26px;border-radius:8px;cursor:pointer;display:grid;place-items:center;
    font-size:14px;line-height:1;transition:background .15s,color .15s;
  }
  .icon-btn:hover{background:var(--border);color:var(--text);}
  .icon-btn.danger:hover{background:color-mix(in srgb,var(--high) 16%,transparent);color:var(--high);}
  .icon-btn svg{width:14px;height:14px;}

  /* ---------- Board ---------- */
  .board{display:grid;grid-template-columns:repeat(3,1fr);gap:18px;align-items:start;}
  .column{
    background:color-mix(in srgb,var(--surface) 62%,transparent);
    border:1px solid var(--border);
    border-radius:16px;
    padding:12px 12px 14px;
    min-height:220px;
    transition:border-color .18s,box-shadow .18s,background .18s;
  }
  .column.drag-over{
    border-color:var(--accent);
    background:color-mix(in srgb,var(--accent) 7%,var(--surface));
    box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 15%,transparent);
  }
  .col-head{display:flex;align-items:center;gap:9px;padding:2px 4px 12px;}
  .dot{width:9px;height:9px;border-radius:50%;flex:none;}
  .col-title{font-size:13.5px;font-weight:700;letter-spacing:.03em;text-transform:uppercase;}
  .count{
    font-size:12px;font-weight:700;padding:2px 9px;border-radius:999px;
    background:var(--surface-2);border:1px solid var(--border);color:var(--muted);
    min-width:26px;text-align:center;
  }
  .count.filtered{background:var(--accent-soft);color:var(--accent);border-color:transparent;}
  .col-add{margin-left:auto;}
  .card-list{display:flex;flex-direction:column;gap:10px;min-height:56px;}

  /* ---------- Cards ---------- */
  .card{
    background:var(--surface);
    border:1px solid var(--border);
    border-radius:13px;
    padding:11px 12px 9px;
    box-shadow:var(--shadow-sm);
    cursor:grab;
    position:relative;
    animation:rise .28s cubic-bezier(.2,.8,.3,1) backwards;
    transition:box-shadow .18s,transform .18s,border-color .18s,opacity .18s;
  }
  @keyframes rise{from{opacity:0;transform:translateY(8px);}to{opacity:1;transform:none;}}
  .card:hover{box-shadow:var(--shadow-md);border-color:var(--border-strong);transform:translateY(-1px);}
  .card.dragging{opacity:.35;cursor:grabbing;transform:rotate(1.5deg) scale(.99);}
  .card-top{display:flex;align-items:center;gap:8px;margin-bottom:7px;}
  .badge{
    font-size:10.5px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;
    padding:3px 8px;border-radius:999px;
  }
  .badge.low{color:var(--low);background:color-mix(in srgb,var(--low) 14%,transparent);}
  .badge.medium{color:var(--med);background:color-mix(in srgb,var(--med) 16%,transparent);}
  .badge.high{color:var(--high);background:color-mix(in srgb,var(--high) 14%,transparent);}
  .badge.high::before{content:"▲ ";font-size:8px;vertical-align:1px;}
  .badge.medium::before{content:"◆ ";font-size:8px;vertical-align:1px;}
  .badge.low::before{content:"● ";font-size:8px;vertical-align:1px;}
  .card-title{font-size:14.5px;font-weight:600;line-height:1.35;margin:0 0 9px;letter-spacing:-.01em;word-break:break-word;}
  .card-foot{display:flex;align-items:center;gap:8px;}
  .tag{font-size:11.5px;color:var(--muted);}
  .movers{margin-left:auto;display:flex;gap:2px;opacity:.55;transition:opacity .15s;}
  .card:hover .movers{opacity:1;}

  .empty{
    border:1.5px dashed var(--border-strong);border-radius:12px;
    padding:16px 10px;text-align:center;color:var(--muted);font-size:12.5px;
  }
  .drop-indicator{
    border:2px dashed var(--accent);border-radius:13px;
    background:color-mix(in srgb,var(--accent) 10%,transparent);
    color:var(--accent);font-size:12.5px;font-weight:600;
    display:grid;place-items:center;min-height:62px;
    animation:rise .16s ease backwards;
  }
  .no-results{
    text-align:center;padding:40px 10px;color:var(--muted);font-size:14px;
  }
  .no-results strong{display:block;color:var(--text);font-size:16px;margin-bottom:4px;}

  /* ---------- Modal ---------- */
  .backdrop{
    position:fixed;inset:0;background:rgba(10,15,25,.55);backdrop-filter:blur(3px);
    display:grid;place-items:center;padding:20px;z-index:50;
    animation:fade .18s ease;
  }
  @keyframes fade{from{opacity:0}to{opacity:1}}
  .backdrop[hidden]{display:none;}
  .modal{
    width:100%;max-width:420px;background:var(--surface);border:1px solid var(--border);
    border-radius:18px;padding:22px;box-shadow:var(--shadow-lg);
    animation:pop .22s cubic-bezier(.2,.9,.3,1.1);
  }
  @keyframes pop{from{opacity:0;transform:translateY(12px) scale(.97)}to{opacity:1;transform:none;}}
  .modal h2{margin:0 0 4px;font-size:18px;letter-spacing:-.02em;}
  .modal p.hint{margin:0 0 16px;color:var(--muted);font-size:12.5px;}
  label{display:block;font-size:12.5px;font-weight:600;color:var(--muted);margin-bottom:14px;}
  label span{display:block;margin-bottom:6px;}
  .field{
    width:100%;padding:10px 12px;border-radius:11px;border:1px solid var(--border);
    background:var(--surface-2);color:var(--text);font:inherit;font-size:14px;outline:none;
    transition:border-color .15s,box-shadow .15s;
  }
  .field:focus{border-color:var(--accent);box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 18%,transparent);}
  select.field{appearance:none;background-image:linear-gradient(45deg,transparent 50%,var(--muted) 50%),linear-gradient(135deg,var(--muted) 50%,transparent 50%);background-position:calc(100% - 18px) 50%,calc(100% - 13px) 50%;background-size:5px 5px,5px 5px;background-repeat:no-repeat;}
  .err{color:var(--high);font-size:12.5px;min-height:0;margin:-6px 0 12px;}
  .modal-actions{display:flex;gap:9px;justify-content:flex-end;margin-top:6px;}

  .hint-bar{
    margin-top:18px;text-align:center;color:var(--muted);font-size:12.5px;
  }
  .hint-bar kbd{
    background:var(--surface);border:1px solid var(--border);border-bottom-width:2px;
    border-radius:6px;padding:1px 6px;font-family:inherit;font-size:11.5px;color:var(--text);
  }
  @media (max-width:860px){
    .board{grid-template-columns:1fr;}
    .brand{margin-right:0;width:100%;}
    .search{max-width:none;flex:1 1 100%;order:3;}
    header.top{gap:10px;}
  }
  @media (max-width:420px){
    .wrap{padding:14px 12px 40px;}
    .btn span.label{display:none;}
    .btn{padding:10px 12px;}
  }
</style>
</head>
<body>
<div class="wrap">
  <header class="top">
    <div class="brand">
      <div class="logo" aria-hidden="true">
        <svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
          <rect x="3" y="3" width="5" height="18" rx="1.5"></rect>
          <rect x="10" y="3" width="5" height="11" rx="1.5"></rect>
          <rect x="17" y="3" width="4" height="7" rx="1.5"></rect>
        </svg>
      </div>
      <div>
        <h1>Sprint Board</h1>
        <p class="sub">Drag cards between columns to update status</p>
      </div>
    </div>

    <div class="search" id="searchWrap">
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="7"></circle><path d="M20 20l-3.5-3.5"></path></svg>
      <input id="search" type="search" placeholder="Search cards…" aria-label="Search cards by title" autocomplete="off">
      <button class="clear-x" id="clearSearch" aria-label="Clear search">✕</button>
    </div>

    <button class="btn ghost" id="themeBtn" title="Toggle light / dark theme" aria-label="Toggle theme">
      <svg id="themeIcon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
        <circle cx="12" cy="12" r="4.2"></circle>
        <path d="M12 2.5v2M12 19.5v2M2.5 12h2M19.5 12h2M5.2 5.2l1.4 1.4M17.4 17.4l1.4 1.4M18.8 5.2l-1.4 1.4M6.6 17.4l-1.4 1.4"></path>
      </svg>
      <span class="label" id="themeLabel">Dark</span>
    </button>

    <button class="btn primary" id="addBtn">
      <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M12 5v14M5 12h14"></path></svg>
      <span class="label">New card</span>
    </button>
  </header>

  <main class="board" id="board">
    <section class="column" data-col="todo">
      <div class="col-head">
        <span class="dot" style="background:var(--c-todo)"></span>
        <span class="col-title">To do</span>
        <span class="count" data-count="todo">0</span>
        <button class="icon-btn col-add" data-add="todo" title="Add card to To do" aria-label="Add card to To do">
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M12 5v14M5 12h14"></path></svg>
        </button>
      </div>
      <div class="card-list" data-list="todo"></div>
    </section>

    <section class="column" data-col="doing">
      <div class="col-head">
        <span class="dot" style="background:var(--c-doing)"></span>
        <span class="col-title">In progress</span>
        <span class="count" data-count="doing">0</span>
        <button class="icon-btn col-add" data-add="doing" title="Add card to In progress" aria-label="Add card to In progress">
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M12 5v14M5 12h14"></path></svg>
        </button>
      </div>
      <div class="card-list" data-list="doing"></div>
    </section>

    <section class="column" data-col="done">
      <div class="col-head">
        <span class="dot" style="background:var(--c-done)"></span>
        <span class="col-title">Done</span>
        <span class="count" data-count="done">0</span>
        <button class="icon-btn col-add" data-add="done" title="Add card to Done" aria-label="Add card to Done">
          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round"><path d="M12 5v14M5 12h14"></path></svg>
        </button>
      </div>
      <div class="card-list" data-list="done"></div>
    </section>
  </main>

  <p class="hint-bar">Tip: drag a card with the mouse, or use the <kbd>‹</kbd> <kbd>›</kbd> buttons on a card to move it between columns.</p>
</div>

<!-- Modal -->
<div class="backdrop" id="backdrop" hidden>
  <div class="modal" role="dialog" aria-modal="true" aria-labelledby="modalTitle">
    <h2 id="modalTitle">Add a new card</h2>
    <p class="hint">Give it a short, actionable title.</p>
    <form id="cardForm" novalidate>
      <label><span>Title <b style="color:var(--high)">*</b></span>
        <input class="field" id="fTitle" type="text" placeholder="e.g. Design empty state illustrations" maxlength="90" autocomplete="off">
      </label>
      <label><span>Priority</span>
        <select class="field" id="fPriority">
          <option value="low">Low</option>
          <option value="medium" selected>Medium</option>
          <option value="high">High</option>
        </select>
      </label>
      <label><span>Column</span>
        <select class="field" id="fColumn">
          <option value="todo">To do</option>
          <option value="doing">In progress</option>
          <option value="done">Done</option>
        </select>
      </label>
      <p class="err" id="formErr"></p>
      <div class="modal-actions">
        <button type="button" class="btn ghost" id="cancelBtn">Cancel</button>
        <button type="submit" class="btn primary">Add card</button>
      </div>
    </form>
  </div>
</div>

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

  var COLS = [
    {key:'todo',  label:'To do'},
    {key:'doing', label:'In progress'},
    {key:'done',  label:'Done'}
  ];

  var state = {
    lists: {
      todo: [
        {id:'c1', title:'Redesign onboarding email sequence', prio:'high'},
        {id:'c2', title:'Audit colour contrast on settings page', prio:'medium'},
        {id:'c3', title:'Write release notes for v2.4', prio:'low'}
      ],
      doing: [
        {id:'c4', title:'Integrate Stripe billing webhooks', prio:'high'},
        {id:'c5', title:'Prototype drag-and-drop kanban view', prio:'medium'}
      ],
      done: [
        {id:'c6', title:'Set up CI pipeline with preview deploys', prio:'medium'},
        {id:'c7', title:'Fix timezone bug in activity feed', prio:'low'}
      ]
    },
    query: '',
    theme: 'light'
  };

  var board = document.getElementById('board');
  var searchInput = document.getElementById('search');
  var searchWrap = document.getElementById('searchWrap');
  var clearBtn = document.getElementById('clearSearch');
  var backdrop = document.getElementById('backdrop');
  var form = document.getElementById('cardForm');
  var fTitle = document.getElementById('fTitle');
  var fPriority = document.getElementById('fPriority');
  var fColumn = document.getElementById('fColumn');
  var formErr = document.getElementById('formErr');
  var themeBtn = document.getElementById('themeBtn');
  var themeLabel = document.getElementById('themeLabel');
  var themeIcon = document.getElementById('themeIcon');
  var uid = 100;

  /* ---------------- rendering ---------------- */
  function matches(c, q){
    return c.title.toLowerCase().indexOf(q) !== -1;
  }

  function makeCard(c, colKey, index){
    var el = document.createElement('article');
    el.className = 'card';
    el.draggable = true;
    el.dataset.id = c.id;
    el.style.animationDelay = (index * 45) + 'ms';

    var top = document.createElement('div');
    top.className = 'card-top';
    var badge = document.createElement('span');
    badge.className = 'badge ' + c.prio;
    badge.textContent = c.prio;
    var del = document.createElement('button');
    del.className = 'icon-btn danger del';
    del.title = 'Delete card';
    del.setAttribute('aria-label','Delete card');
    del.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M6 6l12 12M18 6L6 18"></path></svg>';
    top.appendChild(badge);
    top.appendChild(del);

    var h = document.createElement('h3');
    h.className = 'card-title';
    h.textContent = c.title;

    var foot = document.createElement('div');
    foot.className = 'card-foot';
    var tag = document.createElement('span');
    tag.className = 'tag';
    tag.textContent = '#' + c.id.slice(1);
    var movers = document.createElement('span');
    movers.className = 'movers';
    movers.innerHTML = '<button class="icon-btn mv" data-dir="-1" title="Move left" aria-label="Move to previous column">‹</button>' +
                      '<button class="icon-btn mv" data-dir="1" title="Move right" aria-label="Move to next column">›</button>';
    foot.appendChild(tag);
    foot.appendChild(movers);

    el.appendChild(top);
    el.appendChild(h);
    el.appendChild(foot);
    return el;
  }

  function render(){
    var q = state.query.trim().toLowerCase();
    var anyVisible = false;

    COLS.forEach(function(col){
      var list = document.querySelector('.card-list[data-list="' + col.key + '"]');
      var items = state.lists[col.key];
      var visible = q ? items.filter(function(c){ return matches(c, q); }) : items.slice();
      var countEl = document.querySelector('.count[data-count="' + col.key + '"]');

      list.innerHTML = '';
      if (visible.length){
        visible.forEach(function(c, i){ list.appendChild(makeCard(c, col.key, i)); });
        anyVisible = true;
      } else if (q){
        var e = document.createElement('div');
        e.className = 'empty';
        e.textContent = 'No matching cards';
        list.appendChild(e);
      } else {
        var e2 = document.createElement('div');
        e2.className = 'empty';
        e2.textContent = 'Nothing here yet';
        list.appendChild(e2);
      }

      if (q && visible.length !== items.length){
        countEl.textContent = visible.length + '/' + items.length;
        countEl.classList.add('filtered');
      } else {
        countEl.textContent = items.length;
        countEl.classList.remove('filtered');
      }
    });

    if (!anyVisible && q){
      // nothing matched anywhere: keep per-column empties, that's fine
    }
  }

  /* ---------------- card operations ---------------- */
  function findCard(id){
    for (var i = 0; i < COLS.length; i++){
      var k = COLS[i].key, l = state.lists[k];
      for (var j = 0; j < l.length; j++) if (l[j].id === id) return {card:l[j], col:k, index:j};
    }
    return null;
  }

  function removeCard(id){
    COLS.forEach(function(col){
      var l = state.lists[col.key];
      for (var i = l.length - 1; i >= 0; i--) if (l[i].id === id) l.splice(i, 1);
    });
  }

  function moveCardToColumn(id, targetKey, position){
    var found = findCard(id);
    if (!found) return;
    removeCard(id);
    var l = state.lists[targetKey];
    if (typeof position === 'number' && position >= 0){
      l.splice(Math.min(position, l.length), 0, found.card);
    } else {
      l.push(found.card);
    }
    render();
  }

  /* ---------------- drag & drop ---------------- */
  var dragId = null;
  var indicator = null;

  function getIndicator(){
    if (!indicator){
      indicator = document.createElement('div');
      indicator.className = 'drop-indicator';
      indicator.textContent = 'Drop here';
    }
    return indicator;
  }
  function removeIndicator(){
    if (indicator && indicator.parentNode) indicator.parentNode.removeChild(indicator);
  }
  function afterElement(list, y){
    var cards = [].slice.call(list.querySelectorAll('.card:not(.dragging)'));
    for (var i = 0; i < cards.length; i++){
      var box = cards[i].getBoundingClientRect();
      var offset = y - box.top - box.height / 2;
      if (offset < 0) return cards[i];
    }
    return null;
  }

  board.addEventListener('dragstart', function(e){
    var card = e.target.closest ? e.target.closest('.card') : null;
    if (!card) return;
    dragId = card.dataset.id;
    try {
      e.dataTransfer.effectAllowed = 'move';
      e.dataTransfer.setData('text/plain', dragId);
    } catch(err){}
    setTimeout(function(){ card.classList.add('dragging'); }, 0);
  });

  board.addEventListener('dragend', function(e){
    var card = e.target.closest ? e.target.closest('.card') : null;
    if (card) card.classList.remove('dragging');
    removeIndicator();
    document.querySelectorAll('.column.drag-over').forEach(function(c){ c.classList.remove('drag-over'); });
    dragId = null;
  });

  board.addEventListener('dragover', function(e){
    var list = e.target.closest ? e.target.closest('.card-list') : null;
    if (!list) return;
    e.preventDefault();
    try { e.dataTransfer.dropEffect = 'move'; } catch(err){}

    document.querySelectorAll('.column.drag-over').forEach(function(c){ c.classList.remove('drag-over'); });
    list.closest('.column').classList.add('drag-over');

    var ind = getIndicator();
    var after = afterElement(list, e.clientY);
    if (after){
      if (ind.parentNode !== list || ind.nextSibling !== after) list.insertBefore(ind, after);
    } else {
      if (ind.parentNode !== list || ind.nextSibling) list.appendChild(ind);
    }
  });

  board.addEventListener('dragleave', function(e){
    var list = e.target.closest ? e.target.closest('.card-list') : null;
    if (list && !list.contains(e.relatedTarget)){
      list.closest('.column').classList.remove('drag-over');
    }
  });

  board.addEventListener('drop', function(e){
    var list = e.target.closest ? e.target.closest('.card-list') : null;
    if (!list) return;
    e.preventDefault();
    var id = dragId;
    try { id = e.dataTransfer.getData('text/plain') || dragId; } catch(err){}
    removeIndicator();
    document.querySelectorAll('.column.drag-over').forEach(function(c){ c.classList.remove('drag-over'); });
    if (!id) return;

    var targetKey = list.dataset.list;
    var cards = [].slice.call(list.querySelectorAll('.card'));
    var draggingEl = null;
    for (var i = 0; i < cards.length; i++) if (cards[i].dataset.id === id) draggingEl = cards[i];

    // position index in the target list (DOM order of existing cards in that list)
    var domOrder = [].slice.call(list.querySelectorAll('.card')).map(function(c){ return c.dataset.id; });
    var position = domOrder.indexOf(id);
    if (position < 0) position = state.lists[targetKey].length;

    moveCardToColumn(id, targetKey, position);
  });

  /* ---------------- clicks on cards ---------------- */
  board.addEventListener('click', function(e){
    var addBtn = e.target.closest && e.target.closest('[data-add]');
    if (addBtn){ openModal(addBtn.dataset.add); return; }

    var del = e.target.closest && e.target.closest('.del');
    if (del){
      var card = del.closest('.card');
      var id = card.dataset.id;
      var el = card;
      el.style.transition = 'opacity .18s ease, transform .18s ease';
      el.style.opacity = '0';
      el.style.transform = 'scale(.95)';
      setTimeout(function(){
        removeCard(id);
        render();
      }, 160);
      return;
    }

    var mv = e.target.closest && e.target.closest('.mv');
    if (mv){
      var c = mv.closest('.card');
      var found = findCard(c.dataset.id);
      if (!found) return;
      var idx = COLS.findIndex(function(col){ return col.key === found.col; });
      var next = idx + parseInt(mv.dataset.dir, 10);
      if (next < 0 || next >= COLS.length) return;
      moveCardToColumn(c.dataset.id, COLS[next].key, null);
      return;
    }
  });

  /* ---------------- search ---------------- */
  searchInput.addEventListener('input', function(){
    state.query = searchInput.value;
    searchWrap.classList.toggle('has-query', !!state.query);
    render();
  });
  clearBtn.addEventListener('click', function(){
    searchInput.value = '';
    state.query = '';
    searchWrap.classList.remove('has-query');
    render();
    searchInput.focus();
  });

  /* ---------------- theme ---------------- */
  function applyTheme(){
    document.documentElement.setAttribute('data-theme', state.theme);
    themeLabel.textContent = state.theme === 'light' ? 'Dark' : 'Light';
    themeIcon.innerHTML = state.theme === 'light'
      ? '<circle cx="12" cy="12" r="4.2"></circle><path d="M12 2.5v2M12 19.5v2M2.5 12h2M19.5 12h2M5.2 5.2l1.4 1.4M17.4 17.4l1.4 1.4M18.8 5.2l-1.4 1.4M6.6 17.4l-1.4 1.4"></path>'
      : '<path d="M20.5 14.5A8.5 8.5 0 1 1 9.5 3.5a7 7 0 0 0 11 11z"></path>';
  }
  themeBtn.addEventListener('click', function(){
    state.theme = state.theme === 'light' ? 'dark' : 'light';
    applyTheme();
  });
  applyTheme();

  /* ---------------- modal ---------------- */
  var lastFocus = null;
  function openModal(col){
    lastFocus = document.activeElement;
    fColumn.value = col || 'todo';
    fPriority.value = 'medium';
    fTitle.value = '';
    formErr.textContent = '';
    backdrop.hidden = false;
    setTimeout(function(){ fTitle.focus(); }, 30);
  }
  function closeModal(){
    backdrop.hidden = true;
    if (lastFocus && lastFocus.focus) lastFocus.focus();
  }
  document.getElementById('addBtn').addEventListener('click', function(){ openModal('todo'); });
  document.getElementById('cancelBtn').addEventListener('click', closeModal);
  backdrop.addEventListener('mousedown', function(e){ if (e.target === backdrop) closeModal(); });
  document.addEventListener('keydown', function(e){
    if (e.key === 'Escape' && !backdrop.hidden) closeModal();
  });
  form.addEventListener('submit', function(e){
    e.preventDefault();
    var title = fTitle.value.trim();
    if (!title){
      formErr.textContent = 'Please enter a title for the card.';
      fTitle.focus();
      return;
    }
    if (title.length < 2){
      formErr.textContent = 'Title is too short — use at least 2 characters.';
      fTitle.focus();
      return;
    }
    state.lists[fColumn.value].push({
      id: 'c' + (++uid),
      title: title,
      prio: fPriority.value
    });
    render();
    closeModal();
  });

  /* ---------------- init ---------------- */
  render();
})();
</script>
</body>
</html>
Reasoning trace (2,416 tokens)
Let me build a polished Kanban board.

Key features: three columns, counts, 6 pre-populated cards, priority badges, add-card modal with required title, delete button, HTML5 drag & drop with drop indicator, live search filter, theme toggle, in-memory state.

Let me write clean code with nice CSS.

Design: header with title, search input, theme toggle, "New card" button. Board with 3 columns. Cards draggable.

Drop indicator: a placeholder element inserted in the column showing where the card will land.

Theme: light/dark via data-theme attribute on html/body; CSS variables.

Let me write it carefully.

Drag and drop: use dragstart on card, dragover on column -> compute insertion index and move placeholder. drop -> insert card into data.

Also touch support? Not required, but could add via pointer events fallback. Keep simple but maybe add click-to-move buttons for accessibility? Could add small "move" buttons. Let's add keyboard-friendly move arrows maybe. Keep moderate.

Modal: overlay with form, title input required, priority select, column select. Focus trap basic, ESC closes, backdrop click closes.

Let me write HTML.

Card data: id, title, priority, column.

Counts: number of cards per column, including while filtering (show filtered count). Show "3 of 5" when searching maybe. Just show count of visible cards and total if different.

Let's produce.

CSS variables for light/dark.

I'll write the code now.

Details on drag indicator: a div.drag-placeholder with dashed border and height matching card.

Implementation plan JS:

const state = { cards: [...], query:'', theme:'light' };

render() builds columns inner HTML.

Cards draggable attribute draggable="true" with dragstart handler setting dataTransfer, adding class dragging.

Column list: on dragover preventDefault, compute afterElement via getDragAfterElement(list, y), insert placeholder.

On drop: find card element from dataTransfer, insert card at placeholder position, update state model, re-render.

Simpler: maintain DOM order as source of truth but also update state. I'll update state from DOM at drop time: read each column's card ids in order.

Let me implement: cards rendered in column containers as `.card` elements with data-id. On drop, for each column, read ids of children (excluding placeholder), rebuild state order and column assignment.

Search filter: on input, set state.query, re-render but preserve? Re-render is fine.

To avoid re-render during drag, keep drag data in JS variable.

Let's write.

Also add a "double click card to edit"? Not required. Add priority badge.

Add empty column message.

Header stats: total cards.

Theme toggle button with sun/moon icon (inline SVG).

Let's write final code.

For first paint screenshot: cards visible. Modal hidden. Fine.

Write code.

Column color accents: todo gray/blue, in progress amber, done green.

Card styling: white surface, subtle shadow, radius 12, priority badge top-right small, title text, footer with priority label maybe. Delete button appears on hover (but should be visible enough; maybe always visible in footer). I'll put delete button in footer always visible but subtle.

Let me write CSS:

:root vars light:
--bg: #f4f6fb; --surface:#fff; --text:#16202e; --muted:#64748b; --border:#e2e8f0; --accent:#4f46e5; --shadow...

[data-theme="dark"]: --bg:#0f141b; --surface:#171e28; --text:#e6edf5; --muted:#94a3b8; --border:#243040;

Priority colors: low #10b981 green, medium #f59e0b amber, high #ef4444 red. Badge with tinted background: use color-mix or rgba with opacity: background: color-mix(in srgb, var(--p) 15%, transparent). color-mix is supported in modern Chrome. Fine.

I'll define per-priority CSS vars on badge classes.

Now write the full file.

Modal markup:

<div class="modal-backdrop" id="modal" hidden>
 <div class="modal" role="dialog" aria-modal="true" aria-labelledby="mTitle">
  <form id="cardForm">
   <h2 id="mTitle">New card</h2>
   <label>Title <input id="fTitle" required maxlength="80" placeholder="e.g. Fix login redirect"></label>
   <label>Priority <select id="fPrio"><option value="low">Low</option>...</select></label>
   <label>Column <select id="fCol">...</select></label>
   <p class="err" id="err"></p>
   <div class="actions"><button type="button" class="btn ghost" id="cancelBtn">Cancel</button><button type="submit" class="btn primary">Add card</button></div>
  </form>
 </div>
</div>

Use required attribute + custom validation message.

Also keyboard: Escape closes modal; Enter submits.

Add "Click or press a key to start"? That's for games. Not needed, but interaction works anyway. I'll add a subtle hint in header? Not necessary.

Write it.

Drag handle: whole card draggable.

Also implement pointer-based drag fallback? Skip; mouse drag with HTML5 DnD works in Chrome. But note: HTML5 drag and drop in iframe should work fine.

Also add move-left/right buttons on card footer for non-drag interaction (nice, and helps if DnD blocked). I'll add small arrow buttons to move card between columns. Good extra.

Let's write final code now.

For drop indicator: placeholder div with class "drop-indicator" showing text? A dashed rounded rect ~ height of card, with text "Drop here".

Implementation of getDragAfterElement: iterate children of column list that are .card:not(.dragging), compare midpoints.

Let me code render():

function render(){
  document.querySelectorAll('.column').forEach(col=>{
    const id = col.dataset.col;
    const list = col.querySelector('.card-list');
    const items = state.cards.filter(c=>c.col===id);
    const q = state.query.trim().toLowerCase();
    const visible = items.filter(c=>!q || c.title.toLowerCase().includes(q));
    list.innerHTML='';
    visible.forEach(c=> list.appendChild(makeCard(c)));
    count badge: col.querySelector('.count').textContent = q? visible.length+'/'+items.length : items.length;
    empty message if visible.length===0.
  });
}

But dragging re-render would kill it; I'll re-render only after drop.

makeCard builds element via innerHTML template with escaped title. Use textContent for title to avoid escaping issues. I'll create elements programmatically-ish with template string then set title via textContent after.

Let me write helper esc() for safety with innerHTML, but also set via textContent. I'll just use DOM creation:

function makeCard(c){
 const el = document.createElement('article');
 el.className='card'; el.draggable=true; el.dataset.id=c.id;
 el.innerHTML = `
  <div class="card-top">
    <span class="badge ${c.prio}">${c.prio}</span>
    <button class="icon-btn del" title="Delete card" aria-label="Delete card">✕</button>
  </div>
  <h3 class="card-title"></h3>
  <div class="card-foot">
    <span class="tag">${c.colLabel}</span>
    <span class="movers"><button class="icon-btn mv" data-dir="-1">‹</button><button class="icon-btn mv" data-dir="1">›</button></span>
  </div>`;
 el.querySelector('.card-title').textContent = c.title;
 ...
}

Handle delete via delegation on board click.

Column label mapping: todo->'To do', doing->'In progress', done->'Done'.

Move buttons: move card to previous/next column.

OK, write it all out.

Drag events:
board.addEventListener('dragstart', e=>{ const card=e.target.closest('.card'); if(!card) return; dragId=card.dataset.id; e.dataTransfer.effectAllowed='move'; e.dataTransfer.setData('text/plain', dragId); card.classList.add('dragging'); });

dragover on .card-list: e.preventDefault(); e.dataTransfer.dropEffect='move'; show placeholder.

dragend: remove placeholder, remove class.

drop: e.preventDefault(); insert real card before placeholder, then update state from DOM.

updateStateFromDOM(): for each column list, iterate children .card, build array of ids in order; assign state cards col & order. Simplest: rebuild state.cards array ordered by column order then DOM order.

I'll keep state.cards as ordered array; reorder per column.

Let me implement:

function syncFromDOM(){
 const out=[];
 document.querySelectorAll('.column').forEach(col=>{
   const cid=col.dataset.col;
   col.querySelectorAll('.card-list > .card').forEach(el=>{
     const c = state.cards.find(x=>x.id===el.dataset.id);
     if(c){ c.col=cid; out.push(c);}
   });
 });
 // keep cards not in DOM? all should be in DOM
 state.cards = out;
}

Fine since search filter may hide cards — but during drag, search likely empty. If search active, hidden cards get dropped from state! Guard: only sync if no query. Otherwise skip hidden ones: collect ids present in DOM, keep others in their original column appended. I'll do: after building out, add remaining state.cards not included at end of their column. Simpler: only allow sync when state.query empty; else keep. Actually I'll handle: build map id->card; for each column, DOM order ids first, then remaining cards of that column not in DOM (hidden by filter). Good.

Order array per column: store cards grouped. I'll maintain state.cards as array; order matters per column. Let me restructure: state.columns = {todo:[], doing:[], done:[]} arrays of card objects, plus state.cards lookup. Simpler to just have state.lists = {todo:[{...}],...}. Filtering uses flatten.

Let's define:
const state = { lists: { todo:[], doing:[], done:[] }, query:'', theme:'light' };

Counts = lists[col].length; visible = filtered.

render for each col: visible cards from list.

sync from DOM: for each col, ids in DOM order -> reorder list: newList = domIds.map(id=>find) concat remaining not in domIds.

Good.

Also add card: push to selected column list.

Delete: remove from list.

Now write CSS fully with nice design. Let me make header sticky, board grid 3 cols, responsive: at <900px stack into one column with horizontal? Requirement: no horizontal scrollbar; stack vertically.

Let me write the code.

Extra polish: column header has colored dot, count pill, and an "+" button to add card to that column (opens modal with column preselected).

Time to write final answer.

Also top-of-page a small "key" hint? Not needed.

Add subtle animated entrance for cards (fade-up) — nice for screenshot but screenshot at 3s, animation done.

Also add drag image? default fine.

Write it.