← Kanban results

Kanban

Qwen 3.6 27b MTP · full

Initial view of Kanban

Reviews & generation details

Reviews

No human review yet. Unreviewed does not mean unsuccessful.

Generation context

Imported historical artifact. Its exact original instructions and lineage are not known.

Generation prompt · exact text unavailable

See the current prompt for reference. It may differ from the instructions used to generate this artifact.

Inspect original source 50,166 bytes · SHA-256 d07c3cb46693
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Kanban Board</title>
<style>
/* ============================================================
   CONSTANTS & RESET
   ============================================================ */
:root {
  --bg: #f4f5f7;
  --column-bg: #ebecf0;
  --card-bg: #ffffff;
  --text-primary: #172b4d;
  --text-secondary: #5e6c84;
  --accent: #0079bf;
  --accent-hover: #026aa7;
  --danger: #eb5a46;
  --success: #61bd4f;
  --border-radius: 8px;
  --shadow-card: 0 1px 2px rgba(0,0,0,0.1);
  --shadow-card-hover: 0 4px 8px rgba(0,0,0,0.12);
  --shadow-drag: 0 8px 24px rgba(0,0,0,0.2);
  --transition-fast: 150ms ease;
  --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
}

*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }

body {
  font-family: var(--font-sans);
  background: var(--bg);
  color: var(--text-primary);
  overflow: hidden;
  height: 100vh;
}

/* ============================================================
   HEADER BAR
   ============================================================ */
.board-header {
  display: flex;
  align-items: center;
  gap: 16px;
  padding: 12px 24px;
  background: #fff;
  border-bottom: 1px solid #ddd;
  position: sticky;
  top: 0;
  z-index: 100;
  height: 56px;
}

.board-header h1 {
  font-size: 20px;
  font-weight: 700;
  color: var(--text-primary);
  white-space: nowrap;
}

.header-controls {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-left: auto;
}

.search-box {
  padding: 6px 12px;
  border: 2px solid #ddd;
  border-radius: var(--border-radius);
  font-size: 14px;
  width: 200px;
  outline: none;
  transition: border-color var(--transition-fast);
}

.search-box:focus {
  border-color: var(--accent);
}

.filter-bar {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 8px 24px;
  background: #fff;
  border-bottom: 1px solid #eee;
  min-height: 40px;
}

.filter-chip {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 3px 10px;
  border-radius: 12px;
  font-size: 12px;
  font-weight: 600;
  cursor: pointer;
  border: none;
  transition: opacity var(--transition-fast);
}

.filter-chip:hover { opacity: 0.8; }

.filter-chip.active { outline: 2px solid var(--text-primary); outline-offset: 1px; }

.clear-filters-btn {
  font-size: 12px;
  color: var(--danger);
  background: none;
  border: none;
  cursor: pointer;
  padding: 4px 8px;
  border-radius: 4px;
}

