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
1 min 28 s
Cost
$0.012
Output tokens
19,968
Spent thinking
6,490 tokens
Speed
230.1 tok/s
Console errors
0
First token after
844 ms
First visible text after
47.7 s
Prompt tokens
665
Code size
45.4 KB
Finish reason
stop
Served by
DigitalOcean
Model id
minimax/minimax-m3.1-flash
Reasoning sent
{"effort":"high"}
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 · 1332 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>Flowboard — Product Roadmap</title>
  <style>
    :root {
      color-scheme: light;
      --bg: #f5f6f9;
      --surface: #ffffff;
      --surface-2: #fafbfc;
      --sidebar: #171a22;
      --sidebar-soft: #222631;
      --sidebar-text: #f5f6f8;
      --sidebar-muted: #969daa;
      --text: #1c1f27;
      --muted: #737b8a;
      --muted-2: #9aa1ad;
      --border: #e4e7ec;
      --column-bg: #eef0f4;
      --column-border: #e2e5ea;
      --card: #ffffff;
      --primary: #5d57d9;
      --primary-hover: #4e48c9;
      --primary-soft: #eeedff;
      --drop: #5d57d9;
      --green: #23966f;
      --green-soft: #e6f6ef;
      --amber: #c17a12;
      --amber-soft: #fff2d9;
      --red: #d44b55;
      --red-soft: #ffeaec;
      --blue: #3478d4;
      --blue-soft: #e9f1ff;
      --shadow: 0 1px 2px rgba(20, 24, 35, .04), 0 5px 16px rgba(20, 24, 35, .04);
      --shadow-lg: 0 20px 60px rgba(18, 21, 31, .2);
    }

    :root[data-theme="dark"] {
      color-scheme: dark;
      --bg: #111319;
      --surface: #191c23;
      --surface-2: #1d2028;
      --sidebar: #0d0f14;
      --sidebar-soft: #1b1e26;
      --sidebar-text: #f2f4f8;
      --sidebar-muted: #9299a8;
      --text: #edf0f6;
      --muted: #9ca4b3;
      --muted-2: #737b8b;
      --border: #2a2e38;
      --column-bg: #171a21;
      --column-border: #262a34;
      --card: #20242d;
      --primary: #8179f0;
      --primary-hover: #918af5;
      --primary-soft: #2a2852;
      --drop: #8d85f4;
      --green: #55c499;
      --green-soft: #183b30;
      --amber: #e8aa4b;
      --amber-soft: #44351d;
      --red: #f07a82;
      --red-soft: #48262b;
      --blue: #73a9f5;
      --blue-soft: #203653;
      --shadow: 0 1px 2px rgba(0, 0, 0, .18), 0 8px 20px rgba(0, 0, 0, .1);
      --shadow-lg: 0 24px 70px rgba(0, 0, 0, .48);
    }

    * { box-sizing: border-box; }

    html, body {
      margin: 0;
      height: 100%;
      font-family: Inter, ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
      background: var(--bg);
      color: var(--text);
      -webkit-font-smoothing: antialiased;
    }

    body { overflow: hidden; }
    button, input, select { font: inherit; }
    button { color: inherit; }
    [hidden] { display: none !important; }

    .icon {
      width: 18px;
      height: 18px;
      flex: 0 0 auto;
      fill: none;
      stroke: currentColor;
      stroke-width: 1.8;
      stroke-linecap: round;
      stroke-linejoin: round;
    }

    .app {
      display: grid;
      grid-template-columns: 236px minmax(0, 1fr);
      height: 100vh;
      height: 100dvh;
    }

    /* Sidebar */
    .sidebar {
      background: var(--sidebar);
      color: var(--sidebar-text);
      padding: 22px 14px 16px;
      display: flex;
      flex-direction: column;
      min-height: 0;
      border-right: 1px solid rgba(255,255,255,.04);
    }

    .brand {
      display: flex;
      align-items: center;
      gap: 11px;
      padding: 0 9px 25px;
      font-weight: 750;
      font-size: 18px;
      letter-spacing: -.3px;
    }

    .brand-mark {
      width: 34px;
      height: 34px;
      border-radius: 10px;
      display: grid;
      place-items: center;
      color: white;
      background: linear-gradient(145deg, #7a70ee, #5148c9);
      box-shadow: 0 7px 18px rgba(93, 87, 217, .34);
    }

    .brand-mark .icon { width: 20px; height: 20px; }

    .nav-label {
      color: var(--sidebar-muted);
      font-size: 10px;
      font-weight: 750;
      letter-spacing: 1.3px;
      text-transform: uppercase;
      padding: 0 11px;
      margin: 8px 0 8px;
    }

    .nav-list { display: grid; gap: 3px; }

    .nav-item {
      height: 40px;
      display: flex;
      align-items: center;
      gap: 11px;
      padding: 0 11px;
      border-radius: 9px;
      color: var(--sidebar-muted);
      font-size: 13.5px;
      font-weight: 560;
      cursor: default;
    }

    .nav-item:hover { background: rgba(255,255,255,.045); color: var(--sidebar-text); }
    .nav-item.active { background: var(--sidebar-soft); color: var(--sidebar-text); }
    .nav-item.active .icon { color: #918af5; }
    .nav-count {
      margin-left: auto;
      font-size: 11px;
      font-weight: 700;
      color: var(--sidebar-muted);
      background: rgba(255,255,255,.07);
      min-width: 23px;
      text-align: center;
      padding: 2px 6px;
      border-radius: 20px;
    }
    .nav-item.active .nav-count { color: #d5d1ff; background: rgba(129,121,240,.18); }

    .sidebar-divider { height: 1px; background: rgba(255,255,255,.07); margin: 18px 8px; }

    .workspace {
      display: flex;
      align-items: center;
      gap: 10px;
      padding: 9px 10px;
      border-radius: 9px;
      font-size: 13px;
      font-weight: 580;
      color: #c9cdd6;
    }
    .workspace:hover { background: rgba(255,255,255,.04); }
    .workspace-dot { width: 8px; height: 8px; border-radius: 3px; background: #f09a54; }
    .workspace-dot.blue { background: #5c9cf0; }
    .workspace-dot.green { background: #54b88b; }

    .sidebar-spacer { flex: 1; }

    .upgrade-card {
      margin: 14px 3px 15px;
      padding: 15px;
      border: 1px solid rgba(255,255,255,.08);
      background: linear-gradient(145deg, rgba(124,113,240,.15), rgba(255,255,255,.025));
      border-radius: 13px;
    }
    .upgrade-card strong { display: block; font-size: 13px; margin-bottom: 5px; }
    .upgrade-card p { color: var(--sidebar-muted); font-size: 11.5px; line-height: 1.45; margin: 0 0 11px; }
    .upgrade-card button {
      border: 0;
      background: #7770e5;
      color: white;
      padding: 7px 11px;
      border-radius: 7px;
      font-size: 11px;
      font-weight: 700;
      cursor: pointer;
    }

    .profile {
      display: flex;
      align-items: center;
      gap: 10px;
      padding: 12px 8px 2px;
      border-top: 1px solid rgba(255,255,255,.07);
    }
    .avatar {
      width: 32px;
      height: 32px;
      flex: 0 0 auto;
      border-radius: 50%;
      display: grid;
      place-items: center;
      font-size: 10.5px;
      font-weight: 800;
      color: #353047;
      background: linear-gradient(145deg, #f1c7a6, #d8a1cb);
      border: 2px solid rgba(255,255,255,.12);
    }
    .profile-name { font-size: 12.5px; font-weight: 650; }
    .profile-role { color: var(--sidebar-muted); font-size: 10.5px; margin-top: 2px; }
    .profile .icon { margin-left: auto; width: 15px; color: var(--sidebar-muted); }

    /* Main */
    .main {
      min-width: 0;
      min-height: 0;
      display: flex;
      flex-direction: column;
      background: var(--bg);
    }

    .topbar {
      height: 66px;
      flex: 0 0 auto;
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 22px;
      padding: 0 30px;
      background: var(--surface);
      border-bottom: 1px solid var(--border);
    }

    .breadcrumbs { display: flex; align-items: center; gap: 9px; color: var(--muted); font-size: 13px; white-space: nowrap; }
    .breadcrumbs strong { color: var(--text); font-weight: 650; }
    .breadcrumbs .icon { width: 14px; height: 14px; color: var(--muted-2); }

    .topbar-tools { display: flex; align-items: center; gap: 9px; min-width: 0; }

    .search {
      position: relative;
      display: flex;
      align-items: center;
      width: min(380px, 35vw);
      height: 38px;
      background: var(--bg);
      border: 1px solid var(--border);
      border-radius: 10px;
      transition: border-color .2s, box-shadow .2s, background .2s;
    }
    .search:focus-within {
      border-color: var(--primary);
      box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 15%, transparent);
      background: var(--surface);
    }
    .search > .icon { margin-left: 11px; color: var(--muted); width: 16px; }
    .search input {
      width: 100%;
      min-width: 0;
      height: 100%;
      padding: 0 8px 0 9px;
      border: 0;
      outline: 0;
      color: var(--text);
      background: transparent;
      font-size: 13px;
    }
    .search input::placeholder { color: var(--muted-2); }
    .search kbd {
      margin-right: 8px;
      border: 1px solid var(--border);
      background: var(--surface);
      color: var(--muted-2);
      border-radius: 5px;
      padding: 2px 6px;
      font: 11px inherit;
      box-shadow: 0 1px 0 var(--border);
    }
    .search-clear {
      border: 0;
      background: transparent;
      padding: 4px;
      margin-right: 5px;
      display: grid;
      place-items: center;
      color: var(--muted);
      cursor: pointer;
      border-radius: 5px;
    }
    .search-clear:hover { color: var(--text); background: var(--border); }
    .search-clear .icon { width: 14px; height: 14px; }

    .icon-button {
      width: 38px;
      height: 38px;
      display: grid;
      place-items: center;
      border-radius: 9px;
      border: 1px solid var(--border);
      background: var(--surface);
      color: var(--muted);
      cursor: pointer;
      transition: background .18s, color .18s, transform .18s;
    }
    .icon-button:hover { background: var(--bg); color: var(--text); }
    .icon-button:active { transform: scale(.96); }
    .notification-button { position: relative; }
    .notification-button::after {
      content: "";
      position: absolute;
      width: 6px;
      height: 6px;
      border-radius: 50%;
      background: #ef6a72;
      border: 2px solid var(--surface);
      top: 7px;
      right: 7px;
    }
    .theme-button .sun-icon { display: none; }
    :root[data-theme="dark"] .theme-button .moon-icon { display: none; }
    :root[data-theme="dark"] .theme-button .sun-icon { display: block; }
    .mobile-brand { display: none; align-items: center; gap: 8px; font-weight: 750; }
    .mobile-brand .brand-mark { width: 29px; height: 29px; border-radius: 8px; }
    .mobile-brand .brand-mark .icon { width: 17px; height: 17px; }

    .content {
      flex: 1;
      min-height: 0;
      display: flex;
      flex-direction: column;
      padding: 25px 30px 0;
    }

    .page-heading {
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 20px;
      margin-bottom: 22px;
      flex: 0 0 auto;
    }
    .eyebrow {
      color: var(--primary);
      font-size: 10px;
      font-weight: 800;
      letter-spacing: 1.25px;
      text-transform: uppercase;
      margin-bottom: 7px;
    }
    .title-line { display: flex; align-items: center; gap: 11px; }
    h1 {
      margin: 0;
      font-size: 27px;
      line-height: 1.15;
      letter-spacing: -.8px;
      font-weight: 760;
    }
    .total-pill {
      color: var(--muted);
      background: var(--surface);
      border: 1px solid var(--border);
      padding: 4px 8px;
      border-radius: 20px;
      font-size: 11px;
      font-weight: 650;
    }
    .page-subtitle { color: var(--muted); font-size: 13px; margin: 7px 0 0; }

    .primary-button {
      height: 40px;
      padding: 0 16px;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      gap: 8px;
      border: 0;
      border-radius: 10px;
      background: var(--primary);
      color: #fff;
      font-size: 13px;
      font-weight: 700;
      cursor: pointer;
      box-shadow: 0 4px 12px color-mix(in srgb, var(--primary) 22%, transparent);
      transition: background .18s, transform .18s, box-shadow .18s;
      white-space: nowrap;
    }
    .primary-button:hover { background: var(--primary-hover); transform: translateY(-1px); }
    .primary-button:active { transform: translateY(0); }
    .primary-button .icon { width: 16px; height: 16px; stroke-width: 2.2; }

    /* Board */
    .board {
      flex: 1;
      min-height: 0;
      overflow-y: auto;
      overflow-x: hidden;
      display: grid;
      grid-template-columns: repeat(3, minmax(0, 1fr));
      gap: 18px;
      padding-bottom: 28px;
      align-items: stretch;
      scrollbar-width: thin;
      scrollbar-color: var(--border) transparent;
    }

    .column {
      min-width: 0;
      min-height: 100%;
      display: flex;
      flex-direction: column;
      border: 1px solid var(--column-border);
      background: var(--column-bg);
      border-radius: 15px;
      transition: border-color .18s, background .18s, box-shadow .18s;
    }
    .column.drag-over {
      border-color: var(--primary);
      background: color-mix(in srgb, var(--primary) 7%, var(--column-bg));
      box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 10%, transparent);
    }
    .column-head {
      display: flex;
      align-items: center;
      gap: 9px;
      padding: 15px 15px 12px;
      flex: 0 0 auto;
    }
    .column-dot { width: 8px; height: 8px; border-radius: 3px; background: var(--amber); }
    .column.progress .column-dot { background: var(--blue); }
    .column.done .column-dot { background: var(--green); }
    .column-title { font-size: 13px; font-weight: 750; letter-spacing: -.1px; }
    .column-count {
      min-width: 23px;
      height: 21px;
      display: grid;
      place-items: center;
      padding: 0 7px;
      border-radius: 7px;
      background: var(--surface);
      border: 1px solid var(--border);
      color: var(--muted);
      font-size: 10.5px;
      font-weight: 750;
    }
    .column-add {
      margin-left: auto;
      width: 27px;
      height: 27px;
      border: 0;
      background: transparent;
      color: var(--muted);
      display: grid;
      place-items: center;
      border-radius: 7px;
      cursor: pointer;
    }
    .column-add:hover { background: var(--surface); color: var(--text); }
    .column-add .icon { width: 16px; height: 16px; }

    .column-list {
      position: relative;
      display: flex;
      flex-direction: column;
      gap: 10px;
      padding: 1px 11px 13px;
      min-height: 120px;
      flex: 1;
    }

    .drop-indicator {
      position: absolute;
      left: 12px;
      right: 12px;
      height: 3px;
      top: 0;
      border-radius: 10px;
      background: var(--drop);
      box-shadow: 0 0 0 2px color-mix(in srgb, var(--drop) 14%, transparent);
      opacity: 0;
      pointer-events: none;
      z-index: 5;
      transition: top .08s linear, opacity .1s;
    }
    .drop-indicator::before {
      content: "";
      position: absolute;
      width: 8px;
      height: 8px;
      border-radius: 50%;
      background: var(--drop);
      left: -3px;
      top: -2.5px;
    }
    .drop-indicator.visible { opacity: 1; }

    .task-card {
      position: relative;
      background: var(--card);
      border: 1px solid var(--border);
      border-radius: 11px;
      padding: 14px;
      box-shadow: var(--shadow);
      cursor: grab;
      transition: transform .18s, box-shadow .18s, opacity .15s, border-color .18s;
    }
    .task-card:hover {
      transform: translateY(-2px);
      border-color: color-mix(in srgb, var(--primary) 32%, var(--border));
      box-shadow: 0 8px 24px rgba(20,24,35,.09);
    }
    .task-card:active { cursor: grabbing; }
    .task-card.dragging { opacity: .35; transform: rotate(1deg) scale(.98); }

    .task-top { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 10px; }
    .task-tag {
      font-size: 9px;
      line-height: 1;
      font-weight: 800;
      letter-spacing: .8px;
      padding: 5px 7px;
      border-radius: 5px;
    }
    .tag-purple { color: #7064d8; background: #eeecff; }
    .tag-blue { color: #3977bf; background: #e6f1fc; }
    .tag-slate { color: #657183; background: #edf0f4; }
    .tag-orange { color: #c27422; background: #fff0dd; }
    .tag-pink { color: #c15087; background: #fbe8f3; }
    .tag-green { color: #288866; background: #e4f5ed; }
    :root[data-theme="dark"] .tag-purple { color: #aaa3ff; background: #302d55; }
    :root[data-theme="dark"] .tag-blue { color: #84b8f2; background: #203750; }
    :root[data-theme="dark"] .tag-slate { color: #a9b2c1; background: #2a2f39; }
    :root[data-theme="dark"] .tag-orange { color: #eeb369; background: #493521; }
    :root[data-theme="dark"] .tag-pink { color: #e493bd; background: #472d3c; }
    :root[data-theme="dark"] .tag-green { color: #71c9a4; background: #1d4034; }

    .card-actions { display: flex; align-items: center; gap: 2px; }
    .card-action {
      width: 25px;
      height: 25px;
      display: grid;
      place-items: center;
      border: 0;
      background: transparent;
      color: var(--muted-2);
      border-radius: 6px;
      cursor: pointer;
      opacity: .72;
    }
    .card-action:hover { color: var(--text); background: var(--bg); opacity: 1; }
    .card-action.delete-task:hover { color: var(--red); background: var(--red-soft); }
    .card-action .icon { width: 14px; height: 14px; }
    .drag-handle { cursor: grab; }

    .task-title {
      margin: 0;
      font-size: 14px;
      line-height: 1.4;
      font-weight: 670;
      letter-spacing: -.15px;
      color: var(--text);
    }

    .task-footer {
      display: flex;
      align-items: center;
      gap: 9px;
      margin-top: 14px;
      padding-top: 12px;
      border-top: 1px solid var(--border);
    }
    .priority-badge {
      display: inline-flex;
      align-items: center;
      gap: 5px;
      border-radius: 20px;
      padding: 4px 8px;
      font-size: 9.5px;
      font-weight: 750;
      text-transform: capitalize;
    }
    .priority-badge::before { content: ""; width: 5px; height: 5px; border-radius: 50%; background: currentColor; }
    .priority-low { color: var(--green); background: var(--green-soft); }
    .priority-medium { color: var(--amber); background: var(--amber-soft); }
    .priority-high { color: var(--red); background: var(--red-soft); }

    .task-due {
      display: inline-flex;
      align-items: center;
      gap: 4px;
      color: var(--muted);
      font-size: 10.5px;
      font-weight: 560;
      margin-left: auto;
      white-space: nowrap;
    }
    .task-due .icon { width: 12px; height: 12px; }
    .mini-avatar {
      width: 24px;
      height: 24px;
      border-radius: 50%;
      display: grid;
      place-items: center;
      font-size: 8.5px;
      font-weight: 800;
      color: #464153;
      background: linear-gradient(145deg, #e3d9f3, #b8d9e4);
      border: 2px solid var(--card);
      flex: 0 0 auto;
    }

    .empty-column {
      flex: 1;
      min-height: 90px;
      border: 1px dashed var(--column-border);
      border-radius: 10px;
      display: grid;
      place-content: center;
      text-align: center;
      color: var(--muted-2);
      font-size: 11.5px;
      line-height: 1.5;
      padding: 15px;
    }
    .empty-column .icon { width: 20px; height: 20px; margin: 0 auto 6px; }

    /* Modal */
    .modal-backdrop {
      position: fixed;
      inset: 0;
      background: rgba(15, 18, 26, .52);
      backdrop-filter: blur(3px);
      display: grid;
      place-items: center;
      padding: 20px;
      z-index: 100;
      animation: fade-in .16s ease-out;
    }
    .modal {
      width: min(470px, 100%);
      max-height: calc(100vh - 40px);
      overflow: auto;
      background: var(--surface);
      border: 1px solid var(--border);
      border-radius: 17px;
      box-shadow: var(--shadow-lg);
      animation: modal-in .2s ease-out;
    }
    @keyframes fade-in { from { opacity: 0; } }
    @keyframes modal-in { from { opacity: 0; transform: translateY(10px) scale(.98); } }

    .modal-head {
      display: flex;
      align-items: flex-start;
      justify-content: space-between;
      padding: 21px 22px 17px;
      border-bottom: 1px solid var(--border);
    }
    .modal-head h2 { margin: 0 0 4px; font-size: 18px; letter-spacing: -.35px; }
    .modal-head p { margin: 0; color: var(--muted); font-size: 12px; }
    .modal-close {
      width: 31px;
      height: 31px;
      border: 0;
      background: var(--bg);
      color: var(--muted);
      border-radius: 8px;
      display: grid;
      place-items: center;
      cursor: pointer;
    }
    .modal-close:hover { color: var(--text); }
    .modal-close .icon { width: 16px; height: 16px; }

    .modal-form { padding: 20px 22px 22px; }
    .form-group { margin-bottom: 18px; }
    .form-label {
      display: block;
      font-size: 11px;
      font-weight: 750;
      color: var(--text);
      margin-bottom: 7px;
    }
    .form-input, .form-select {
      width: 100%;
      height: 41px;
      border: 1px solid var(--border);
      border-radius: 9px;
      background: var(--surface-2);
      color: var(--text);
      padding: 0 12px;
      outline: 0;
      font-size: 13px;
      transition: border-color .18s, box-shadow .18s;
    }
    .form-input:focus, .form-select:focus {
      border-color: var(--primary);
      box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 14%, transparent);
    }
    .form-input::placeholder { color: var(--muted-2); }

    .priority-choices { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; }
    .priority-choice { position: relative; cursor: pointer; }
    .priority-choice input { position: absolute; opacity: 0; pointer-events: none; }
    .choice-body {
      height: 40px;
      border: 1px solid var(--border);
      border-radius: 9px;
      display: flex;
      align-items: center;
      justify-content: center;
      gap: 7px;
      font-size: 12px;
      font-weight: 650;
      color: var(--muted);
      background: var(--surface-2);
      transition: all .16s;
    }
    .choice-body::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: currentColor; }
    .priority-choice input:checked + .choice-body { border-color: var(--primary); background: var(--primary-soft); color: var(--primary); }
    .priority-choice input:focus-visible + .choice-body { box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 15%, transparent); }
    .priority-choice[data-priority="low"] .choice-body::before { color: var(--green); }
    .priority-choice[data-priority="medium"] .choice-body::before { color: var(--amber); }
    .priority-choice[data-priority="high"] .choice-body::before { color: var(--red); }

    .modal-actions {
      display: flex;
      justify-content: flex-end;
      gap: 9px;
      padding-top: 5px;
    }
    .secondary-button {
      height: 39px;
      padding: 0 15px;
      border: 1px solid var(--border);
      background: var(--surface);
      border-radius: 9px;
      font-size: 12.5px;
      font-weight: 650;
      cursor: pointer;
    }
    .secondary-button:hover { background: var(--bg); }

    /* Toast */
    .toast {
      position: fixed;
      right: 24px;
      bottom: 24px;
      z-index: 200;
      display: flex;
      align-items: center;
      gap: 16px;
      background: var(--text);
      color: var(--surface);
      border-radius: 11px;
      padding: 12px 15px;
      box-shadow: 0 12px 35px rgba(0,0,0,.25);
      font-size: 12.5px;
      font-weight: 600;
      opacity: 0;
      transform: translateY(12px);
      pointer-events: none;
      transition: opacity .2s, transform .2s;
    }
    .toast.show { opacity: 1; transform: translateY(0); pointer-events: auto; }
    .toast button {
      border: 0;
      background: transparent;
      color: var(--primary);
      font-weight: 750;
      font-size: 12px;
      padding: 3px;
      cursor: pointer;
    }
    :root[data-theme="dark"] .toast { background: #f0f2f7; color: #171a22; }

    @media (max-width: 980px) {
      .app { grid-template-columns: minmax(0, 1fr); }
      .sidebar { display: none; }
      .mobile-brand { display: flex; }
      .breadcrumbs { display: none; }
      .topbar { padding: 0 20px; }
      .search { width: min(420px, 55vw); }
    }

    @media (max-width: 700px) {
      body { overflow: auto; }
      .app { height: auto; min-height: 100dvh; }
      .main { min-height: 100dvh; }
      .topbar { height: 62px; padding: 0 13px; gap: 10px; }
      .mobile-brand span:last-child { display: none; }
      .topbar-tools { flex: 1; justify-content: flex-end; }
      .search { width: auto; flex: 1; max-width: none; }
      .search kbd, .notification-button { display: none; }
      .content { padding: 20px 13px 0; }
      .page-heading { align-items: flex-start; flex-direction: column; gap: 15px; margin-bottom: 18px; }
      h1 { font-size: 23px; }
      .page-heading .primary-button { width: 100%; }
      .board { grid-template-columns: 1fr; gap: 14px; overflow: visible; }
      .column { min-height: 0; }
      .column-list { min-height: 85px; }
      .toast { left: 13px; right: 13px; bottom: 13px; justify-content: space-between; }
    }

    @media (max-width: 390px) {
      .search input { font-size: 12px; }
      .icon-button { width: 35px; height: 35px; }
      .priority-choices { gap: 6px; }
    }
  </style>
</head>
<body>
  <svg aria-hidden="true" style="position:absolute;width:0;height:0;overflow:hidden">
    <symbol id="i-layers" viewBox="0 0 24 24"><path d="m12 2 9 5-9 5-9-5 9-5Z"/><path d="m3 12 9 5 9-5"/><path d="m3 17 9 5 9-5"/></symbol>
    <symbol id="i-board" viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="3"/><path d="M9 3v18M15 3v18M3 9h6M3 15h6M15 12h6M15 17h6"/></symbol>
    <symbol id="i-check" viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="3"/><path d="m8 12 3 3 5-6"/></symbol>
    <symbol id="i-inbox" viewBox="0 0 24 24"><path d="M4 4h16v16H4z"/><path d="M4 13h4l2 3h4l2-3h4"/></symbol>
    <symbol id="i-search" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m20 20-4-4"/></symbol>
    <symbol id="i-plus" viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"/></symbol>
    <symbol id="i-x" viewBox="0 0 24 24"><path d="m6 6 12 12M18 6 6 18"/></symbol>
    <symbol id="i-moon" viewBox="0 0 24 24"><path d="M20.5 14.3A8.5 8.5 0 0 1 9.7 3.5 8.5 8.5 0 1 0 20.5 14.3Z"/></symbol>
    <symbol id="i-sun" viewBox="0 0 24 24"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></symbol>
    <symbol id="i-bell" viewBox="0 0 24 24"><path d="M18 8a6 6 0 0 0-12 0c0 7-3 7-3 9h18c0-2-3-2-3-9M10 21h4"/></symbol>
    <symbol id="i-chevron" viewBox="0 0 24 24"><path d="m9 18 6-6-6-6"/></symbol>
    <symbol id="i-chevron-down" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></symbol>
    <symbol id="i-trash" viewBox="0 0 24 24"><path d="M4 7h16M9 7V4h6v3M6 7l1 14h10l1-14M10 11v6M14 11v6"/></symbol>
    <symbol id="i-grip" viewBox="0 0 24 24"><circle cx="9" cy="6" r="1"/><circle cx="15" cy="6" r="1"/><circle cx="9" cy="12" r="1"/><circle cx="15" cy="12" r="1"/><circle cx="9" cy="18" r="1"/><circle cx="15" cy="18" r="1"/></symbol>
    <symbol id="i-calendar" viewBox="0 0 24 24"><rect x="3" y="5" width="18" height="16" rx="2"/><path d="M16 3v4M8 3v4M3 10h18"/></symbol>
  </svg>

  <div class="app">
    <aside class="sidebar">
      <div class="brand">
        <span class="brand-mark"><svg class="icon"><use href="#i-layers"></use></svg></span>
        Flowboard
      </div>

      <div class="nav-label">Workspace</div>
      <nav class="nav-list" aria-label="Main navigation">
        <div class="nav-item active"><svg class="icon"><use href="#i-board"></use></svg>Board<span class="nav-count" id="sidebarCount">6</span></div>
        <div class="nav-item"><svg class="icon"><use href="#i-check"></use></svg>My tasks<span class="nav-count">3</span></div>
        <div class="nav-item"><svg class="icon"><use href="#i-inbox"></use></svg>Inbox<span class="nav-count">2</span></div>
      </nav>

      <div class="sidebar-divider"></div>
      <div class="nav-label">Workspaces</div>
      <nav class="nav-list" aria-label="Workspaces">
        <div class="workspace"><span class="workspace-dot"></span>Product</div>
        <div class="workspace"><span class="workspace-dot blue"></span>Engineering</div>
        <div class="workspace"><span class="workspace-dot green"></span>Marketing</div>
      </nav>

      <div class="sidebar-spacer"></div>

      <div class="upgrade-card">
        <strong>Keep work moving</strong>
        <p>Invite your team and plan your next project together.</p>
        <button type="button">Invite members</button>
      </div>

      <div class="profile">
        <div class="avatar">AM</div>
        <div>
          <div class="profile-name">Avery Morgan</div>
          <div class="profile-role">Product lead</div>
        </div>
        <svg class="icon"><use href="#i-chevron-down"></use></svg>
      </div>
    </aside>

    <main class="main">
      <header class="topbar">
        <div class="mobile-brand">
          <span class="brand-mark"><svg class="icon"><use href="#i-layers"></use></svg></span>
          <span>Flowboard</span>
        </div>
        <div class="breadcrumbs">
          <span>Product</span>
          <svg class="icon"><use href="#i-chevron"></use></svg>
          <strong>Roadmap</strong>
        </div>

        <div class="topbar-tools">
          <label class="search" for="searchInput">
            <svg class="icon"><use href="#i-search"></use></svg>
            <input id="searchInput" type="search" placeholder="Search tasks by title…" autocomplete="off">
            <button class="search-clear" id="searchClear" type="button" aria-label="Clear search" hidden>
              <svg class="icon"><use href="#i-x"></use></svg>
            </button>
            <kbd>/</kbd>
          </label>
          <button class="icon-button theme-button" id="themeToggle" type="button" aria-label="Switch to dark theme" title="Switch theme">
            <svg class="icon moon-icon"><use href="#i-moon"></use></svg>
            <svg class="icon sun-icon"><use href="#i-sun"></use></svg>
          </button>
          <button class="icon-button notification-button" type="button" aria-label="Notifications">
            <svg class="icon"><use href="#i-bell"></use></svg>
          </button>
        </div>
      </header>

      <section class="content">
        <div class="page-heading">
          <div>
            <div class="eyebrow">Sprint 24 · July</div>
            <div class="title-line">
              <h1>Product roadmap</h1>
              <span class="total-pill" id="totalCount">6 tasks</span>
            </div>
            <p class="page-subtitle">Plan the work, track progress, and ship with confidence.</p>
          </div>
          <button class="primary-button" id="addTaskButton" type="button">
            <svg class="icon"><use href="#i-plus"></use></svg>
            Add task
          </button>
        </div>

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

  <div class="modal-backdrop" id="modalBackdrop" hidden>
    <div class="modal" role="dialog" aria-modal="true" aria-labelledby="modalTitle">
      <div class="modal-head">
        <div>
          <h2 id="modalTitle">Create a new task</h2>
          <p>Add a task to your product roadmap.</p>
        </div>
        <button class="modal-close" id="modalClose" type="button" aria-label="Close modal">
          <svg class="icon"><use href="#i-x"></use></svg>
        </button>
      </div>
      <form class="modal-form" id="taskForm">
        <div class="form-group">
          <label class="form-label" for="taskTitle">Task title</label>
          <input class="form-input" id="taskTitle" type="text" placeholder="e.g. Prepare customer interviews" maxlength="100" required>
        </div>
        <div class="form-group">
          <span class="form-label">Priority</span>
          <div class="priority-choices">
            <label class="priority-choice" data-priority="low">
              <input type="radio" name="priority" value="low">
              <span class="choice-body">Low</span>
            </label>
            <label class="priority-choice" data-priority="medium">
              <input type="radio" name="priority" value="medium" checked>
              <span class="choice-body">Medium</span>
            </label>
            <label class="priority-choice" data-priority="high">
              <input type="radio" name="priority" value="high">
              <span class="choice-body">High</span>
            </label>
          </div>
        </div>
        <div class="form-group">
          <label class="form-label" for="taskColumn">Column</label>
          <select class="form-select" id="taskColumn">
            <option value="todo">To do</option>
            <option value="progress">In progress</option>
            <option value="done">Done</option>
          </select>
        </div>
        <div class="modal-actions">
          <button class="secondary-button" id="cancelTask" type="button">Cancel</button>
          <button class="primary-button" type="submit">Add task</button>
        </div>
      </form>
    </div>
  </div>

  <div class="toast" id="toast" role="status" aria-live="polite"></div>

  <script>
    (() => {
      const columns = [
        { key: 'todo', title: 'To do', className: 'todo' },
        { key: 'progress', title: 'In progress', className: 'progress' },
        { key: 'done', title: 'Done', className: 'done' }
      ];

      let cards = [
        { id: 't1', title: 'Design the onboarding checklist', priority: 'medium', column: 'todo', tag: 'DESIGN', tagClass: 'tag-purple', due: 'Jul 18', owner: 'AL' },
        { id: 't2', title: 'Set up product analytics events', priority: 'high', column: 'todo', tag: 'ANALYTICS', tagClass: 'tag-blue', due: 'Jul 19', owner: 'JK' },
        { id: 't3', title: 'Draft Q3 release notes', priority: 'low', column: 'todo', tag: 'DOCS', tagClass: 'tag-slate', due: 'Jul 22', owner: 'AM' },
        { id: 't4', title: 'Build the workspace invite flow', priority: 'high', column: 'progress', tag: 'FEATURE', tagClass: 'tag-orange', due: 'Today', owner: 'SK' },
        { id: 't5', title: 'Improve card search performance', priority: 'medium', column: 'progress', tag: 'PERF', tagClass: 'tag-pink', due: 'Jul 21', owner: 'AM' },
        { id: 't6', title: 'Create the responsive navigation', priority: 'low', column: 'done', tag: 'NAVIGATION', tagClass: 'tag-green', due: 'Jul 14', owner: 'JM' }
      ];

      const board = document.getElementById('board');
      const searchInput = document.getElementById('searchInput');
      const searchClear = document.getElementById('searchClear');
      const totalCount = document.getElementById('totalCount');
      const sidebarCount = document.getElementById('sidebarCount');
      const modalBackdrop = document.getElementById('modalBackdrop');
      const taskForm = document.getElementById('taskForm');
      const taskTitle = document.getElementById('taskTitle');
      const taskColumn = document.getElementById('taskColumn');
      const toast = document.getElementById('toast');
      const themeToggle = document.getElementById('themeToggle');

      let searchTerm = '';
      let draggingId = null;
      let toastTimer = null;
      let undoAction = null;
      let idCounter = 0;

      function cardTemplate(card) {
        const draggable = searchTerm ? 'false' : 'true';
        return `
          <article class="task-card" draggable="${draggable}" data-id="${card.id}" tabindex="0" aria-label="${escapeHtml(card.title)}">
            <div class="task-top">
              <span class="task-tag ${card.tagClass}">${card.tag}</span>
              <div class="card-actions">
                <button class="card-action drag-handle" type="button" aria-label="Drag ${escapeHtml(card.title)}" title="Drag to move">
                  <svg class="icon"><use href="#i-grip"></use></svg>
                </button>
                <button class="card-action delete-task" type="button" data-delete="${card.id}" aria-label="Delete ${escapeHtml(card.title)}" title="Delete task">
                  <svg class="icon"><use href="#i-trash"></use></svg>
                </button>
              </div>
            </div>
            <h3 class="task-title">${escapeHtml(card.title)}</h3>
            <div class="task-footer">
              <span class="priority-badge priority-${card.priority}">${card.priority}</span>
              <span class="task-due"><svg class="icon"><use href="#i-calendar"></use></svg>${card.due}</span>
              <span class="mini-avatar" title="Assignee ${card.owner}">${card.owner}</span>
            </div>
          </article>`;
      }

      function escapeHtml(value) {
        return String(value).replace(/[&<>"']/g, char => ({
          '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;'
        })[char]);
      }

      function render() {
        const query = searchTerm.trim().toLowerCase();

        board.innerHTML = columns.map(column => {
          const allCards = cards.filter(card => card.column === column.key);
          const visibleCards = allCards.filter(card => card.title.toLowerCase().includes(query));
          const count = query && visibleCards.length !== allCards.length
            ? `${visibleCards.length}/${allCards.length}`
            : allCards.length;

          const cardMarkup = visibleCards.length
            ? visibleCards.map(cardTemplate).join('')
            : `<div class="empty-column">
                 <svg class="icon"><use href="#i-inbox"></use></svg>
                 ${query ? 'No matching tasks' : 'Drop a task here'}
               </div>`;

          return `
            <section class="column ${column.className}" data-column="${column.key}">
              <div class="column-head">
                <span class="column-dot"></span>
                <span class="column-title">${column.title}</span>
                <span class="column-count" aria-label="${allCards.length} tasks">${count}</span>
                <button class="column-add" type="button" data-add-column="${column.key}" aria-label="Add task to ${column.title}" title="Add task">
                  <svg class="icon"><use href="#i-plus"></use></svg>
                </button>
              </div>
              <div class="column-list" data-column="${column.key}">
                <div class="drop-indicator"></div>
                ${cardMarkup}
              </div>
            </section>`;
        }).join('');

        totalCount.textContent = `${cards.length} task${cards.length === 1 ? '' : 's'}`;
        sidebarCount.textContent = cards.length;
        searchClear.hidden = !searchTerm;
      }

      function showToast(message, withUndo = false) {
        clearTimeout(toastTimer);
        toast.innerHTML = `<span>${message}</span>${withUndo ? '<button type="button" id="toastUndo">Undo</button>' : ''}`;
        toast.classList.add('show');
        if (withUndo) {
          document.getElementById('toastUndo').addEventListener('click', () => {
            if (undoAction) undoAction();
            undoAction = null;
            toast.classList.remove('show');
          });
        }
        toastTimer = setTimeout(() => {
          toast.classList.remove('show');
          undoAction = null;
        }, 4200);
      }

      function deleteCard(id) {
        const index = cards.findIndex(card => card.id === id);
        if (index < 0) return;
        const removed = cards[index];
        cards.splice(index, 1);
        render();
        undoAction = () => {
          cards.splice(Math.min(index, cards.length), 0, removed);
          render();
          showToast('Task restored');
        };
        showToast('Task deleted', true);
      }

      function moveCard(id, targetColumn, index) {
        const card = cards.find(item => item.id === id);
        if (!card) return;

        card.column = targetColumn;
        const remaining = cards.filter(item => item.id !== id);
        const targetCards = remaining.filter(item => item.column === targetColumn);

        if (!targetCards.length) {
          remaining.push(card);
        } else if (index < targetCards.length) {
          const before = targetCards[index];
          remaining.splice(remaining.findIndex(item => item.id === before.id), 0, card);
        } else {
          const last = targetCards[targetCards.length - 1];
          remaining.splice(remaining.findIndex(item => item.id === last.id) + 1, 0, card);
        }

        cards = remaining;
        render();
      }

      function clearDragState() {
        draggingId = null;
        board.querySelectorAll('.column.drag-over').forEach(column => column.classList.remove('drag-over'));
        board.querySelectorAll('.drop-indicator').forEach(indicator => {
          indicator.classList.remove('visible');
          indicator.removeAttribute('data-index');
        });
        board.querySelectorAll('.task-card.dragging').forEach(card => card.classList.remove('dragging'));
      }

      board.addEventListener('click', event => {
        const addButton = event.target.closest('[data-add-column]');
        if (addButton) {
          openModal(addButton.dataset.addColumn);
          return;
        }
        const deleteButton = event.target.closest('[data-delete]');
        if (deleteButton) deleteCard(deleteButton.dataset.delete);
      });

      board.addEventListener('mousedown', event => {
        if (event.target.closest('.delete-task')) event.preventDefault();
      });

      board.addEventListener('dragstart', event => {
        const card = event.target.closest('.task-card');
        if (!card || searchTerm) {
          event.preventDefault();
          return;
        }
        draggingId = card.dataset.id;
        event.dataTransfer.effectAllowed = 'move';
        event.dataTransfer.setData('text/plain', draggingId);
        requestAnimationFrame(() => card.classList.add('dragging'));
      });

      board.addEventListener('dragover', event => {
        if (!draggingId) return;
        const list = event.target.closest('.column-list');
        if (!list) return;

        event.preventDefault();
        event.dataTransfer.dropEffect = 'move';

        board.querySelectorAll('.column.drag-over').forEach(column => column.classList.remove('drag-over'));
        list.closest('.column').classList.add('drag-over');

        const visible = [...list.querySelectorAll('.task-card:not(.dragging)')];
        let index = 0;
        for (let i = 0; i < visible.length; i++) {
          const rect = visible[i].getBoundingClientRect();
          if (event.clientY > rect.top + rect.height / 2) index = i + 1;
        }

        const indicator = list.querySelector('.drop-indicator');
        const listRect = list.getBoundingClientRect();
        let top;

        if (!visible.length) {
          top = 7;
        } else if (index === 0) {
          top = visible[0].getBoundingClientRect().top - listRect.top - 7;
        } else if (index >= visible.length) {
          top = visible[visible.length - 1].getBoundingClientRect().bottom - listRect.top + 5;
        } else {
          top = visible[index].getBoundingClientRect().top - listRect.top - 7;
        }

        indicator.style.top = `${Math.max(2, top)}px`;
        indicator.dataset.index = index;
        indicator.classList.add('visible');
      });

      board.addEventListener('drop', event => {
        if (!draggingId) return;
        const list = event.target.closest('.column-list');
        if (!list) return;
        event.preventDefault();
        const indicator = list.querySelector('.drop-indicator');
        const index = Number(indicator.dataset.index || 0);
        moveCard(draggingId, list.dataset.column, index);
        clearDragState();
      });

      board.addEventListener('dragend', clearDragState);

      searchInput.addEventListener('input', event => {
        searchTerm = event.target.value;
        render();
      });

      searchClear.addEventListener('click', () => {
        searchInput.value = '';
        searchTerm = '';
        render();
        searchInput.focus();
      });

      function openModal(column = 'todo') {
        taskForm.reset();
        taskTitle.setCustomValidity('');
        taskColumn.value = column;
        modalBackdrop.hidden = false;
        document.body.style.overflow = 'hidden';
        setTimeout(() => taskTitle.focus(), 40);
      }

      function closeModal() {
        modalBackdrop.hidden = true;
        document.body.style.overflow = '';
        taskTitle.setCustomValidity('');
      }

      document.getElementById('addTaskButton').addEventListener('click', () => openModal('todo'));
      document.getElementById('modalClose').addEventListener('click', closeModal);
      document.getElementById('cancelTask').addEventListener('click', closeModal);

      modalBackdrop.addEventListener('mousedown', event => {
        if (event.target === modalBackdrop) closeModal();
      });

      taskTitle.addEventListener('input', () => taskTitle.setCustomValidity(''));

      taskForm.addEventListener('submit', event => {
        event.preventDefault();
        const title = taskTitle.value.trim();
        if (!title) {
          taskTitle.setCustomValidity('Please enter a task title.');
          taskTitle.reportValidity();
          return;
        }

        const priority = new FormData(taskForm).get('priority') || 'medium';
        const column = taskColumn.value;
        idCounter += 1;
        const newCard = {
          id: `task-${Date.now()}-${idCounter}`,
          title,
          priority,
          column,
          tag: 'NEW',
          tagClass: 'tag-purple',
          due: 'Today',
          owner: 'AM'
        };

        cards.push(newCard);
        closeModal();
        render();
        showToast('Task added to the board');
      });

      themeToggle.addEventListener('click', () => {
        const isDark = document.documentElement.dataset.theme === 'dark';
        document.documentElement.dataset.theme = isDark ? 'light' : 'dark';
        themeToggle.setAttribute('aria-label', isDark ? 'Switch to dark theme' : 'Switch to light theme');
        themeToggle.title = isDark ? 'Switch to dark theme' : 'Switch to light theme';
      });

      document.addEventListener('keydown', event => {
        if (event.key === 'Escape' && !modalBackdrop.hidden) closeModal();
        if (event.key === '/' && modalBackdrop.hidden && !['INPUT', 'TEXTAREA', 'SELECT'].includes(document.activeElement.tagName)) {
          event.preventDefault();
          searchInput.focus();
        }
      });

      render();
    })();
  </script>
</body>
</html>
Reasoning trace (6,490 tokens)
We need produce one fenced HTML complete. Need design polished. Need interactive Kanban vanilla. Requirements. Must be careful drag-drop mouse, visual drop indicator. Modal add. Delete. Search live. Theme toggle. 6 realistic cards prepopulated. Card counts. Self-contained no external assets. Should look great 1280x800 and mobile 375.

We need decide design: maybe elegant app "Flowboard" with sidebar, topbar, three columns. At 1280, sidebar 240, board. Cards. Need not overdo. Use inline SVG icons. Use system fonts (no external fonts). Need meaningful visible first paint. Drag and drop HTML5 DnD with drop indicator line. But "with the mouse" HTML5 works. Provide drag handle, dragging class, column drag-over class and insertion indicator. Count update, search filter live. Search filtering maybe counts show filtered? Requirement each showing its card count; could show total count and filtered count. Better count reflects visible filtered cards, with subtitle "2 of 4" perhaps. Simpler count badge number of visible cards and if filtering maybe "1/3". Implement span count visible and total small.

Drag and drop reordering within columns plus between. Use dragstart, dragover on card/list to compute insertion, drop indicator element. Could use HTML5 DnD. For mobile, HTML5 DnD doesn't work touch; requirement mouse only so fine.

Theme toggle with sun/moon, data-theme on root, initial maybe light. Dark theme attractive.

Modal: dialog? Use custom overlay with hidden attribute and JS open. Title required, priority select, column select. Add. Focus trap maybe simple. Escape close, backdrop close. Form submit validates title via required. Delete button with trash icon, maybe confirm not allowed (confirm blocked) so delete directly with subtle undo toast? Good: delete immediately and show toast with Undo button. That improves UX and no confirm. Undo re-adds card at original index.

Search live by title. Empty state within column when no matches. Also global no results? Columns show empty state.

Sidebar perhaps nav items, workspace, stats. But board is the focus. Responsive: sidebar hidden below 900, header mobile. Columns stack horizontally on mobile? Requirement no horizontal scrollbar. Use grid columns 1fr 1fr 1fr desktop; at <=760 stack vertically. Board scroll vertical, page fixed height? Use app height 100vh, board overflow auto. At mobile stack, each column min-height auto.

Let's create layout:
body #f4f2ee light maybe warm neutral. App shell display grid grid-template-columns 240px 1fr; height 100vh.
Sidebar background #161a22 (dark always?) Or theme-aware. Better in light, deep navy sidebar; dark darker. Brand "K." logo coral. Sidebar nav Board active, My tasks, Inbox with counts. Teams section list with color dots. Bottom upgrade card? Keep simple; user avatar.

Main: header with breadcrumb "Workspace / Product", title "Product roadmap", subtitle "Plan, prioritize, and ship..." plus add card button top right. Below toolbar row search, filter? Only search required. Theme toggle and avatar in header top-right. Maybe header top bar with search.

Design:
Topbar (height 68) border-bottom, contains mobile brand, search centered (max-width 420), theme toggle, notification, avatar.
Content padding 28px 32px.
Page heading flex: h1 "Product roadmap", p "Sprint 24 · 6 tasks · Updated just now". Button "+ Add task" primary dark/indigo.
Stats? maybe small progress: columns.

Board grid gap 20. Column cards white with border radius 14, header padding, subtle top color dot (todo amber, progress blue, done green), count badge.
Column body background slightly tinted? Kanban style columns as tinted container with header and list. Column shell background #eceef2 / border. Column header white-ish.
Cards: white, border, radius 12, padding 16, shadow small. Tag "FEATURE", maybe labels extra beyond requirements, realistic. Title, priority badge, footer avatars? Since no external images, use initials circles. Could add due date and comment count icons. Six cards:
To do (3): "Design onboarding checklist" medium; "Set up product analytics" high; "Draft Q3 release notes" low.
In progress (2): "Build workspace invite flow" high; "Improve card search performance" medium.
Done (1): "Create responsive navigation" low.
That's 6. Maybe counts 3/2/1.

Card structure with top row: tag label + drag handle; title; bottom row priority badge, due date, assignee initials.

Drag indicator: In each column list, a div.drop-indicator with height 3px accent. During dragover show between cards. Also column highlight.

JS data array objects {id,title,priority,column,tag,due,initials}. render() builds columns. Use event delegation for delete. Drag events attach after render to card elements.

Potential issue rendering during drag? Fine.

Search input filters by title. On input render(). Empty placeholder.

Add modal custom:
<div class="modal-backdrop" id="modal" hidden> <div class="modal" role="dialog" aria-modal> header title, close button, form: label Task title input required maxlength; priority radio segmented low/medium/high; column select. footer Cancel + Add task. Clicking backdrop closes.
Use hidden attribute CSS `[hidden]{display:none!important}`. Focus input.

Theme toggle: root dataset theme. Initially maybe light. Button aria-label. Toggle text maybe sun/moon icons. CSS variables. In dark, sidebar #10131a, main #14171d, columns #1b1f27, card #232833 etc.

We need first screenshot around 3 sec. Since no game. All visible.

Accessibility: labels, buttons.

No external fonts/images. Use SVG inline symbols? Can use inline <svg> in markup. For dynamic card icons use innerHTML with svg strings. Fine.

Let's carefully write CSS.

Layout desktop 1280: sidebar 236, main 1044. content padding 30. board three columns each ~320. Good. Height 800: topbar 66, content heading ~86, board ~600. Cards 3 in todo column each ~140 = 420 fits. Progress 2. Done 1. Column body min-height maybe 400. Board area height calc? Use .content display flex column; min-height:0; .board flex:1; overflow-y:auto; align-items:start. Actually content fixed within grid row. app height 100dvh; main display flex column; min-width 0. topbar flex none. content flex 1 min-height 0 display flex col. board flex 1 min-height 0 overflow auto. Columns each min-height 100%.

At mobile, sidebar hidden, app display block; topbar sticky? content scroll; board overflow visible; page height auto min-height 100vh. body overflow hidden desktop, auto mobile. Media.

Topbar search: on desktop maybe margin-left auto? Layout: sidebar then main. topbar left mobile menu hidden; .topbar-title hidden? Put search as first flex. Search width 380. Right actions.

Board columns:
.column { background var(--column-bg); border:1px solid var(--column-border); border-radius:16px; display:flex; flex-direction:column; min-height:100%; }
.column-head {padding:16px 16px 12px; display:flex...} no background separate maybe same.
.column-list {padding:4px 12px 16px; display:flex; flex-direction:column; gap:10px; min-height:120px; position:relative;}
Cards.

Drop indicator absolute? Easier as element in flow inserted among cards. But if element inserted into list, DOM flex gap adds extra spacing around it (gap 10 between). Could use margin -5px 0; height 4. indicator. But dragging over compute position. Alternative absolutely positioned inside column list. Need calculate. Implement indicator as child of list with position:absolute; left:8px right:8px; height:3px; background accent; border-radius; z-index. Then compute y = e.clientY - listRect.top, determine card rects, set indicator top relative. Show. On dragover, preventDefault, add drag-over class to column, show indicator. On drop use target column and index. Reorder.

Order of data array? We'll maintain cards array in display order; each card has order maybe array order. render groups. On drop:
target column; determine insertion index among visible cards in target. But if searching, moving ambiguous. Disable dragging while search active? Better allow but index computed relative to visible cards. Simpler: when search filter active, hide drag handles and disable draggable (set draggable false) to avoid confusion; show "Clear search to reorder". Requirement doesn't demand reorder with filter. But DnD between columns should work normally. We'll set cards draggable only if !searchTerm.

When drag over card, compute before/after and indicator position. Determine insertion index = number of visible cards whose midpoint < pointerY. On drop, remove dragged card from cards array, then insert into target relative. Data array holds global; grouping sorted by array order. We need preserve order per column. Use cards array global ordered per column blocks? Easier each card has `order` numeric. But move compute. Use cards array where order preserved as array; render groups filter by column preserving array order. When dropping into column at index k, gather visible target cards excluding dragged. Then insert dragged into cards global after appropriate card. Could assign each card `order` per column? Let's implement data as object and separate order. Use `cards` array global; reorder function:
const remaining = cards.filter(c=>c.id!==dragId);
const targetVisible = remaining.filter(c=>c.column===targetCol); // all remaining (no filter)
let insertGlobalIndex:
if targetVisible.length===0 -> push
else if index>=targetVisible.length -> insert after last target visible in remaining (end of that group in remaining)
else insert before targetVisible[index] in remaining.
remaining.splice(globalIndex,0,moved); cards=remaining.
This preserves relative column grouping if array starts grouped and after insert maybe group still contiguous? Suppose moving todo card to done, insert before done first; todo group remains contiguous minus card. Fine. Moving done to todo inserted among todo. Fine. Moving within done fine.
Index computed from visible non-drag cards. Since no filter when dragging, same.

On dragover target list, determine index based on cards in that list excluding drag element. Use card elements with data-id. Iterate their rect midpoints, count where e.clientY > mid. Set indicator top: if index 0 => first card top - 5 maybe; else after previous card bottom +5. Use `list.getBoundingClientRect`; indicator top = px relative. style.top. Also hide indicator if dropping on same position? fine.
Drop: e.preventDefault, get targetColumn from closest .column dataset column, index = indicator dataset.index. call moveCard.
dragend clear indicators.

Cards have delete button top right. Drag handle as button maybe draggable only card. Make card draggable=true; but clicking inputs? none. To ensure delete button not initiating drag, on mousedown stopPropagation and set? Dragstart could trigger; in dragstart check if e.target closest button? dragstart target is card, original target may be button? We can in button mousedown set card.draggable=false, mouseup restore. Simpler card draggable true, delete button pointerdown e.stopPropagation doesn't prevent dragstart maybe original target button then dragstart target card. Add in dragstart: if state.justClickedDelete skip? Better set card.setAttribute('draggable','false') on delete mousedown, then after click render anyway. Actually mousedown on delete -> stopPropagation; on click -> delete and render, drag never starts due to no movement. Fine.

Drag handle icon shown. Entire card draggable, cursor grab.

Clicking card maybe no action. Could make title button? no.

Toast for delete with undo. Position fixed bottom right. Timeout 4 sec. Undo reinsert card at index.

Add card: button opens modal. Priority radio buttons custom. On submit validate title trimmed. New card id `task-${Date.now()}`. column. push to appropriate position: need insert into target column group order. Find first card of later column to splice before. Since groups. Implement:
const firstOfNext = cards.findIndex(c => columnOrder.indexOf(c.column) > columnOrder.indexOf(col));
if -1 push else splice.
Render, toast "Task added". Close modal, reset.

Counts: column header count visible/total. If no filter just count. Render count as total number. If filtering and visible != total show `visible / total`. Requirement "showing its card count" satisfied.

Could add top title total count "6 tasks" update.

Search clear X button inside search field.

Theme toggle persist not allowed storage. Memory only; theme in memory variable; could set root attr. Toggle button icon changes.

We need include "Memory only" perhaps no need.

Need ensure no console errors. Use `crypto.randomUUID` maybe secure context? sandbox maybe. Use Date.now+counter.

Let's draft HTML.

Icon SVG inline repeated. Use CSS `.icon`. We can use Unicode? SVG nicer. Define symbol sprite at top hidden:
<svg style="display:none"><symbol id=...>...</symbol></svg>. Then <svg><use href="#i-search"/></svg>. `use` with internal works. Dynamic innerHTML for card uses `<svg class="icon"><use href="#i-calendar"></use></svg>`. Fine. But symbols in hidden svg display none can still reference. Yes.

Alternatively inline each.

Sidebar:
brand <div class logo> with svg layers.
nav:
Board active badge 6
My tasks 3
Inbox badge 2
Divider, "WORKSPACES" label, list Product active? Teams.
Maybe nav items not functional. Since demo board, clickable-looking but not needed. Avoid dead? fine.

User bottom: avatar "AM" name Avery Morgan role Product lead, online dot.

Topbar:
mobile brand maybe.
search with icon, input placeholder "Search tasks..." and keyboard hint "/" maybe implement "/" focuses when not input.
right: theme toggle, bell, avatar on mobile? Sidebar has avatar.
A separator.

Content heading:
left overline "PRODUCT / ROADMAP", h1 "Product roadmap", desc "Keep the team aligned and move work forward." Maybe count pill "6 tasks".
right button Add task.

Also optional board toolbar under heading: tabs "Board" active, "List" disabled? Could include but unnecessary. Maybe a small "This sprint" dropdown-like filter. Since requirement search topbar.

Column header:
dot, title, count pill, plus button that opens modal preselected column. Also kebab.
Column body cards.

Cards:
`<article class="task-card" draggable="true" data-id=...>`
top row: `.task-meta` tag pill e.g. "DESIGN"; `.card-actions` drag handle + delete.
h3 title
footer: priority badge with dot; spacer; due date with icon; assignee circle.
Priority colors:
low green, medium amber, high red. Badge background tinted.
Tags use different subtle colors.

Maybe card heights ~150. To do 3 => 480 plus header 60; viewport content board maybe 560, scroll within. First screenshot shows maybe all 3 partially. 1280x800: topbar 66; content pad top 26 + heading ~72 + gap 24 + board remaining 590. Column 590. 3 cards 140 each plus gaps = 440 fits. Good.

Let's compute card: padding 15*2=30; meta 22; title 40; margin 12; footer 24 =>128. 3*128+20=404. fine.

Use CSS variables.

Dark theme: root[data-theme=dark]. Ensure initial light. Toggle button shows moon in light, sun in dark via CSS hide.

Modal styling.

Potential responsive at 375: topbar height 62; search fills width, hide keyboard hint, right theme only; content padding 18 14; heading stack, button width 100%; board single column; columns min-height auto; board overflow visible. Body overflow auto. Sidebar hidden. Topbar search maybe order. Topbar .topbar-left brand visible mobile only, search flex 1; bell hidden; theme visible.
At 900 hide sidebar; app grid one column.
At 760 board grid-template-columns 1fr; .column min-height unset; board overflow-y:auto still works with body hidden. Since app 100dvh and content board scrolls. Good, no horizontal.
At 375 card footer wrap? keep.

Need ensure search input not causing horizontal.

Implement render:
const columns = {todo:{title:'To do',color...}, progress, done}.
cards initial array.
`renderBoard()`:
board.innerHTML = columns.map(columnHTML).join('');
attach? use event delegation at board level for delete and plus. Add listener once to board.
For each column compute all = cards.filter(c=>c.column===key); visible = all.filter(matches);
columnHTML includes header, list `data-column`.
list innerHTML visible.map(cardHTML).join('') or empty state.
Counts.
Set total task count in heading.
Card draggable attribute if search empty.
Assign due etc.

`cardHTML` priority class. tag stored e.g. DESIGN. Use tagClass maybe card.tagClass.

Events delegation:
board.addEventListener('click', e => { const add=e.target.closest('.column-add'); if(add) openModal(add.dataset.column); const del=... if(del){deleteCard(id)} });
Delete button has aria-label.
Also maybe clicking card title focuses search? no.

`matchesQuery = c.title.toLowerCase().includes(searchTerm)`.

Search input event updates searchTerm and render. Clear button display.

Drag:
board.addEventListener('dragstart', e=>{ const card=e.target.closest('.task-card'); if(!card || searchTerm){e.preventDefault();return;} draggingId=...; e.dataTransfer.effectAllowed='move'; e.dataTransfer.setData('text/plain',id); requestAnimationFrame(()=>card.classList.add('dragging'));});
Potential render on search only.

board.addEventListener('dragover', e=>{ if(!draggingId)return; const list=e.target.closest('.column-list'); if(!list)return; e.preventDefault(); e.dataTransfer.dropEffect='move'; const col=list.closest('.column'); remove drag-over other columns; add; const cardEls=[...list.querySelectorAll('.task-card:not(.dragging)')]; let index=0; for (let i...) {rect; if(e.clientY > rect.top+rect.height/2) index=i+1; } indicator style top computed.
Need indicator exists in list always, hidden by default. list has `.drop-indicator`. Compute top:
const listRect=list.getBoundingClientRect();
if cardEls.length===0 top = 8;
else if index===0 top = cardEls[0].offsetTop - 7? offsetTop relative list (list position relative) but card may have margins? list padding. offsetTop includes. Better use rect: top = firstRect.top - listRect.top - 6; else if index>=cardEls.length top = lastRect.bottom - listRect.top + 5; else top = cardEls[index].rect.top - listRect.top - 7.
Indicator dataset.index=index.
show.
Also update column-header maybe? Column drag-over class background.
});
dragenter? dragover enough.
drop: find list; e.preventDefault; const index=Number(indicator.dataset.index||0); moveCard(draggingId, list.dataset.column, index); cleanup.
dragend cleanup: remove dragging classes, indicators hide, col class remove.

Note `drop` event bubbles to board. fine.

moveCard:
const card=cards.find; oldcol; card.column=target; then reorder array:
const remaining=cards.filter(id!=);
const targetCards=remaining.filter(c=>c.column===target);
let insertBeforeId = targetCards[index]?.id;
if insertBefore -> idx=remaining.findIndex(...); splice.
else if targetCards.length -> insert after last target card: idx=remaining.indexOf(last); splice idx+1.
else -> append? But if columns later exist, pushing at end means new empty target column at end not grouped. Example move todo card to progress when progress has cards, handled. If target empty progress and done cards exist, append at end after done => rendering groups anyway so order irrelevant because render filters by column! Wait global array order only determines within-column order, and group rendering filters preserves relative order among same column regardless of other columns. So appending is fine. Great. For target with cards, inserting relative works.
Actually simplest: after changing column, remove card; then compute visible target cards in remaining (target column) and insert card in global array before targetCards[index] or after last target card. Good.
Render.

Within same column moving to index: targetCards excludes dragged; index from indicator count excluding dragging card. Works.

delete card: store index and card; remove; render; show toast "Task deleted" Undo. Undo: cards.splice(Math.min(index,cards.length),0,card); render.
Toast maybe fixed bottom 24 right 24; mobile full width.

Modal:
openModal(column='todo'): reset form; set select value column; modal.hidden=false; document.body.classList.add('modal-open'); setTimeout focus.
`closeModal`: hidden true; class remove.
form submit: e.preventDefault(); title=input.value.trim(); if(!title){input.setCustomValidity('...'); reportValidity(); return} But required handles empty spaces not empty. Trim then if empty set custom validity. priority from radio checked; column select.
new card tag 'NEW' and maybe due "Today", initials "AM". tagClass 'tag-new'.
Close, render, toast.

Radio custom: input hidden? Use `.priority-option input {position:absolute;opacity:0}` label. Each label class. Use actual radio visually hidden and label span. Use required on one? radio group required.
HTML:
<div class=priority-choices>
<label class=priority-choice><input type=radio name=priority value=low checked><span class=choice-dot low>...</span>Low</label> etc.
CSS input:checked + span? Need style whole label via :has(input:checked) (Chrome latest supports). Better input + .choice-content and style label:has. :has supported latest. Or use JS class. Use `label:has(input:checked)`. Fine. To be robust, input checked + span.

We'll structure label input then span class=choice-body. `label:has(input:checked){...}`.

Column select styled.

Focus.

Theme toggle button includes both icons with CSS.

Need maybe keyboard "/" focus search. keydown listener on document: if e.key==='/' and not input and modal closed, prevent and focus search. Also Escape closes modal. Add tip in search `<kbd>/</kbd>`.

Accessibility aria live for toast.

Let's write code fully.

Need exactly one fenced code block. No text outside.

Let's ensure CSS variables and dark.

Variables light:
--bg:#f5f6f8;
--surface:#ffffff;
--sidebar:#171a21;
--sidebar-text:#f4f5f7;
--muted:#6f7785;
--text:#181b22;
--border:#e4e7ec;
--column-bg:#eef0f4;
--column-head:#f7f8fa? Actually header same.
--card-shadow:0 1px 2px rgba...
--accent:#5b5bd6 maybe indigo. Use vibrant purple #6c5ce7? Brand coral? Let's choose refined indigo #5b5ce2 and accent orange? Keep accent #6157d9.
--drop:#6157d9.

Sidebar deep #171922 with logo square gradient purple.

Main light #f6f7f9.

Column body var #edeff3. Column header var #f8f9fb? Column shell same. Card white.

Dark:
--bg:#101217; --surface:#181b22; --sidebar:#0c0e13; --text:#eef0f5; --muted:#969eac; --border:#2a2e38; --column-bg:#171a21; card #20242d; shadow.

Topbar surface var(--surface), border.

Heading.

Buttons primary background var(--text)? In light dark navy, in dark maybe accent. Use --primary:#5c56dc; --primary-hover.
Sidebar nav active rgba.

Logo.

Sidebar workspace list.

Sidebar fixed.

Potential counts heading `.total-pill`.

Let's make HTML.

One concern: task card title h3, card top meta tag. Priority badge colored. Good.

Empty state for column: if filtered no matches, show "No matching tasks" / "Drop tasks here". During drag, empty list still indicator.

When search filters a column to zero but column has total tasks hidden, empty state "No matching tasks". if all==0 "No tasks yet — drop one here".

Global no results? each column empty, plus maybe a top notice "No tasks match". Could show `.no-results-banner` above board? board is grid, adding would break. Instead heading subtitle updates? Add small filter status next to search. Search clear button.

Search count.

Card counts in column header: if query show `visible/total`; otherwise total.

Add button with plus.

Column plus opens modal for column.

Card assignee initials.

Delete button opacity appears on hover but visible? Requirement delete with button; keep visible low opacity 0.7, hover 1.

Drag handle cursor grab.

Drop indicator absolute within list. list position relative. cards are flex.

During drag card opacity .4.

Column drag-over border accent.

Let's ensure CSS `min-width:0` for columns.

Sidebar nav items pointer.

Responsive sidebar hide at 980 maybe at 1280 visible (236). main 1044. good. At 900 hide.

At 700 board single. Fine.

Maybe screenshot desired at 1280 shows all.

Let's write JS carefully without template errors.

Symbols:
i-search, i-plus, i-moon, i-sun, i-bell, i-board, i-check, i-clock, i-trash, i-grip, i-calendar, i-chevron, i-more, i-layers, i-inbox, i-x, i-sparkle? internal.

Use `<svg class="icon" aria-hidden="true"><use href="#i-search"></use></svg>`. href attribute works.

Card innerHTML generated includes use href.

`tagClass` mapping initial: design tag-purple, analytics tag-blue, docs tag-slate, feature tag-orange, performance tag-pink, navigation tag-green. New tag-purple.

Initial cards due: "Jul 18" etc. Current? no matter.

Column order keys todo, progress, done.

Data:
{id:'t1',title:'Design the onboarding checklist', priority:'medium', column:'todo', tag:'DESIGN', tagClass:'tag-purple', due:'Jul 18', owner:'AL'}
t2 'Set up product analytics events' high todo tag ANALYTICS tag-blue due Jul 19 owner JK
t3 'Draft Q3 release notes' low todo tag DOCS tag-slate due Jul 22 owner AM
t4 'Build the workspace invite flow' high progress FEATURE tag-orange due Today owner SK
t5 'Improve card search performance' medium progress PERF tag-pink due Jul 21 owner AM
t6 'Create the responsive navigation' low done NAVIGATION tag-green due Jul 14 owner JM
Realistic.

Heading total pill maybe "6 tasks".

Date not actual current.

Need keyboard to start? Only for games. Not applicable.

No localStorage.

Let's include a subtle topbar status "All changes saved" with green dot? Memory only, but saying saved implies storage. Avoid. Could say "Sprint 24". Topbar maybe breadcrumb.

Topbar left contains breadcrumb "Acme Studio / Product" and search on right? Design: topbar search left-ish center. Let's do:
<div class=topbar>
<div class=crumbs> <span class=crumb-dot> maybe workspace icon + "Product team" chevron</div>
<div class=topbar-tools>
<label search>...</label>
button theme, bell, avatar? 
</div>
At mobile crumbs hidden, search visible.

Content heading right add button.

Sidebar.

Let's write.

JS toast creation: existing toast hidden class? `<div id=toast class=toast role=status aria-live=polite></div>` and set innerHTML including button. Add click listener via toast onclick checking target closest. Timer.

`showToast(message, undoFn)` set innerHTML `<span>${message}</span><button>Undo</button>`, add class show, assign undoAction. When undo click execute.

Add toast no undo, maybe button absent. CSS.

Modal open with column select.

Search clear button hidden when no query. `searchClear.hidden = !term`.

Render board after DOM ready script at end.

Theme: `document.documentElement.dataset.theme='light'`; toggle click toggles and updates aria-label/title. Icons CSS based on theme. Could also set button text? no.

Keyboard: "/" focus. Escape.

Maybe nav item clicks no effect.

Drag indicator clear:
function clearDragState(){draggingId=null; board.querySelectorAll('.drag-over').forEach...; indicators.forEach(i=>{i.classList.remove('visible'); i.removeAttribute('data-index')}); document.querySelectorAll('.dragging').forEach(...);}
In dragend, card removed by render so dragging class gone. cleanup.

dragover needs not clear indicator then compute. fine.

When dragging over column but list target could be card. closest works.

Drop index from indicator. If drop on column header no list -> no move. acceptable.

Add `list.addEventListener`? Delegated board dragover works because events bubble.

`e.dataTransfer.setData('text/plain', draggingId)` needed Chrome.

In `dragstart`, `e.target` may be child? event target is draggable element (card). good.

Card `draggable="true"`.

Prevent dragging when clicking delete? Add board mousedown listener: if target.closest('.delete-task') then e.preventDefault() stops maybe but then click still fires? preventDefault on mousedown doesn't stop click. It prevents drag. fine.

Column counts update.

No empty search global.

Modal max width 480.

Dark theme modal.

Need style scrollbars maybe.

Let's produce final code.