.clear-filters-btn:hover { background: #fee; }

/* ============================================================
   BOARD CONTAINER
   ============================================================ */
.board-container {
  display: flex;
  align-items: flex-start;
  gap: 16px;
  padding: 20px 24px;
  overflow-x: auto;
  overflow-y: hidden;
  height: calc(100vh - 96px);
}

.board-container::-webkit-scrollbar { height: 10px; }
.board-container::-webkit-scrollbar-track { background: transparent; }
.board-container::-webkit-scrollbar-thumb { background: #ccc; border-radius: 5px; }
.board-container::-webkit-scrollbar-thumb:hover { background: #aaa; }

/* ============================================================
   COLUMNS
   ============================================================ */
.column {
  min-width: 280px;
  max-width: 320px;
  width: 300px;
  flex-shrink: 0;
  background: var(--column-bg);
  border-radius: var(--border-radius);
  display: flex;
  flex-direction: column;
  max-height: calc(100vh - 140px);
  transition: box-shadow var(--transition-fast);
}

.column.drag-over-column {
  background: #d6d8db;
}

.column.wip-exceeded {
  outline: 3px solid var(--danger);
  outline-offset: -2px;
}

.column-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px 12px;
  cursor: grab;
  user-select: none;
}

.column-header:active { cursor: grabbing; }

.column-title-group {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: 1;
  min-width: 0;
}

.column-title {
  font-size: 14px;
  font-weight: 700;
  color: var(--text-primary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  cursor: pointer;
  padding: 2px 4px;
  border-radius: 4px;
  border: none;
  background: transparent;
}

.column-title:hover { background: rgba(0,0,0,0.06); }

.card-count {
  font-size: 12px;
  color: var(--text-secondary);
  background: rgba(0,0,0,0.08);
  padding: 1px 7px;
  border-radius: 10px;
  flex-shrink: 0;
}

.column-menu-btn {
  background: none;
  border: none;
  cursor: pointer;
  font-size: 18px;
  color: var(--text-secondary);
  padding: 4px 6px;
  border-radius: 4px;
  line-height: 1;
}

.column-menu-btn:hover { background: rgba(0,0,0,0.1); }

/* Column dropdown menu */
.column-dropdown {
  position: absolute;
  top: 100%;
  right: 0;
  background: #fff;
  border-radius: var(--border-radius);
  box-shadow: 0 4px 16px rgba(0,0,0,0.2);
  padding: 8px 0;
  z-index: 200;
  min-width: 180px;
}

.column-dropdown button {
  display: block;
  width: 100%;
  text-align: left;
  padding: 8px 16px;
  border: none;
  background: none;
  font-size: 14px;
  cursor: pointer;
  color: var(--text-primary);
}

.column-dropdown button:hover { background: #f0f0f0; }
.column-dropdown button.danger { color: var(--danger); }

/* WIP limit input in dropdown */
.wip-input-row {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 16px;
}

.wip-input-row label { font-size: 13px; color: var(--text-secondary); }

.wip-input-row input {
  width: 50px;
  padding: 4px 6px;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 13px;
}

/* ============================================================
   CARD LIST
   ============================================================ */
.card-list {
  flex: 1;
  overflow-y: auto;
  padding: 4px 8px;
  min-height: 20px;
}

.card-list::-webkit-scrollbar { width: 6px; }
.card-list::-webkit-scrollbar-track { background: transparent; }
.card-list::-webkit-scrollbar-thumb { background: #ccc; border-radius: 3px; }

/* Empty column drop zone */
.card-list.empty-column::before {
  content: 'Drop cards here';
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 60px;
  border: 2px dashed #bbb;
  border-radius: var(--border-radius);
  color: var(--text-secondary);
  font-size: 13px;
  margin: 4px 0;
}

/* ============================================================
   CARDS
   ============================================================ */
.card {
  background: var(--card-bg);
  border-radius: var(--border-radius);
  padding: 10px 12px;
  margin-bottom: 8px;
  box-shadow: var(--shadow-card);
  cursor: grab;
  transition: transform var(--transition-fast), box-shadow var(--transition-fast);
  position: relative;
}

.card:hover {
  box-shadow: var(--shadow-card-hover);
  transform: translateY(-1px);
}

.card.dragging {
  opacity: 0.3;
  pointer-events: none;
}

.card.hidden-by-filter {
  display: none;
}

.card-labels {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  margin-bottom: 6px;
}

.label-chip {
  height: 8px;
  width: 40px;
  border-radius: 4px;
  display: inline-block;
}

.card-title {
  font-size: 14px;
  line-height: 1.4;
  color: var(--text-primary);
  word-break: break-word;
}

.card-description {
  font-size: 12px;
  color: var(--text-secondary);
  margin-top: 4px;
  line-height: 1.3;
}

.card-meta {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-top: 8px;
  font-size: 12px;
  color: var(--text-secondary);
}

.due-date {
  display: inline-flex;
  align-items: center;
  gap: 3px;
}

.due-date.overdue { color: var(--danger); font-weight: 600; }

.card-actions {
  position: absolute;
  top: 8px;
  right: 8px;
  display: none;
  gap: 2px;
}

.card:hover .card-actions { display: flex; }

.card-action-btn {
  background: rgba(0,0,0,0.06);
  border: none;
  cursor: pointer;
  font-size: 14px;
  padding: 2px 5px;
  border-radius: 3px;
  color: var(--text-secondary);
}

.card-action-btn:hover { background: rgba(0,0,0,0.12); }

/* ============================================================
   ADD CARD BUTTON & FORM
   ============================================================ */
.add-card-row {
  padding: 4px 8px 8px;
}

.add-card-btn {
  display: flex;
  align-items: center;
  gap: 6px;
  width: 100%;
  padding: 8px;
  border: none;
  background: transparent;
  color: var(--text-secondary);
  font-size: 14px;
  cursor: pointer;
  border-radius: var(--border-radius);
  text-align: left;
}

.add-card-btn:hover { background: rgba(0,0,0,0.06); color: var(--text-primary); }

/* ============================================================
   ADD COLUMN BUTTON
   ============================================================ */
.add-column-btn {
  min-width: 280px;
  padding: 12px 16px;
  border: none;
  background: rgba(255,255,255,0.5);
  color: var(--text-secondary);
  font-size: 14px;
  cursor: pointer;
  border-radius: var(--border-radius);
  flex-shrink: 0;
}

.add-column-btn:hover { background: rgba(255,255,255,0.8); color: var(--text-primary); }

/* ============================================================
   MODAL / INLINE FORMS
   ============================================================ */
.inline-form {
  padding: 4px 8px;
}

.inline-form textarea {
  width: 100%;
  min-height: 60px;
  border: 2px solid var(--accent);
  border-radius: var(--border-radius);
  padding: 8px;
  font-size: 14px;
  font-family: inherit;
  resize: vertical;
  outline: none;
}

.inline-form .form-actions {
  display: flex;
  align-items: center;
  gap: 6px;
  margin-top: 8px;
}

.btn-primary {
  padding: 6px 14px;
  background: var(--accent);
  color: #fff;
  border: none;
  border-radius: var(--border-radius);
  font-size: 14px;
  cursor: pointer;
}

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

.btn-secondary {
  padding: 6px 10px;
  background: transparent;
  color: var(--text-secondary);
  border: none;
  font-size: 14px;
  cursor: pointer;
}

.btn-secondary:hover { text-decoration: underline; }

/* Label picker in form */
.label-picker {
  display: flex;
  gap: 6px;
  margin-top: 8px;
  flex-wrap: wrap;
}

.label-option {
  width: 24px;
  height: 24px;
  border-radius: 50%;
  cursor: pointer;
  border: 3px solid transparent;
  transition: border-color var(--transition-fast);
}

.label-option.selected { border-color: var(--text-primary); }

.due-date-input {
  margin-top: 8px;
  padding: 6px 8px;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 13px;
}

/* ============================================================
   DRAG GHOST (follows cursor)
   ============================================================ */
.drag-ghost {
  position: fixed;
  pointer-events: none;
  z-index: 9999;
  width: 280px;
  transform: rotate(3deg);
  box-shadow: var(--shadow-drag);
  opacity: 0.95;
}

/* Drop placeholder */
.drop-placeholder {
  height: 4px;
  background: var(--accent);
  border-radius: 2px;
  margin: 4px 0;
  transition: all 100ms ease;
}

/* ============================================================
   TOAST NOTIFICATIONS
   ============================================================ */
.toast-container {
  position: fixed;
  bottom: 24px;
  left: 50%;
  transform: translateX(-50%);
  z-index: 10000;
  display: flex;
  flex-direction: column;
  gap: 8px;
  align-items: center;
}

.toast {
  background: #333;
  color: #fff;
  padding: 10px 20px;
  border-radius: var(--border-radius);
  font-size: 14px;
  display: flex;
  align-items: center;
  gap: 12px;
  box-shadow: 0 4px 16px rgba(0,0,0,0.3);
  animation: toastIn 200ms ease forwards;
}

.toast.removing { animation: toastOut 200ms ease forwards; }

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

.toast .undo-btn {
  color: #79b8ff;
  background: none;
  border: none;
  font-size: 14px;
  cursor: pointer;
  font-weight: 600;
}

.toast .undo-btn:hover { text-decoration: underline; }

/* ============================================================
   COLUMN TITLE EDITING
   ============================================================ */
.column-title-input {
  font-size: 14px;
  font-weight: 700;
  padding: 2px 6px;
  border: 2px solid var(--accent);
  border-radius: 4px;
  outline: none;
  width: 150px;
}

/* ============================================================
   CARD TITLE INLINE EDITING
   ============================================================ */
.card-title-input {
  font-size: 14px;
  line-height: 1.4;
  color: var(--text-primary);
  border: 2px solid var(--accent);
  border-radius: 4px;
  padding: 2px 6px;
  outline: none;
  width: 100%;
  font-family: inherit;
}

/* ============================================================
   CARD EDIT MODAL
   ============================================================ */
.modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.5);
  z-index: 5000;
  display: flex;
  align-items: center;
  justify-content: center;
}

.modal {
  background: #fff;
  border-radius: var(--border-radius);
  padding: 24px;
  width: 480px;
  max-width: 90vw;
  box-shadow: 0 8px 32px rgba(0,0,0,0.3);
}

.modal h2 { font-size: 18px; margin-bottom: 16px; }

.modal textarea {
  width: 100%;
  min-height: 80px;
  border: 1px solid #ddd;
  border-radius: var(--border-radius);
  padding: 8px;
  font-size: 14px;
  font-family: inherit;
  resize: vertical;
  outline: none;
}

.modal textarea:focus { border-color: var(--accent); }

.modal .form-group { margin-top: 12px; }
.modal .form-group label { display: block; font-size: 13px; color: var(--text-secondary); margin-bottom: 4px; }

.modal-actions {
  display: flex;
  justify-content: flex-end;
  gap: 8px;
  margin-top: 16px;
}
</style>
</head>
<body>

<!-- ============================================================
     HTML STRUCTURE
     ============================================================ -->
<div class="board-header">
  <h1>Kanban Board</h1>
  <div class="header-controls">
    <input type="text" class="search-box" id="searchBox" placeholder="Search cards...">
  </div>
</div>

<div class="filter-bar" id="filterBar"></div>

<div class="board-container" id="boardContainer"></div>

<div class="toast-container" id="toastContainer"></div>

<script>
/* ============================================================
   TUNABLE CONSTANTS
   ============================================================ */
const CONFIG = {
  STORAGE_KEY: 'kanban-board-state',
  LABELS: [
    { name: 'Bug', color: '#eb5a46' },
    { name: 'Feature', color: '#61bd4f' },
    { name: 'Enhancement', color: '#f2d600' },
    { name: 'Design', color: '#c377e0' },
    { name: 'Documentation', color: '#0079bf' },
    { name: 'Urgent', color: '#ff78cb' },
  ],
  DRAG_THRESHOLD: 5,       // px before drag starts
  AUTO_SCROLL_SPEED: 12,   // px per frame near edge
  AUTO_SCROLL_MARGIN: 60,  // px from edge to trigger scroll
  TOAST_DURATION: 4000,    // ms before toast auto-dismisses
  ANIMATION_DURATION: 200, // ms for settle animation
};

/* ============================================================
   UTILITY FUNCTIONS
   ============================================================ */
function generateId() {
  return Date.now().toString(36) + Math.random().toString(36).slice(2, 9);
}

function deepClone(obj) {
  return JSON.parse(JSON.stringify(obj));
}

function formatDate(dateStr) {
  if (!dateStr) return '';
  const d = new Date(dateStr + 'T00:00:00');
  const today = new Date();
  today.setHours(0, 0, 0, 0);
  const tomorrow = new Date(today);
  tomorrow.setDate(tomorrow.getDate() + 1);
  if (d.getTime() === today.getTime()) return 'Today';
  if (d.getTime() === tomorrow.getTime()) return 'Tomorrow';
  if (d < today) {
    const days = Math.floor((today - d) / 86400000);
    return `${days}d overdue`;
  }
  return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
}

function isOverdue(dateStr) {
  if (!dateStr) return false;
  const d = new Date(dateStr + 'T00:00:00');
  const today = new Date();
  today.setHours(0, 0, 0, 0);
  return d < today;
}

/* ============================================================
   BOARD MODEL — SINGLE SOURCE OF TRUTH
   ============================================================ */
const defaultBoardModel = {
  columns: [
    { id: generateId(), title: 'Backlog', cardIds: [], wipLimit: null },
    { id: generateId(), title: 'To Do', cardIds: [], wipLimit: null },
    { id: generateId(), title: 'In Progress', cardIds: [], wipLimit: 3 },
    { id: generateId(), title: 'Review', cardIds: [], wipLimit: null },
    { id: generateId(), title: 'Done', cardIds: [], wipLimit: null },
  ],
  cards: {}, // keyed by cardId
  filterLabels: [],
  searchQuery: '',
};

let boardModel = deepClone(defaultBoardModel);

// Undo stack — stores snapshots before destructive actions
const undoStack = [];

function saveUndoSnapshot() {
  undoStack.push(deepClone(boardModel));
  if (undoStack.length > 50) undoStack.shift(); // cap at 50
}

function restoreFromUndo() {
  if (undoStack.length === 0) return;
  boardModel = undoStack.pop();
  persistBoard();
  renderAll();
}

/* ============================================================
   PERSISTENCE
   ============================================================ */
function persistBoard() {
  try {
    localStorage.setItem(CONFIG.STORAGE_KEY, JSON.stringify(boardModel));
  } catch (e) { /* quota exceeded — silently ignore */ }
}

function loadBoard() {
  try {
    const saved = localStorage.getItem(CONFIG.STORAGE_KEY);
    if (saved) {
      boardModel = JSON.parse(saved);
      // Ensure filterLabels and searchQuery exist for older saves
      if (!boardModel.filterLabels) boardModel.filterLabels = [];
      if (!boardModel.searchQuery) boardModel.searchQuery = '';
    }
  } catch (e) { /* corrupted save — use defaults */ }
}

/* ============================================================
   TOAST SYSTEM
   ============================================================ */
function showToast(message, undoCallback) {
  const container = document.getElementById('toastContainer');
  const toast = document.createElement('div');
  toast.className = 'toast';

  const msgSpan = document.createElement('span');
  msgSpan.textContent = message;
  toast.appendChild(msgSpan);

  if (undoCallback) {
    const undoBtn = document.createElement('button');
    undoBtn.className = 'undo-btn';
    undoBtn.textContent = 'Undo';
    undoBtn.addEventListener('click', () => {
      undoCallback();
      removeToast(toast);
    });
    toast.appendChild(undoBtn);
  }

  container.appendChild(toast);

  setTimeout(() => removeToast(toast), CONFIG.TOAST_DURATION);
}

function removeToast(toast) {
  if (!toast.parentNode) return;
  toast.classList.add('removing');
  setTimeout(() => toast.remove(), 200);
}

/* ============================================================
   RENDERING — DERIVED ENTIRELY FROM MODEL
   ============================================================ */
const boardContainer = document.getElementById('boardContainer');
const filterBarEl = document.getElementById('filterBar');
const searchBoxEl = document.getElementById('searchBox');

function renderAll() {
  renderFilterBar();
  renderBoard();
}

function renderFilterBar() {
  const activeLabels = boardModel.filterLabels;
  let html = '';

  CONFIG.LABELS.forEach(label => {
    const isActive = activeLabels.includes(label.name);
    html += `<button class="filter-chip ${isActive ? 'active' : ''}" 
                     style="background:${label.color}20;color:${label.color};${isActive ? '' : 'opacity:0.6'}"
                     data-label="${label.name}">
              <span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:${label.color}"></span>
              ${label.name}
            </button>`;
  });

  if (activeLabels.length > 0) {
    html += `<button class="clear-filters-btn" id="clearFiltersBtn">Clear filters</button>`;
  }

  filterBarEl.innerHTML = html;

  // Bind events
  filterBarEl.querySelectorAll('.filter-chip').forEach(btn => {
    btn.addEventListener('click', () => {
      const labelName = btn.dataset.label;
      if (boardModel.filterLabels.includes(labelName)) {
        boardModel.filterLabels = boardModel.filterLabels.filter(l => l !== labelName);
      } else {
        boardModel.filterLabels.push(labelName);
      }
      persistBoard();
      renderAll();
    });
  });

  const clearBtn = document.getElementById('clearFiltersBtn');
  if (clearBtn) {
    clearBtn.addEventListener('click', () => {
      boardModel.filterLabels = [];
      persistBoard();
      renderAll();
    });
  }
}

function isCardVisible(cardId) {
  const card = boardModel.cards[cardId];
  if (!card) return false;

  // Search filter
  if (boardModel.searchQuery) {
    const q = boardModel.searchQuery.toLowerCase();
    if (!card.title.toLowerCase().includes(q)) return false;
  }

  // Label filter — only show cards that have at least one selected label
  if (boardModel.filterLabels.length > 0) {
    const hasMatchingLabel = card.labels.some(l => boardModel.filterLabels.includes(l));
    if (!hasMatchingLabel) return false;
  }

  return true;
}

function renderBoard() {
  let html = '';

  boardModel.columns.forEach(col => {
    const wipExceeded = col.wipLimit !== null && col.cardIds.length > col.wipLimit;
    const visibleCards = col.cardIds.filter(id => isCardVisible(id));
    const isEmpty = col.cardIds.length === 0;

    html += `<div class="column ${wipExceeded ? 'wip-exceeded' : ''}" data-column-id="${col.id}">`;

    // Column header
    html += `<div class="column-header" data-drag-column="${col.id}">
      <div class="column-title-group">
        <span class="column-title" data-edit-col="${col.id}">${escapeHtml(col.title)}</span>
        <span class="card-count">${col.cardIds.length}</span>
      </div>
      <button class="column-menu-btn" data-open-menu="${col.id}">⋯</button>
    </div>`;

    // Card list
    html += `<div class="card-list ${isEmpty ? 'empty-column' : ''}" data-cardlist="${col.id}">`;

    col.cardIds.forEach(cardId => {
      const card = boardModel.cards[cardId];
      if (!card) return;

      const visible = isCardVisible(cardId);
      html += renderCard(card, !visible);
    });

    html += `</div>`; // card-list

    // Add card button
    html += `<div class="add-card-row">
      <button class="add-card-btn" data-add-card="${col.id}">+ Add a card</button>
    </div>`;

    html += `</div>`; // column
  });

  // Add column button
  html += `<button class="add-column-btn" id="addColumnBtn">+ Add another column</button>`;

  boardContainer.innerHTML = html;

  bindBoardEvents();
}

function renderCard(card, hidden) {
  const labelsHtml = card.labels.map(l => {
    const labelDef = CONFIG.LABELS.find(ld => ld.name === l);
    const color = labelDef ? labelDef.color : '#999';
    return `<span class="label-chip" style="background:${color}" title="${escapeHtml(l)}"></span>`;
  }).join('');

  const descHtml = card.description ? `<div class="card-description">${escapeHtml(card.description)}</div>` : '';

  let dueDateHtml = '';
  if (card.dueDate) {
    const overdueClass = isOverdue(card.dueDate) ? 'overdue' : '';
    dueDateHtml = `<span class="due-date ${overdueClass}">📅 ${formatDate(card.dueDate)}</span>`;
  }

  return `<div class="card ${hidden ? 'hidden-by-filter' : ''}" data-card-id="${card.id}">
    <div class="card-actions">
      <button class="card-action-btn" data-edit-card="${card.id}" title="Edit">✏️</button>
      <button class="card-action-btn" data-delete-card="${card.id}" title="Delete">🗑️</button>
    </div>
    ${labelsHtml ? `<div class="card-labels">${labelsHtml}</div>` : ''}
    <div class="card-title" data-inline-edit="${card.id}">${escapeHtml(card.title)}</div>
    ${descHtml}
    <div class="card-meta">
      ${dueDateHtml}
    </div>
  </div>`;
}

function escapeHtml(str) {
  const div = document.createElement('div');
  div.textContent = str;
  return div.innerHTML;
}

/* ============================================================
   EVENT BINDING
   ============================================================ */
function bindBoardEvents() {
  // Add card buttons
  boardContainer.querySelectorAll('[data-add-card]').forEach(btn => {
    btn.addEventListener('click', () => showAddCardForm(btn.dataset.addCard));
  });

  // Delete card buttons
  boardContainer.querySelectorAll('[data-delete-card]').forEach(btn => {
    btn.addEventListener('click', (e) => {
      e.stopPropagation();
      deleteCard(btn.dataset.deleteCard);
    });
  });

  // Edit card buttons
  boardContainer.querySelectorAll('[data-edit-card]').forEach(btn => {
    btn.addEventListener('click', (e) => {
      e.stopPropagation();
      showEditCardModal(btn.dataset.editCard);
    });
  });

  // Inline title editing
  boardContainer.querySelectorAll('[data-inline-edit]').forEach(el => {
    el.addEventListener('dblclick', (e) => {
      e.stopPropagation();
      startInlineTitleEdit(el, el.dataset.inlineEdit);
    });
  });

  // Column menu buttons
  boardContainer.querySelectorAll('[data-open-menu]').forEach(btn => {
    btn.addEventListener('click', (e) => {
      e.stopPropagation();
      toggleColumnMenu(btn.dataset.openMenu);
    });
  });

  // Column title editing
  boardContainer.querySelectorAll('[data-edit-col]').forEach(el => {
    el.addEventListener('dblclick', (e) => {
      e.stopPropagation();
      startColumnTitleEdit(el, el.dataset.editCol);
    });
  });

  // Add column button
  const addColBtn = document.getElementById('addColumnBtn');
  if (addColBtn) {
    addColBtn.addEventListener('click', () => addColumn());
  }

  // Search box
  searchBoxEl.value = boardModel.searchQuery || '';
  searchBoxEl.oninput = () => {
    boardModel.searchQuery = searchBoxEl.value;
    persistBoard();
    renderAll();
  };
}

/* ============================================================
   CARD CRUD
   ============================================================ */
function showAddCardForm(columnId) {
  const columnEl = document.querySelector(`[data-column-id="${columnId}"]`);
  if (!columnEl) return;

  const addRow = columnEl.querySelector('.add-card-row');
  addRow.innerHTML = `
    <div class="inline-form">
      <textarea id="newCardTitle" placeholder="Enter a title for this card..." autofocus></textarea>
      <div class="form-actions">
        <button class="btn-primary" id="submitNewCard">Add card</button>
        <button class="btn-secondary" id="cancelNewCard">Cancel</button>
      </div>
    </div>`;

  const textarea = document.getElementById('newCardTitle');
  textarea.focus();

  function submit() {
    const title = textarea.value.trim();
    if (!title) return;
    addCard(columnId, title);
    renderAll();
  }

  document.getElementById('submitNewCard').addEventListener('click', submit);
  document.getElementById('cancelNewCard').addEventListener('click', () => renderAll());
  textarea.addEventListener('keydown', (e) => {
    if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); submit(); }
    if (e.key === 'Escape') renderAll();
  });
}

function addCard(columnId, title, description = '', labels = [], dueDate = '') {
  const cardId = generateId();
  boardModel.cards[cardId] = {
    id: cardId,
    title,
    description,
    labels,
    dueDate,
  };

  // Find column and append card
  const col = boardModel.columns.find(c => c.id === columnId);
  if (col) col.cardIds.push(cardId);

  persistBoard();
}

function deleteCard(cardId) {
  saveUndoSnapshot();
  const card = boardModel.cards[cardId];
  if (!card) return;

  // Remove from all columns
  boardModel.columns.forEach(col => {
    col.cardIds = col.cardIds.filter(id => id !== cardId);
  });

  delete boardModel.cards[cardId];
  persistBoard();
  renderAll();
  showToast('Card deleted', restoreFromUndo);
}

function showEditCardModal(cardId) {
  const card = boardModel.cards[cardId];
  if (!card) return;

  const overlay = document.createElement('div');
  overlay.className = 'modal-overlay';

  const labelOptionsHtml = CONFIG.LABELS.map(l => {
    const selected = card.labels.includes(l.name);
    return `<div class="label-option ${selected ? 'selected' : ''}" 
                 style="background:${l.color}" data-label="${l.name}" title="${l.name}"></div>`;
  }).join('');

  overlay.innerHTML = `
    <div class="modal">
      <h2>Edit Card</h2>
      <input type="text" id="editCardTitleInput" value="${escapeHtml(card.title)}" 
             style="width:100%;padding:8px;border:1px solid #ddd;border-radius:var(--border-radius);font-size:14px;font-family:inherit;outline:none;margin-bottom:8px;">
      <textarea id="editCardDescInput">${escapeHtml(card.description)}</textarea>
      <div class="form-group">
        <label>Labels</label>
        <div class="label-picker" id="editLabelPicker">${labelOptionsHtml}</div>
      </div>
      <div class="form-group">
        <label>Due Date</label>
        <input type="date" class="due-date-input" id="editCardDueDate" value="${card.dueDate || ''}">
      </div>
      <div class="modal-actions">
        <button class="btn-secondary" id="cancelEditCard">Cancel</button>
        <button class="btn-primary" id="saveEditCard">Save</button>
      </div>
    </div>`;

  document.body.appendChild(overlay);

  // Label picker toggle
  overlay.querySelectorAll('.label-option').forEach(opt => {
    opt.addEventListener('click', () => opt.classList.toggle('selected'));
  });

  function save() {
    card.title = document.getElementById('editCardTitleInput').value.trim() || card.title;
    card.description = document.getElementById('editCardDescInput').value.trim();
    card.labels = Array.from(overlay.querySelectorAll('.label-option.selected')).map(el => el.dataset.label);
    card.dueDate = document.getElementById('editCardDueDate').value;
    persistBoard();
    renderAll();
  }

  overlay.querySelector('#saveEditCard').addEventListener('click', save);
  overlay.querySelector('#cancelEditCard').addEventListener('click', () => overlay.remove());
  overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); });

  document.getElementById('editCardTitleInput').focus();
}

function startInlineTitleEdit(el, cardId) {
  const card = boardModel.cards[cardId];
  if (!card) return;

  el.innerHTML = `<input class="card-title-input" value="${escapeHtml(card.title)}">`;
  const input = el.querySelector('input');
  input.focus();
  input.select();

  function commit() {
    const newTitle = input.value.trim();
    if (newTitle) card.title = newTitle;
    persistBoard();
    renderAll();
  }

  function cancel() {
    renderAll();
  }

  input.addEventListener('keydown', (e) => {
    if (e.key === 'Enter') commit();
    if (e.key === 'Escape') cancel();
  });
  input.addEventListener('blur', () => setTimeout(commit, 100));
}

/* ============================================================
   COLUMN CRUD
   ============================================================ */
function addColumn() {
  const title = prompt('Column name:');
  if (!title || !title.trim()) return;

  boardModel.columns.push({
    id: generateId(),
    title: title.trim(),
    cardIds: [],
    wipLimit: null,
  });

  persistBoard();
  renderAll();
}

function deleteColumn(columnId) {
  if (boardModel.columns.length <= 1) return; // keep at least one

  saveUndoSnapshot();
  const col = boardModel.columns.find(c => c.id === columnId);
  if (!col) return;

  // Remove cards from model
  col.cardIds.forEach(cardId => delete boardModel.cards[cardId]);

  boardModel.columns = boardModel.columns.filter(c => c.id !== columnId);
  persistBoard();
  renderAll();
  showToast('Column deleted', restoreFromUndo);
}

function startColumnTitleEdit(el, columnId) {
  const col = boardModel.columns.find(c => c.id === columnId);
  if (!col) return;

  el.innerHTML = `<input class="column-title-input" value="${escapeHtml(col.title)}">`;
  const input = el.querySelector('input');
  input.focus();
  input.select();

  function commit() {
    const newTitle = input.value.trim();
    if (newTitle) col.title = newTitle;
    persistBoard();
    renderAll();
  }

  function cancel() { renderAll(); }

  input.addEventListener('keydown', (e) => {
    if (e.key === 'Enter') commit();
    if (e.key === 'Escape') cancel();
  });
  input.addEventListener('blur', () => setTimeout(commit, 100));
}

/* ============================================================
   COLUMN DROPDOWN MENU
   ============================================================ */
let activeMenuColumnId = null;

function toggleColumnMenu(columnId) {
  // Close any existing menu
  closeAllMenus();

  const colHeader = document.querySelector(`[data-column-id="${columnId}"] .column-header`);
  if (!colHeader) return;

  const col = boardModel.columns.find(c => c.id === columnId);
  if (!col) return;

  const menu = document.createElement('div');
  menu.className = 'column-dropdown';
  menu.innerHTML = `
    <button id="menuRenameCol">Rename</button>
    <div class="wip-input-row">
      <label>WIP Limit:</label>
      <input type="number" min="0" max="99" value="${col.wipLimit === null ? '' : col.wipLimit}" id="menuWipInput">
      <button id="menuSetWip">Set</button>
    </div>
    <button id="menuClearWip">Clear WIP Limit</button>
    <button class="danger" id="menuDeleteCol">Delete column</button>`;

  colHeader.style.position = 'relative';
  colHeader.appendChild(menu);
  activeMenuColumnId = columnId;

  document.getElementById('menuRenameCol').addEventListener('click', () => {
    closeAllMenus();
    const titleEl = document.querySelector(`[data-column-id="${columnId}"] .column-title`);
    if (titleEl) startColumnTitleEdit(titleEl, columnId);
  });

  document.getElementById('menuSetWip').addEventListener('click', () => {
    const val = parseInt(document.getElementById('menuWipInput').value);
    col.wipLimit = isNaN(val) || val <= 0 ? null : val;
    closeAllMenus();
    persistBoard();
    renderAll();
  });

  document.getElementById('menuClearWip').addEventListener('click', () => {
    col.wipLimit = null;
    closeAllMenus();
    persistBoard();
    renderAll();
  });

  document.getElementById('menuDeleteCol').addEventListener('click', () => {
    if (confirm(`Delete column "${col.title}" and all its cards?`)) {
      deleteColumn(columnId);
    }
    closeAllMenus();
  });
}

function closeAllMenus() {
  document.querySelectorAll('.column-dropdown').forEach(m => m.remove());
  activeMenuColumnId = null;
}

// Close menus on outside click
document.addEventListener('click', (e) => {
  if (!e.target.closest('.column-menu-btn') && !e.target.closest('.column-dropdown')) {
    closeAllMenus();
  }
});

/* ============================================================
   DRAG AND DROP — CARD LEVEL
   Uses pointer events for pixel-precise control.
   ============================================================ */
let dragState = null; // null or { type: 'card'|'column', ... }

boardContainer.addEventListener('pointerdown', onPointerDown);

function onPointerDown(e) {
  // Only left button
  if (e.button !== 0) return;

  const cardEl = e.target.closest('.card');
  const colHeaderEl = e.target.closest('[data-drag-column]');

  // Don't start drag from buttons/inputs inside cards
  if (e.target.closest('button, input, textarea')) return;

  if (cardEl && !cardEl.classList.contains('hidden-by-filter')) {
    startCardDrag(e, cardEl);
  } else if (colHeaderEl) {
    // Check it's not clicking a button inside the header
    if (!e.target.closest('.column-menu-btn') && !e.target.closest('.column-title-input')) {
      startColumnDrag(e, colHeaderEl);
    }
  }
}

/* --- Card Drag --- */
function startCardDrag(e, cardEl) {
  const cardId = cardEl.dataset.cardId;
  const columnId = findParentColumnId(cardEl);
  if (!columnId) return;

  // Record original position for cancel
  const originalModel = deepClone(boardModel);

  dragState = {
    type: 'card',
    cardId,
    originColumnId: columnId,
    originIndex: getCardIndexInColumn(columnId, cardId),
    startX: e.clientX,
    startY: e.clientY,
    hasMoved: false,
    originalModel,
  };

  // Create ghost element (visual copy that follows cursor)
  const ghost = createDragGhost(cardEl);
  dragState.ghost = ghost;
  document.body.appendChild(ghost);

  // Mark original card as dragging (dimmed placeholder)
  cardEl.classList.add('dragging');

  window.addEventListener('pointermove', onCardPointerMove, { passive: false });
  window.addEventListener('pointerup', onCardPointerUp);
  window.addEventListener('keydown', onCardDragKeydown);

  e.preventDefault();
}

function createDragGhost(cardEl) {
  const ghost = cardEl.cloneNode(true);
  ghost.className = 'card drag-ghost';
  ghost.style.position = 'fixed';
  ghost.style.zIndex = '9999';
  ghost.style.pointerEvents = 'none';
  ghost.style.width = cardEl.offsetWidth + 'px';
  ghost.style.transform = 'rotate(3deg) scale(1.02)';
  ghost.style.boxShadow = '0 8px 24px rgba(0,0,0,0.2)';
  // Store width for centering
  dragState._ghostWidth = cardEl.offsetWidth;
  return ghost;
}

function onCardPointerMove(e) {
  if (!dragState || dragState.type !== 'card') return;

  const dx = e.clientX - dragState.startX;
  const dy = e.clientY - dragState.startY;

  // Check threshold before starting actual drag
  if (!dragState.hasMoved && Math.abs(dx) < CONFIG.DRAG_THRESHOLD && Math.abs(dy) < CONFIG.DRAG_THRESHOLD) {
    return;
  }

  dragState.hasMoved = true;

  // Move ghost to follow cursor — centered horizontally, offset slightly above finger
  const ghost = dragState.ghost;
  if (ghost) {
    const gw = dragState._ghostWidth || 280;
    ghost.style.left = (e.clientX - gw / 2) + 'px';
    ghost.style.top = (e.clientY - 30) + 'px';
  }

  // Auto-scroll
  handleAutoScroll(e);

  // Find drop target and show placeholder
  updateDropIndicator(e);
}

function onCardPointerUp(e) {
  if (!dragState || dragState.type !== 'card') return;

  window.removeEventListener('pointermove', onCardPointerMove);
  window.removeEventListener('pointerup', onCardPointerUp);
  window.removeEventListener('keydown', onCardDragKeydown);

  const ghost = dragState.ghost;
  if (ghost) ghost.remove();

  // Remove placeholder
  removeDropIndicator();

  if (!dragState.hasMoved) {
    // Didn't actually drag — just a click, restore card appearance
    const cardEl = document.querySelector(`[data-card-id="${dragState.cardId}"]`);
    if (cardEl) cardEl.classList.remove('dragging');
    dragState = null;
    return;
  }

  // Determine drop target
  const dropTarget = getDropTarget(e.clientX, e.clientY);

  if (dropTarget) {
    saveUndoSnapshot();
    moveCardInModel(dragState.cardId, dropTarget.columnId, dropTarget.index);
    persistBoard();
  } else {
    // No valid target — restore original model
    boardModel = dragState.originalModel;
    persistBoard();
  }

  const droppedCardId = dragState.cardId;
  renderAll();
  
  // Settle animation: briefly highlight the dropped card
  requestAnimationFrame(() => {
    const droppedEl = document.querySelector(`[data-card-id="${droppedCardId}"]`);
    if (droppedEl) {
      droppedEl.style.transition = 'transform 200ms ease, box-shadow 200ms ease';
      droppedEl.style.transform = 'scale(1.03)';
      droppedEl.style.boxShadow = '0 4px 12px rgba(0,121,191,0.3)';
      requestAnimationFrame(() => {
        droppedEl.style.transform = '';
        droppedEl.style.boxShadow = '';
        setTimeout(() => { droppedEl.style.transition = ''; }, 250);
      });
    }
  });
  
  dragState = null;
}

function onCardDragKeydown(e) {
  if (!dragState || dragState.type !== 'card') return;
  if (e.key === 'Escape') {
    // Cancel: restore original model
    boardModel = dragState.originalModel;
    persistBoard();

    window.removeEventListener('pointermove', onCardPointerMove);
    window.removeEventListener('pointerup', onCardPointerUp);
    window.removeEventListener('keydown', onCardDragKeydown);

    if (dragState.ghost) dragState.ghost.remove();
    removeDropIndicator();

    renderAll();
    dragState = null;
  }
}

function getCardIndexInColumn(columnId, cardId) {
  const col = boardModel.columns.find(c => c.id === columnId);
  if (!col) return -1;
  return col.cardIds.indexOf(cardId);
}

function findParentColumnId(el) {
  let current = el;
  while (current && current !== document.body) {
    const colEl = current.closest('.column');
    if (colEl) return colEl.dataset.columnId;
    current = current.parentElement;
  }
  return null;
}

function moveCardInModel(cardId, targetColumnId, targetIndex) {
  // Find source column and remove card
  let sourceCol = null;
  boardModel.columns.forEach(col => {
    const idx = col.cardIds.indexOf(cardId);
    if (idx !== -1) {
      sourceCol = col;
      col.cardIds.splice(idx, 1);
    }
  });

  if (!sourceCol) return;

  // Find target column and insert at index
  const targetCol = boardModel.columns.find(c => c.id === targetColumnId);
  if (!targetCol) return;

  // Clamp index
  const idx = Math.min(Math.max(0, targetIndex), targetCol.cardIds.length);
  targetCol.cardIds.splice(idx, 0, cardId);
}

/* --- Drop Indicator (placeholder line) --- */
let dropIndicatorEl = null;

function updateDropIndicator(e) {
  removeDropIndicator();

  const target = getDropTarget(e.clientX, e.clientY);
  if (!target) return;

  // Create indicator element
  const indicator = document.createElement('div');
  indicator.className = 'drop-placeholder';
  dropIndicatorEl = indicator;

  const cardList = document.querySelector(`[data-cardlist="${target.columnId}"]`);
  if (!cardList) return;

  // Insert at the right position in the DOM
  const cardsInColumn = Array.from(cardList.querySelectorAll('.card:not(.dragging):not(.hidden-by-filter)'));
  
  if (cardsInColumn.length === 0 || target.index >= cardsInColumn.length) {
    cardList.appendChild(indicator);
  } else {
    const refCard = cardsInColumn[target.index];
    cardList.insertBefore(indicator, refCard);
  }

  // Highlight the column being dragged over
  highlightDropColumn(target.columnId);
}

function removeDropIndicator() {
  if (dropIndicatorEl && dropIndicatorEl.parentNode) {
    dropIndicatorEl.remove();
  }
  dropIndicatorEl = null;
  unhighlightAllColumns();
}

function highlightDropColumn(columnId) {
  unhighlightAllColumns();
  const colEl = document.querySelector(`[data-column-id="${columnId}"]`);
  if (colEl) colEl.classList.add('drag-over-column');
}

function unhighlightAllColumns() {
  document.querySelectorAll('.drag-over-column').forEach(el => el.classList.remove('drag-over-column'));
}

/* --- Drop Target Calculation --- */
function getDropTarget(x, y) {
  // Find which column the cursor is over
  const columns = Array.from(document.querySelectorAll('.column'));
  
  for (const colEl of columns) {
    const rect = colEl.getBoundingClientRect();
    
    // Check if x is within column horizontally
    if (x < rect.left - 10 || x > rect.right + 10) continue;

    const cardList = colEl.querySelector('.card-list');
    if (!cardList) continue;

    const visibleCards = Array.from(cardList.querySelectorAll('.card:not(.dragging):not(.hidden-by-filter)'));

    // Empty column — drop here at index 0
    if (visibleCards.length === 0) {
      return { columnId: colEl.dataset.columnId, index: 0 };
    }

    // Find the card we're closest to / between
    for (let i = 0; i < visibleCards.length; i++) {
      const cardRect = visibleCards[i].getBoundingClientRect();
      const cardMidY = cardRect.top + cardRect.height / 2;

      if (y < cardMidY) {
        // Drop before this card
        return { columnId: colEl.dataset.columnId, index: i };
      }
    }

    // Past all cards — append at end
    const lastCard = visibleCards[visibleCards.length - 1];
    if (y >= lastCard.getBoundingClientRect().top + lastCard.getBoundingClientRect().height / 2) {
      return { columnId: colEl.dataset.columnId, index: visibleCards.length };
    }

    // Fallback
    return { columnId: colEl.dataset.columnId, index: visibleCards.length };
  }

  return null;
}

/* --- Auto-Scroll During Drag --- */
function handleAutoScroll(e) {
  const margin = CONFIG.AUTO_SCROLL_MARGIN;
  const speed = CONFIG.AUTO_SCROLL_SPEED;
  const boardRect = boardContainer.getBoundingClientRect();

  // Horizontal scroll (board level)
  if (e.clientX > boardRect.right - margin) {
    boardContainer.scrollBy(speed, 0);
  } else if (e.clientX < boardRect.left + margin) {
    boardContainer.scrollBy(-speed, 0);
  }

  // Vertical scroll (column level) — find column under cursor
  const columns = document.querySelectorAll('.column');
  for (const col of columns) {
    const rect = col.getBoundingClientRect();
    if (e.clientX >= rect.left && e.clientX <= rect.right) {
      const cardList = col.querySelector('.card-list');
      if (!cardList) continue;

      if (e.clientY > rect.bottom - margin) {
        cardList.scrollTop += speed;
      } else if (e.clientY < rect.top + margin) {
        cardList.scrollTop -= speed;
      }
      break;
    }
  }
}

/* --- Column Drag-to-Reorder --- */
function startColumnDrag(e, colHeaderEl) {
  const columnId = colHeaderEl.dataset.dragColumn;
  if (!columnId) return;

  dragState = {
    type: 'column',
    columnId,
    originIndex: boardModel.columns.findIndex(c => c.id === columnId),
    startX: e.clientX,
    startY: e.clientY,
    hasMoved: false,
    originalModel: deepClone(boardModel),
  };

  window.addEventListener('pointermove', onColumnPointerMove, { passive: false });
  window.addEventListener('pointerup', onColumnPointerUp);
  window.addEventListener('keydown', onColumnDragKeydown);

  e.preventDefault();
}

function onColumnPointerMove(e) {
  if (!dragState || dragState.type !== 'column') return;

  const dx = e.clientX - dragState.startX;
  const dy = e.clientY - dragState.startY;

  if (!dragState.hasMoved && Math.abs(dx) < CONFIG.DRAG_THRESHOLD && Math.abs(dy) < CONFIG.DRAG_THRESHOLD) {
    return;
  }

  dragState.hasMoved = true;

  // Visual feedback: highlight columns being swapped over
  const allColEls = Array.from(document.querySelectorAll('.column'));
  
  for (const colEl of allColEls) {
    const rect = colEl.getBoundingClientRect();
    if (e.clientX > rect.left && e.clientX < rect.right) {
      // Find the column index in model
      const targetIndex = boardModel.columns.findIndex(c => c.id === colEl.dataset.columnId);
      
      if (targetIndex !== dragState.originIndex) {
        // Swap columns in model temporarily for visual feedback
        swapColumnsInModel(dragState.originIndex, targetIndex);
        renderAll();
        
        // Re-mark the dragged column header visually
        const draggedColEl = document.querySelector(`[data-column-id="${dragState.columnId}"]`);
        if (draggedColEl) {
          draggedColEl.style.opacity = '0.5';
          draggedColEl.style.transform = 'scale(1.02)';
        }
      }
      break;
    }
  }

  handleAutoScroll(e);
}

function onColumnPointerUp(e) {
  if (!dragState || dragState.type !== 'column') return;

  window.removeEventListener('pointermove', onColumnPointerMove);
  window.removeEventListener('pointerup', onColumnPointerUp);
  window.removeEventListener('keydown', onColumnDragKeydown);

  if (!dragState.hasMoved) {
    dragState = null;
    return;
  }

  // Commit the swap (already done in model during move)
  persistBoard();
  renderAll();
  dragState = null;
}

function onColumnDragKeydown(e) {
  if (!dragState || dragState.type !== 'column') return;
  if (e.key === 'Escape') {
    boardModel = dragState.originalModel;
    persistBoard();

    window.removeEventListener('pointermove', onColumnPointerMove);
    window.removeEventListener('pointerup', onColumnPointerUp);
    window.removeEventListener('keydown', onColumnDragKeydown);

    renderAll();
    dragState = null;
  }
}

function swapColumnsInModel(fromIndex, toIndex) {
  if (fromIndex === toIndex || fromIndex < 0 || toIndex < 0) return;
  const cols = boardModel.columns;
  const temp = cols[fromIndex];
  cols.splice(fromIndex, 1);
  cols.splice(toIndex, 0, temp);
}

/* ============================================================
   INITIALIZATION
   ============================================================ */
function init() {
  loadBoard();
  
  // If no saved state, add some sample cards for demo
  if (Object.keys(boardModel.cards).length === 0) {
    const colIds = boardModel.columns.map(c => c.id);
    
    // Sample cards for Backlog
    addCard(colIds[0], 'Research competitor features', '', ['Feature'], '');
    addCard(colIds[0], 'Write API documentation', '', ['Documentation'], '2025-02-15');
    addCard(colIds[0], 'Fix login page crash on mobile', '', ['Bug', 'Urgent'], '2025-01-20');

    // Sample cards for To Do
    addCard(colIds[1], 'Design new dashboard layout', '', ['Design'], '');
    addCard(colIds[1], 'Implement dark mode toggle', '', ['Enhancement'], '2025-02-01');

    // Sample cards for In Progress
    addCard(colIds[2], 'Set up CI/CD pipeline', 'Configure GitHub Actions for automated testing and deployment.', ['Feature'], '');
    addCard(colIds[2], 'Refactor authentication module', '', ['Enhancement'], '2025-01-30');

    // Sample cards for Review
    addCard(colIds[3], 'Add keyboard shortcuts', 'Support Space to jump, Enter to confirm.', ['Feature'], '');

    persistBoard();
  }

  renderAll();
}

init();
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen3.6-27b-mtp","provider":"lmstudio","persona":"full","sessionId":"67adfe99-fc12-4408-b9f1-0ddfbe0dea1f","tokensIn":826767,"tokensOut":28212,"tokensTotal":854979,"turns":27,"toolCalls":26,"failedToolCalls":0,"timestamp":"2026-07-29T22:53:44.467Z"} -->