← Kanban results

Kanban

Ornith 1.5 35B A3B · 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 43,943 bytes · SHA-256 ccd6fe067cdc
<!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>
/* =========================================================
   TUNABLE CONSTANTS (visual)
   ========================================================= */
:root{
  --bg:#eef1f6; --board-bg:#e4e7ed; --col-bg:#f5f7fa; --col-bg-2:#ffffff;
  --card-bg:#ffffff; --text:#1f2430; --text-soft:#6b7280; --text-faint:#9aa1ad;
  --line:#e2e6ec; --accent:#4f6ef7; --accent-soft:#e8ecff;
  --danger:#ef4444; --danger-soft:#fdeaea;
  --shadow:0 1px 2px rgba(16,24,40,.06),0 1px 3px rgba(16,24,40,.1);
  --shadow-lg:0 10px 25px rgba(16,24,40,.18),0 4px 10px rgba(16,24,40,.1);
  --radius:12px; --radius-sm:8px; --col-w:288px;
}
*{box-sizing:border-box}
html,body{height:100%}
body{
  margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
  background:var(--bg);color:var(--text);overflow:hidden;
  -webkit-font-smoothing:antialiased;
}
button{font-family:inherit}

/* ===================== HEADER ===================== */
#header{
  position:sticky;top:0;z-index:40;display:flex;align-items:center;gap:14px;
  padding:12px 20px;background:rgba(255,255,255,.85);backdrop-filter:blur(8px);
  border-bottom:1px solid var(--line);
}
#header h1{font-size:18px;margin:0;font-weight:800;letter-spacing:-.02em}
#header .brand{color:var(--accent)}
#search{
  margin-left;display:flex;align-items:center;
  background:var(--card-bg);border:1px solid var(--line);border-radius:20px;padding:7px 12px;min-width:220px;
}
#search input{border:0;outline:0;background:transparent;width:100%;font-size:13px;color:var(--text)}
#search svg{flex:0 0 auto;margin-right:8px}
.btn{
  border:1px solid var(--line);background:var(--card-bg);color:var(--text);
  border-radius:var(--radius-sm);padding:8px 13px;font-size:13px;font-weight:600;cursor:pointer;
  transition:background .12s,border-color .12s,transform .05s;
}
.btn:hover{background:#f3f5f9}
.btn:active{transform:translateY(1px)}
.btn-icon{padding:6px 8px;line-height:1}

/* ===================== FILTER BAR ===================== */
#filterbar{
  display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:9px 20px;
  border-bottom:1px solid var(--line);background:rgba(255,255,255,.6);z-index:30;
}
.filter-chip{
  display:inline-flex;align-items:center;gap:6px;padding:4px 6px 4px 8px;
  border-radius:20px;font-size:12px;font-weight:600;border:1px solid var(--line);
  background:var(--card-bg);cursor:pointer;color:var(--text);transition:box-shadow .12s;
}
.filter-chip:hover{box-shadow:var(--shadow)}
.filter-chip .dot{width:12px;height:12px;border-radius:50%;flex:0 0 auto}
.filter-chip .x{opacity:.5;font-size:13px}
.filter-chip .x:hover{opacity:1}
.filter-label{font-size:12px;color:var(--text-soft);font-weight:700;margin-right:2px}
#filterHint{font-size:12px;color:var(--text-faint);margin-left:auto}

/* ===================== BOARD ===================== */
#boardwrap{
  position:relative;height:calc(100vh - 96px);overflow:auto;scroll-behavior:smooth;
}
#board{display:flex;gap:16px;padding:18px 20px 24px;align-items:flex-start}

/* ===================== COLUMNS ===================== */
.column{
  flex:0 0 var(--col-w);width:var(--col-w);background:var(--col-bg);
  border-radius:var(--radius);display:flex;flex-direction:column;max-height:100%;
  border:1px solid transparent;transition:border-color .12s,box-shadow .12s,background .12s;
}
.column.drag-col{cursor:grabbing}
.column.drag-over{border-color:var(--accent);background:var(--col-bg-2);box-shadow:0 0 0 1px var(--accent) inset}
.column.wip-exceeded{background:var(--danger-soft)}
.column.wip-exceeded .col-header{color:var(--danger)}
.col-header{
  display:flex;align-items:center;gap:8px;padding:10px 12px 8px;position:relative;
  cursor:grab;user-select:none;
}
.col-header:active{cursor:grabbing}
.col-title{font-size:13px;font-weight:700;flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;letter-spacing:-.01em}
.col-count{
  font-size:11px;font-weight:700;color:var(--text-soft);background:#e9ecf1;
  border-radius:10px;padding:1px 7px;min-width:20px;text-align:center;
}
.col-count.warn{background:var(--danger-soft);color:var(--danger)}
.col-menu-btn{opacity:.4;transition:opacity .12s}
.col-menu-btn:hover{opacity:1}
.wip-badge{font-size:10px;font-weight:700;color:var(--danger);border:1px solid var(--danger);border-radius:6px;padding:1px 6px;display:none}
.column.wip-exceeded .wip-badge{display:inline-flex}

.col-body{position:relative;flex:1;overflow:auto;padding:4px 10px 12px;min-height:44px}
.col-body.drag-target{background:rgba(79,110,247,.05)}
.card-list{display:flex;flex-direction:column;gap:9px;min-height:8px}
.empty-drop{
  display:none;border:2px dashed var(--line);border-radius:var(--radius-sm);
  padding:26px 12px;text-align:center;color:var(--text-faint);font-size:13px;font-weight:500;margin-top:4px;
}
.column.drag-over .empty-drop{border-color:var(--accent);color:var(--accent);display:block}

/* ===================== CARDS ===================== */
.card{
  position:relative;background:var(--card-bg);border-radius:var(--radius-sm);
  box-shadow:var(--shadow);padding:10px 11px;cursor:grab;user-select:none;
  transition:box-shadow .15s transform .15s,opacity .12s;border:1px solid rgba(0,0,0,.03);
}
.card:hover{box-shadow:var(--shadow-lg);transform:translateY(-1px)}
.card:active{cursor:grabbing}
.card.dragging{opacity:.35;box-shadow:none;transform:none!important;border:1px dashed var(--line);background:#fafbfd}
.card.hidden-filter{display:none}
.card-title{font-size:13.5px;font-weight:600;line-height:1.35;margin:1px 0 6px;word-wrap:break-word}
.card-desc{font-size:12px;color:var(--text-soft);line-height:1.4;margin-bottom:8px;
  display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
.card-labels{display:flex;flex-wrap:wrap;gap:5px;margin-bottom:8px}
.label-chip{width:22px;height:18px;border-radius:5px;font-size:9px;font-weight:700;color:#fff;
  display:flex;align-items:center;justify-content:center;padding:0 3px}
.card-meta{display:flex;align-items:center;gap:10px}
.due{font-size:11px;font-weight:600;color:var(--text-soft);display:inline-flex;align-items:center;gap:4px}
.due.overdue{color:var(--danger)}
.due.today{color:#d97706}
.card-tools{position:absolute;top:6px;right:6px;display:flex;gap:2px;opacity:0;transition:opacity .12s}
.card:hover .card-tools{opacity:1}
.tool{border:0;background:transparent;color:var(--text-faint);cursor:pointer;font-size:14px;line-height:1;
  border-radius:5px;padding:2px 5px}
.tool:hover{background:var(--danger-soft);color:var(--danger)}
.tool.edit:hover{background:#eef1f6;color:var(--text)}

.card-edit,.col-edit{
  width:100%;border:0;outline:0;background:transparent;font:inherit;color:var(--text);
  border-bottom:2px solid var(--accent);padding:0;
}
.card-edit{font-weight:600;font-size:13.5px}
.col-edit{font-weight:700;font-size:13px}

.add-card{
  margin:4px 2px 8px;border:1px dashed var(--line);background:transparent;color:var(--text-soft);
  border-radius:var(--radius-sm);padding:8px;font-size:12.5px;font-weight:600;cursor:pointer;
  text-align:left;transition:background .12s,border-color .12s,color .12s;
}
.add-card:hover{background:#fff;border-color:var(--accent);color:var(--accent)}
.add-col{
  flex:0 0 var(--col-w);width:var(--col-w);border:1px dashed var(--line);border-radius:var(--radius);
  background:rgba(255,255,255,.35);color:var(--text-soft);cursor:pointer;
  display:flex;align-items:center;justify-content:center;gap:6px;min-height:80px;font-weight:600;font-size:13.5px;
  transition:background .12s,border-color .12s,color .12s;
}
.add-col:hover{background:#fff;border-color:var(--accent);color:var(--accent)}

/* ===================== DRAG GHOST ===================== */
#dragghost{
  position:fixed;z-index:60;pointer-events:none;width:calc(var(--col-w) - 8px);
  background:var(--card-bg);border-radius:var(--radius-sm);box-shadow:var(--shadow-lg);
  padding:12px 13px;opacity:.96;transform:rotate(-1.5deg);transition:transform .1s;
}
.drop-indicator{
  height:3px;border-radius:3px;background:var(--accent);margin:0;
  box-shadow:0 0 0 4px rgba(79,110,247,.15);transition:height .1s;
}

/* ===================== CARD EDITOR PANEL ===================== */
#editor{
  position:fixed;z-index:70;width:320px;max-width:90vw;
  background:#fff;border:1px solid var(--line);border-radius:var(--radius);
  box-shadow:var(--shadow-lg);padding:14px;
}
#editor .ed-title{width:100%;font-weight:700;font-size:15px;border:1px solid transparent;background:#f7f9fc;border-radius:7px;padding:8px;margin-bottom:10px}
#editor .ed-title:focus{border-color:var(--accent);background:#fff;outline:0}
#editor .ed-desc{width:100%;min-height:70px;font-size:13px;line-height:1.4;border:1px solid var(--line);border-radius:7px;padding:8px;resize:vertical;font-family:inherit}
#editor .ed-desc:focus{border-color:var(--accent);outline:0}
#editor .ed-row{margin-bottom:12px}
#editor .ed-label{font-size:11px;font-weight:700;color:var(--text-soft);text-transform:uppercase;letter-spacing:.04em;margin-bottom:6px;display:block}
#editor .ed-labels{display:flex;flex-wrap:wrap;gap:6px}
#editor .ed-lbl{display:flex;align-items:center;gap:6px;font-size:12px;cursor:pointer;border-radius:6px;padding:3px 6px;border:1px solid var(--line)}
#editor .ed-lbl.active{border-color:transparent}
#editor .ed-lbl input{display:none}
#editor .swatch{width:14px;height:14px;border-radius:4px}
#editor .ed-actions{display:flex;gap:8px;align-items:center;margin-top:6px}
#editor .ed-actions .spacer{flex:1}
#editor .ed-del{color:var(--danger);background:transparent;border:0;cursor:pointer;font-size:13px;font-weight:600}
#editor .ed-del:hover{text-decoration:underline}

/* ===================== MENU ===================== */
.menu{position:fixed;z-index:70;min-width:200px;background:#fff;border:1px solid var(--line);
  border-radius:var(--radius-sm);box-shadow:var(--shadow-lg);padding:7px}
.menu .row{display:flex;align-items:center;gap:8px;padding:8px 9px;border-radius:7px;font-size:13px;cursor:pointer}
.menu .row:hover{background:#f3f5f9}
.menu .row.danger{color:var(--danger)}
.menu .row.danger:hover{background:var(--danger-soft)}
.menu .sep{height:1px;background:var(--line);margin:6px 4px}
.menu input[type=text]{border:1px solid var(--line);border-radius:6px;padding:6px 8px;font-size:13px;width:100%;outline:none}
.menu input[type=date]{border:1px solid var(--line);border-radius:6px;padding:5px 7px;font-size:12px}
.menu label.lbl{display:flex;align-items:center;gap:8px;padding:7px 9px;font-size:13px;cursor:pointer;border-radius:7px}
.menu label.lbl:hover{background:#f3f5f9}
.menu label.lbl input{accent-color:var(--accent)}
.menu .swatch{width:16px;height:16px;border-radius:4px}

/* ===================== TOAST ===================== */
#toast{
  position:fixed;left:50%;bottom:26px;transform:translateX(-50%) translateY(120%);z-index:80;
  display:flex;align-items:center;gap:14px;background:#1f2430;color:#fff;border-radius:10px;
  padding:11px 16px;font-size:13.5px;font-weight:500;box-shadow:0 12px 30px rgba(0,0,0,.28);
  transition:transform .28s cubic-bezier(.2,.8,.2,1);max-width:90vw;
}
#toast.show{transform:translateX(-50%) translateY(0)}
#toast .undo{background:#fff;color:#1f2430;border:0;border-radius:7px;padding:6px 13px;font-weight:700;cursor:pointer;font-size:13px;white-space:nowrap}
#toast .undo:hover{background:#eef1f6}
</style>
</head>
<body>

<!-- ===================== HEADER ===================== -->
<div id="header">
  <h1><span class="brand">◆</span> Kanban</h1>
  <div id="search">
    <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="#6b7280" stroke-width="2.2" stroke-linecap="round"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg>
    <input id="searchInput" type="text" placeholder="Search cards…">
  </div>
  <button class="btn" id="resetBtn">Reset board</button>
</div>

<!-- ===================== FILTER BAR ===================== -->
<div id="filterbar">
  <span class="filter-label">Filter labels:</span>
  <span id="filterChips"></span>
  <span class="filter-chip" id="addFilterBtn">+ Add filter</span>
  <span id="filterHint"></span>
</div>

<!-- ===================== BOARD ===================== -->
<div id="boardwrap"><div id="board"></div></div>

<!-- ===================== TOAST ===================== -->
<div id="toast"><span id="toastMsg"></span><button class="undo" id="undoBtn">Undo</button></div>

<script>
/* =========================================================
   SECTION 1 — TUNABLE CONSTANTS
   ========================================================= */
const CFG = {
  STORAGE_KEY: 'kanban.board.v2',
  COL_WIDTH: 288,
  CARD_GAP: 9,
  SCROLL_MARGIN_H: 42,
  SCROLL_MARGIN_V: 42,
  SCROLL_SPEED: 16,
  DRAG_THRESHOLD: 5,
  LABELS: [
    { id:'urgent',  color:'#ef4444', name:'Urgent' },
    { id:'bug',     color:'#f97316', name:'Bug' },
    { id:'feature', color:'#22c55e', name:'Feature' },
    { id:'design',  color:'#a855f7', name:'Design' },
    { id:'docs',    color:'#3b82f6', name:'Docs' },
    { id:'idea',    color:'#14b8a6', name:'Idea' },
  ],
  DEFAULT_COLUMNS: [
    { id:'c_backlog',  title:'Backlog',    wip:0, cards:[
      { id:'k1', title:'Set up repository & CI', desc:'Initialize git, add CI pipeline and linting.', labels:['docs'], due:'' },
      { id:'k2',  title:'Define data model', desc:'Draft schemas for users, tasks, labels.', labels:['design'], due:'' },
    ]},
    { id:'c_todo', title:'To Do', wip:0, cards:[
      { id:'k3', title:'User authentication', desc:'Email + OAuth login flow.', labels:['feature'], due:'' },
    ]},
    { id:'c_progress', title:'In Progress', wip:5, cards:[
      { id:'k4', title:'Kanban board drag-drop', desc:'Pointer-events based DnD with live indicator.', labels:['feature','bug'], due:'' },
    ]},
    { id:'c_review', title:'Review', wip:0, cards:[] },
    { id:'c_done', title:'Done', wip:0, cards:[
      { id:'k5', title:'Project kickoff', desc:'Kickoff meeting + roadmap.', labels:['docs'], due:'' },
    ]},
  ],
};

/* =========================================================
   SECTION 2 — STATE (single source of truth)
   ========================================================= */
let board = null;
let lastSnapshot = null;
let pendingUndo = null;
let searchQuery = '';
let activeFilters = new Set();

const uid = (p='id') => p + Date.now().toString(36) + Math.random().toString(36).slice(2,7);
const deepClone = o => JSON.parse(JSON.stringify(o));
const findColumn = id => board.columns.find(c=>c.id===id);
const cardInColumn = (col, cid) => col.cards.find(c=>c.id===cid);
const labelName = id => { const l=CFG.LABELS.find(x=>x.id===id); return l?l.name:id; };
const labelColor = id => { const l=CFG.LABELS.find(x=>x.id===id); return l?l.color:'#9aa1ad'; };

/* =========================================================
   SECTION 3 — PERSISTENCE
   ========================================================= */
function save(){ try{ localStorage.setItem(CFG.STORAGE_KEY, JSON.stringify(board)); }catch(e){} }
function load(){
  try{
    const raw = localStorage.getItem(CFG.STORAGE_KEY);
    if(!raw) return seedDefault();
    const b = JSON.parse(raw);
    if(!b || !Array.isArray(b.columns)) return seedDefault();
    board = { columns: b.columns.map(c=>({
      id:c.id||uid('c'), title:c.title||'New Column', wip:+c.wip||0,
      cards:(c.cards||[]).map(k=>({
        id:k.id||uid('k'), title:k.title||'Untitled', desc:k.desc||'',
        labels:Array.isArray(k.labels)?k.labels.filter(Boolean):[], due:k.due||'',
      }))
    })).filter(c=>c.title) };
    if(!board.columns.length) return seedDefault();
    return board;
  }catch(e){ return seedDefault(); }
}
function seedDefault(){ return { columns: deepClone(CFG.DEFAULT_COLUMNS) }; }

/* =========================================================
   SECTION 4 — SNAPSHOT / UNDO
   ========================================================= */
function snapshot(){ lastSnapshot = deepClone(board); }
function scheduleUndo(label){
  pendingUndo = { label };
  showToast(label);
}
function performUndo(){
  if(!pendingUndo) return;
  board = deepClone(lastSnapshot);
  pendingUndo = null;
  render(); hideToast();
}

/* =========================================================
   SECTION 5 — RENDERING
   ========================================================= */
const boardEl = document.getElementById('board');
const boardWrap = document.getElementById('boardwrap');
const searchInput = document.getElementById('searchInput');

const esc = s => String(s).replace(/[&<>"']/g, c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c]));

function cardVisible(card){
  if(searchQuery){
    if(!(card.title.toLowerCase().includes(searchQuery) || (card.desc||'').toLowerCase().includes(searchQuery))) return false;
  }
  if(activeFilters.size){
    for(const f of activeFilters) if(!card.labels.includes(f)) return false;
  }
  return true;
}

function render(){
  boardEl.innerHTML='';
  board.columns.forEach(col=> boardEl.appendChild(renderColumn(col)));
  const addBtn=document.createElement('button');
  addBtn.className='add-col'; addBtn.innerHTML='+ Add column';
  addBtn.onclick=()=>{ snapshot(); const c={id:uid('c'),title:'New Column',wip:0,cards:[]}; board.columns.push(c); save(); render(); };
  boardEl.appendChild(addBtn);
  updateFilterBar();
}

function renderColumn(col){
  const wrap=document.createElement('div'); wrap.className='column'; wrap.dataset.col=col.id;
  if(col.wip && col.wip>0 && col.cards.length>col.wip) wrap.classList.add('wip-exceeded');

  const header=document.createElement('div'); header.className='col-header'; header.dataset.dragcol=col.id;
  const title=document.createElement('span'); title.className='col-title'; title.textContent=col.title;
  const count=document.createElement('span'); count.className='col-count';
  const nVis=col.cards.filter(cardVisible).length; count.textContent=nVis;
  if(col.wip&&col.wip>0&&col.cards.length>col.wip) count.classList.add('warn');
  const wipBadge=document.createElement('span'); wipBadge.className='wip-badge'; wipBadge.textContent='WIP '+col.wip;
  const menuBtn=document.createElement('button'); menuBtn.className='col-menu-btn btn-icon'; menuBtn.textContent='⋯';
  menuBtn.onclick=e=>{ e.stopPropagation(); openColumnMenu(col, menuBtn); };
  header.append(title,count,wipBadge,menuBtn);
  wrap.appendChild(header);

  const addCard=document.createElement('button'); addCard.className='add-card'; addCard.textContent='+ Add card';
  addCard.onclick=()=>{ snapshot(); const k={id:uid('k'),title:'',desc:'',labels:[],due:''}; col.cards.push(k); save(); render(); focusCard(k.id); };
  wrap.appendChild(addCard);

  const body=document.createElement('div'); body.className='col-body'; body.dataset.col=col.id;
  const list=document.createElement('div'); list.className='card-list'; list.dataset.col=col.id;
  col.cards.forEach(k=> list.appendChild(renderCard(col,k)));
  body.appendChild(list);
  const empty=document.createElement('div'); empty.className='empty-drop'; empty.textContent='Drop cards here';
  body.appendChild(empty);
  wrap.appendChild(body);

  makeColumnDraggable(header, col);
  return wrap;
}

function renderCard(col, card){
  const el=document.createElement('div'); el.className='card'; el.dataset.card=card.id;
  if(!cardVisible(card)) el.classList.add('hidden-filter');

  const t=document.createElement('div'); t.className='card-title'; t.textContent=card.title||'Untitled card';
  t.onclick=e=>{ e.stopPropagation(); startTitleEdit(el, card); };
  el.appendChild(t);

  if(card.desc){
    const d=document.createElement('div'); d.className='card-desc'; d.textContent=card.desc;
    d.onclick=e=>{ e.stopPropagation(); openCardEditor(card, col, el); };
    el.appendChild(d);
  }

  if(card.labels && card.labels.length){
    const lab=document.createElement('div'); lab.className='card-labels';
    card.labels.forEach(lid=>{
      const chip=document.createElement('span'); chip.className='label-chip';
      chip.style.background=labelColor(lid); chip.textContent=labelName(lid).slice(0,3); chip.title=labelName(lid);
      chip.onclick=e=>{ e.stopPropagation(); openCardEditor(card, col, el); };
      lab.appendChild(chip);
    });
    el.appendChild(lab);
  }

  const meta=document.createElement('div'); meta.className='card-meta';
  if(card.due){
    const due=document.createElement('span'); due.className='due';
    due.classList.add(isOverdue(card.due)?'overdue': isToday(card.due)?'today':'');
    due.innerHTML=(isOverdue(card.due)?'⚠':'📅')+' '+formatDate(card.due);
    due.onclick=e=>{ e.stopPropagation(); openCardEditor(card, col, el); };
    meta.appendChild(due);
  }
  const tools=document.createElement('div'); tools.className='card-tools';
  const editBtn=document.createElement('button'); editBtn.className='tool edit'; editBtn.textContent='✎'; editBtn.title='Edit';
  editBtn.onclick=e=>{ e.stopPropagation(); openCardEditor(card, col, el); };
  const delBtn=document.createElement('button'); delBtn.className='tool'; delBtn.textContent='×'; delBtn.title='Delete';
  delBtn.onclick=e=>{ e.stopPropagation(); deleteCard(col, card); };
  tools.append(editBtn,delBtn);

  el.appendChild(meta); el.appendChild(tools);

  makeCardDraggable(el, col, card);
  return el;
}

/* date helpers */
function todayStr(){ const d=new Date(); return d.toISOString().slice(0,10); }
function isToday(d){ return d===todayStr(); }
function isOverdue(d){ return d < todayStr(); }
function formatDate(d){ const d2=new Date(d+'T00:00:00'); return d2.toLocaleDateString(undefined,{month:'short',day:'numeric'}); }
function addDays(s,n){ const d=new Date(s+'T00:00:00'); d.setDate(d.getDate()+n); return d.toISOString().slice(0,10); }
function cycleDue(card){
  if(!card.due) card.due=todayStr();
  else if(card.due<todayStr()) card.due=addDays(todayStr(),1);
  else if(card.due===todayStr()) card.due=addDays(todayStr(),1);
  else card.due='';
}

function focusCard(id){
  requestAnimationFrame(()=>{ const t=boardEl.querySelector('.card-edit[data-card="'+id+'"]'); if(t){ t.focus(); t.selectionStart=t.selectionEnd=t.value.length; } });
}

/* =========================================================
   SECTION 6 — INLINE TITLE EDIT
   ========================================================= */
function startTitleEdit(el, card){
  if(el.querySelector('input')) return;
  const t=el.querySelector('.card-title');
  const inp=document.createElement('input'); inp.className='card-edit'; inp.type='text'; inp.dataset.card=card.id;
  inp.value=card.title;
  t.replaceWith(inp); inp.focus(); inp.select();
  const finish=(commitVal)=>{
    if(commitVal){ const v=inp.value.trim(); card.title=v||'Untitled card'; save(); }
    render();
    if(commitVal) focusCard(card.id);
  };
  inp.addEventListener('keydown', e=>{
    if(e.key==='Enter'){ e.preventDefault(); inp.blur(); }
    else if(e.key==='Escape'){ e.preventDefault(); render(); }
  });
  inp.addEventListener('blur', ()=> finish(true));
}

/* =========================================================
   SECTION 7 — CARD EDITOR PANEL
   ========================================================= */
let editorEl=null;
function openCardEditor(card, col, anchor){
  closeEditor();
  snapshot();
  const el=document.createElement('div'); el.id='editor';
  const r=anchor.getBoundingClientRect();
  let top=r.top, left=r.right+12;
  if(left+340>window.innerWidth) left=Math.max(12, r.left-352);
  if(top+400>window.innerHeight) top=Math.max(12, window.innerHeight-410);
  el.style.top=Math.min(top, window.innerHeight-420)+'px'; el.style.left=left+'px';

  const titleInput=document.createElement('input'); titleInput.className='ed-title'; titleInput.type='text'; titleInput.value=card.title;
  const descInput=document.createElement('textarea'); descInput.className='ed-desc'; descInput.value=card.desc; descInput.rows=3;
  const labelsWrap=document.createElement('div'); labelsWrap.className='ed-labels';
  const actions=document.createElement('div'); actions.className='ed-actions';
  const spacer=document.createElement('span'); spacer.className='spacer';
  const del=document.createElement('button'); del.className='ed-del'; del.textContent='Delete card';
  const close=document.createElement('button'); close.className='btn'; close.textContent='Close';

  const sync=()=>{ save(); render(); };
  titleInput.addEventListener('input', ()=>{ card.title=titleInput.value; sync(); });
  descInput.addEventListener('input', ()=>{ card.desc=descInput.value; sync(); });
  del.onclick=()=>{ closeEditor(); deleteCard(col, card); };
  close.onclick=()=>{ closeEditor(); };

  CFG.LABELS.forEach(l=>{
    const lab=document.createElement('label'); lab.className='ed-lbl'+(card.labels.includes(l.id)?' active':'');
    const cb=document.createElement('input'); cb.type='checkbox'; cb.checked=card.labels.includes(l.id);
    cb.onchange=()=>{ card.labels=card.labels.filter(x=>x!==l.id); if(cb.checked) card.labels.push(l.id);
      lab.classList.toggle('active', cb.checked); sync(); };
    const sw=document.createElement('span'); sw.className='swatch'; sw.style.background=l.color;
    lab.append(cb, sw, document.createTextNode(l.name));
    labelsWrap.appendChild(lab);
  });

  const dueInput=document.createElement('input'); dueInput.type='date'; dueInput.value=card.due||'';
  dueInput.addEventListener('change', ()=>{ card.due=dueInput.value; sync(); });

  el.innerHTML='<span class="ed-label">Title</span>'; el.appendChild(titleInput);
  el.appendChild(Object.assign(document.createElement('span'),{className:'ed-label',textContent:'Description'}));
  el.appendChild(descInput);
  el.appendChild(Object.assign(document.createElement('span'),{className:'ed-label',textContent:'Due date'}));
  el.appendChild(dueInput);
  el.appendChild(Object.assign(document.createElement('span'),{className:'ed-label',textContent:'Labels'}));
  el.appendChild(labelsWrap);
  actions.append(spacer, close, del); el.appendChild(actions);
  document.body.appendChild(el); editorEl=el;

  // keep panel in view after resize / scroll of anchor
  window.addEventListener('scroll', repositionEditor, true);
  window.addEventListener('resize', repositionEditor);
  requestAnimationFrame(repositionEditor);
}
function repositionEditor(){
  if(!editorEl) return;
  const col=board.columns.find(c=>c.cards.some(k=>k.id===editorEl._cardId));
}
function closeEditor(){
  if(!editorEl) return;
  editorEl.remove(); editorEl=null;
  window.removeEventListener('scroll', repositionEditor, true);
  window.removeEventListener('resize', repositionEditor);
}

/* =========================================================
   SECTION 8 — COLUMN MENU
   ========================================================= */
let openMenu=null;
function openColumnMenu(col, anchor){
  closeMenus();
  const menu=document.createElement('div'); menu.className='menu';
  const titleRow=document.createElement('div'); titleRow.className='row'; titleRow.textContent='Rename column';
  const wipRow=document.createElement('div'); wipRow.className='row'; wipRow.textContent='Set WIP limit…';
  const sep=document.createElement('div'); sep.className='sep';
  const delRow=document.createElement('div'); delRow.className='row danger'; delRow.textContent='Delete column';
  menu.append(titleRow,wipRow,sep,delRow);
  document.body.appendChild(menu); positionMenu(menu, anchor); openMenu=menu;

  titleRow.onclick=()=>{
    closeMenus();
    const input=document.createElement('input'); input.type='text'; input.value=col.title;
    titleRow.textContent=''; titleRow.appendChild(input); input.focus(); input.select();
    const done=()=>{ const v=input.value.trim(); if(v && v!==col.title){ snapshot(); col.title=v; save(); render(); } };
    input.addEventListener('keydown',e=>{ if(e.key==='Enter'){e.preventDefault(); input.blur();} if(e.key==='Escape'){ closeMenus(); }});
    input.addEventListener('blur', done);
  };
  wipRow.onclick=()=>{
    closeMenus();
    const v=prompt('WIP limit for “'+col.title+'” (0 = unlimited):', col.wip||'');
    if(v===null) return;
    snapshot(); col.wip=parseInt(v,10); if(isNaN(col.wip)||col.wip<0) col.wip=0; save(); render();
  };
  delRow.onclick=()=>{
    if(board.columns.length<=1){ alert('A board must keep at least one column.'); return; }
    snapshot(); const removed=deepClone(col); board.columns=board.columns.filter(c=>c.id!==col.id); save(); render();
    scheduleUndo('Column “'+removed.title+'” deleted');
  };
  menu.addEventListener('mousedown', e=>e.stopPropagation());
}
function closeMenus(){ if(openMenu){ openMenu.remove(); openMenu=null; } }
document.addEventListener('mousedown', e=>{ if(openMenu && !openMenu.contains(e.target)) closeMenus(); });
document.addEventListener('keydown', e=>{ if(e.key==='Escape' && openMenu) closeMenus(); });
function positionMenu(menu, anchor){
  const r=anchor.getBoundingClientRect();
  menu.style.top=(r.bottom+4)+'px';
  menu.style.left=Math.min(r.left, window.innerWidth-220)+'px';
}

/* =========================================================
   SECTION 9 — FILTER BAR + SEARCH
   ========================================================= */
function updateFilterBar(){
  const chips=document.getElementById('filterChips'); chips.innerHTML='';
  activeFilters.forEach(lid=>{
    const chip=document.createElement('span'); chip.className='filter-chip';
    const dot=document.createElement('span'); dot.className='dot'; dot.style.background=labelColor(lid);
    const txt=document.createElement('span'); txt.textContent=labelName(lid);
    const x=document.createElement('span'); x.className='x'; x.textContent='×';
    x.onclick=()=>{ activeFilters.delete(lid); save(); render(); };
    chip.append(dot,txt,x); chips.appendChild(chip);
  });
  const hint=document.getElementById('filterHint');
  hint.textContent = searchQuery? 'Matching “'+searchQuery+'”'
    : activeFilters.size? activeFilters.size+' label'+(activeFilters.size>1?'s':'')+' active' : '';
}
function openFilterPicker(anchor){
  closeMenus();
  const menu=document.createElement('div'); menu.className='menu';
  const head=document.createElement('div'); head.className='row'; head.style.fontWeight='700'; head.textContent='Filter by labels';
  head.style.cursor='default'; head.style.background='transparent'; menu.appendChild(head);
  CFG.LABELS.forEach(l=>{
    const lab=document.createElement('label'); lab.className='lbl';
    const cb=document.createElement('input'); cb.type='checkbox'; cb.checked=activeFilters.has(l.id);
    cb.onchange=()=>{ if(cb.checked) activeFilters.add(l.id); else activeFilters.delete(l.id); save(); render(); };
    const sw=document.createElement('span'); sw.className='swatch'; sw.style.background=l.color;
    lab.append(cb, sw, document.createTextNode(l.name)); menu.appendChild(lab);
  });
  document.body.appendChild(menu); positionMenu(menu, anchor); openMenu=menu;
  menu.addEventListener('mousedown', e=>e.stopPropagation());
}
document.getElementById('addFilterBtn').onclick=e=> openFilterPicker(e.currentTarget);

searchInput.addEventListener('input', ()=>{
  searchQuery = searchInput.value.trim().toLowerCase();
  boardEl.querySelectorAll('.card').forEach(el=>{
    const cardId=el.dataset.card; let vis=false;
    for(const col of board.columns){ const k=cardInColumn(col,cardId); if(k){ vis=cardVisible(k); break; } }
    el.classList.toggle('hidden-filter', !vis);
  });
  board.columns.forEach(col=>{
    const c=boardEl.querySelector('.column[data-col="'+col.id+'"] .col-count');
    if(c){ c.textContent=col.cards.filter(cardVisible).length; c.classList.toggle('warn', col.wip&&col.wip>0&&col.cards.length>col.wip); }
  });
  updateFilterBar();
});

/* =========================================================
   SECTION 10 — MENUS / TOAST HELPERS
   ========================================================= */
const toastEl=document.getElementById('toast');
const toastMsg=document.getElementById('toastMsg');
let toastTimer=null;
function showToast(msg){ toastMsg.textContent=msg; toastEl.classList.add('show'); clearTimeout(toastTimer); toastTimer=setTimeout(hideToast,4000); }
function hideToast(){ toastEl.classList.remove('show'); clearTimeout(toastTimer); }
document.getElementById('undoBtn').onclick=()=>{ performUndo(); };
document.getElementById('resetBtn').onclick=()=>{ if(confirm('Reset the board to defaults? This clears saved data.')){ snapshot(); board=seedDefault(); save(); render(); } };

/* =========================================================
   SECTION 11 — DRAG & DROP (Pointer Events)
   ========================================================= */
const drag = { active:false, moved:false, cardId:null, sourceColId:null,
  placeholder:null, ghost:null, x:0, y:0, startX:0, startY:0 };
let scrollRAF=null;

function makeCardDraggable(el, col, card){
  el.addEventListener('pointerdown', e=>{
    if(e.button!==0 || e.target.closest('input,button,.tool,.label-chip,.due,.card-tools')) return;
    if(el.classList.contains('hidden-filter')) return;
    drag.active=true; drag.moved=false; drag.placeholder=null; drag.ghost=null;
    drag.cardId=card.id; drag.sourceColId=col.id; drag.colId=col.id;
    drag.startX=e.clientX; drag.startY=e.clientY; drag.x=e.clientX; drag.y=e.clientY;
    el.setPointerCapture?.(e.pointerId);
  });
}

document.addEventListener('pointermove', e=>{
  if(!drag.active) return;
  drag.x=e.clientX; drag.y=e.clientY;
  const dx=e.clientX-drag.startX, dy=e.clientY-drag.startY;
  if(!drag.moved && (Math.abs(dx)>CFG.DRAG_THRESHOLD || Math.abs(dy)>CFG.DRAG_THRESHOLD)){
    drag.moved=true; beginDrag();
  }
  if(drag.moved){ e.preventDefault(); updateDragPosition(); autoScroll(e); }
});
window.addEventListener('pointerup', e=>{
  if(!drag.active) return;
  if(drag.moved) doDrop(); else cancelDrag();
});
window.addEventListener('keydown', e=>{
  if(e.key==='Escape' && drag.active){ cancelDrag(); }
});

function beginDrag(){
  const col=findColumn(drag.sourceColId); if(!col) return;
  const card=cardInColumn(col, drag.cardId); if(!card) return;
  const origEl=boardEl.querySelector('.card[data-card="'+drag.cardId+'"]');
  if(!origEl) return;
  drag.placeholder=origEl;
  origEl.classList.add('dragging');

  const ghost=document.createElement('div'); ghost.id='dragghost';
  ghost.innerHTML=cardInnerHTML(card);
  document.body.appendChild(ghost);
  drag.ghost=ghost;
  applyGhost();
  startScrollLoop();
}

function cardInnerHTML(card){
  let h='<div class="card-title">'+esc(card.title||'Untitled')+'</div>';
  if(card.desc) h+='<div class="card-desc">'+esc(card.desc)+'</div>';
  if(card.labels && card.labels.length){
    h+='<div class="card-labels">';
    card.labels.forEach(lid=> h+='<span class="label-chip" style="background:'+labelColor(lid)+'">'+esc(labelName(lid).slice(0,3))+'</span>');
    h+='</div>';
  }
  if(card.due) h+='<div class="card-meta"><span class="due">'+(isOverdue(card.due)?'⚠':'📅')+' '+esc(formatDate(card.due))+'</span></div>';
  return h;
}

function applyGhost(){
  if(!drag.ghost) return;
  const gr=drag.placeholder.getBoundingClientRect();
  drag.ghost.style.width=(gr.width-8)+'px';
  drag.ghost.style.left=(drag.x - (gr.width)/2 + 4)+'px';
  drag.ghost.style.top=(drag.y - gr.height/2)+'px';
}

function listVisibleCards(list){ return [...list.querySelectorAll('.card:not(.dragging):not(.hidden-filter)')]; }

/* hit-test: find nearest column + insert index using scroll-aware positions */
function hitTest(clientX, clientY){
  const sourceColId = drag.active ? drag.colId : null;
  const placeholder = drag.placeholder;
  // Pick the target column by horizontal overlap (viewport).
  // Prefer the source column (so in-column drag never leaks elsewhere).
  let target = null, targetScore = -Infinity;
  boardEl.querySelectorAll('.column').forEach(colEl=>{
    const body=colEl.querySelector('.col-body');
    const list=body.querySelector('.card-list');
    const br=body.getBoundingClientRect();
    if(clientY < br.top-90 || clientY > br.bottom+90) return;
    if(clientX < br.left || clientX > br.right) return;
    const isSource = colEl.dataset.col === sourceColId;
    const overlap = Math.min(br.right, clientX+1) - Math.max(br.left, clientX-1);
    const score = (isSource ? 1e9 : 0) + overlap;
    if(score > targetScore){ targetScore = score; target = { colId: colEl.dataset.col, list }; }
  });
  if(!target){
    // cursor over empty space between columns: nearest by x
    let nd=Infinity;
    boardEl.querySelectorAll('.column').forEach(colEl=>{
      const r=colEl.getBoundingClientRect();
      if(clientX>r.left-120 && clientX<r.right+120){
        const d=Math.abs(clientX-(r.left+r.right)/2);
        if(d<nd){ nd=d; target={ colId: colEl.dataset.col, list: colEl.querySelector('.card-list') }; }
      }
    });
  }
  if(!target) return null;

  // Determine insertion index within the chosen column by vertical position.
  // The placeholder (dragged card) must be EXCLUDED from the reference set,
  // otherwise dragging within the same column computes a position relative
  // to itself (which moves as we insert it), causing index instability.
  const { list } = target;
  const visible = listVisibleCards(list).filter(el => el !== placeholder);
  let index = visible.length;
  for(let i=0;i<visible.length;i++){
    const cr = visible[i].getBoundingClientRect();
    if(cr.top + cr.height/2 > clientY){ index = i; break; }
  }
  return { colId: target.colId, index, list };
}

function highlightColumns(colId){
  boardEl.querySelectorAll('.column').forEach(c=>c.classList.toggle('drag-over', c.dataset.col===colId));
  boardEl.querySelectorAll('.col-body').forEach(c=>c.classList.toggle('drag-target', c.dataset.col===colId));
}

function updateDragPosition(){
  if(!drag.placeholder || !drag.ghost) return;
  const info=hitTest(drag.x, drag.y);
  if(!info) return;
  highlightColumns(info.colId);
  const list=info.list;
  const visible=listVisibleCards(list);
  const before = visible[info.index] || null;
  if(before) list.insertBefore(drag.placeholder, before);
  else list.appendChild(drag.placeholder);
  applyGhost();
}

/* auto-scroll */
function startScrollLoop(){
  if(scrollRAF) return;
  const loop=()=>{ if(!drag.active || !drag.moved){ scrollRAF=null; return; } autoScroll({x:drag.x,y:drag.y}); scrollRAF=requestAnimationFrame(loop); };
  scrollRAF=requestAnimationFrame(loop);
}
function autoScroll(e){
  const r=boardWrap.getBoundingClientRect();
  const mx=r.left+CFG.SCROLL_MARGIN_H, mnr=r.right-CFG.SCROLL_MARGIN_H;
  if(e.x < mx){ boardWrap.scrollLeft -= ((mx-e.x)/CFG.SCROLL_MARGIN_H)*CFG.SCROLL_SPEED; }
  else if(e.x > mnr){ boardWrap.scrollLeft += ((e.x-mnr)/CFG.SCROLL_MARGIN_H)*CFG.SCROLL_SPEED; }
  // vertical: scroll the column body under the cursor
  const under=boardEl.querySelector('.col-body');
  let body=null; let best=Infinity;
  boardEl.querySelectorAll('.col-body').forEach(b=>{
    const br=b.getBoundingClientRect();
    if(e.x>=br.left && e.x<=br.right && Math.abs(e.y-(br.top+br.height/2))<best){ best=Math.abs(e.y-(br.top+br.height/2)); body=b; }
  });
  if(body){
    const br=body.getBoundingClientRect();
    const topM=br.top+CFG.SCROLL_MARGIN_V, botM=br.bottom-CFG.SCROLL_MARGIN_V;
    if(e.y < topM) body.scrollTop -= ((topM-e.y)/CFG.SCROLL_MARGIN_V)*CFG.SCROLL_SPEED;
    else if(e.y > botM) body.scrollTop += ((e.y-botM)/CFG.SCROLL_MARGIN_V)*CFG.SCROLL_SPEED;
  }
}

function doDrop(){
  const info=hitTest(drag.x, drag.y);
  const srcCol=findColumn(drag.sourceColId);
  const card=srcCol && cardInColumn(srcCol, drag.cardId);
  if(card){
    const ci=srcCol.cards.findIndex(c=>c.id===drag.cardId);
    srcCol.cards.splice(ci,1);
    if(info && info.colId){
      const target=findColumn(info.colId);
      if(target){
        let idx=info.index;
        if(info.colId===drag.sourceColId && ci<idx) idx=Math.max(0,idx-1);
        target.cards.splice(Math.max(0,Math.min(idx,target.cards.length)),0,card);
        card.colId=target.id;
      }
    }
    save();
  }
  cleanupDrag();
  render();
}

function cancelDrag(){
  cleanupDrag();
  render(); // model never changed during drag → exact restore
}

function cleanupDrag(){
  if(drag.placeholder) drag.placeholder.classList.remove('dragging');
  if(drag.ghost) drag.ghost.remove();
  boardEl.querySelectorAll('.column').forEach(c=>c.classList.remove('drag-over'));
  boardEl.querySelectorAll('.col-body').forEach(c=>c.classList.remove('drag-target'));
  if(scrollRAF){ cancelAnimationFrame(scrollRAF); scrollRAF=null; }
  drag.active=false; drag.moved=false; drag.placeholder=null; drag.ghost=null;
}

/* =========================================================
   SECTION 12 — COLUMN REORDER (drag by header)
   ========================================================= */
let colDrag=null;
function makeColumnDraggable(header, col){
  header.addEventListener('pointerdown', e=>{
    if(e.target.closest('button')) return;
    e.stopPropagation();
    colDrag={ colId:col.id, startX:e.clientX, startY:e.clientY, moved:false, ghost:null, el:null };
    header.setPointerCapture?.(e.pointerId);
  });
  header.addEventListener('pointermove', e=>{
    if(!colDrag || colDrag.colId!==col.id) return;
    const dx=e.clientX-colDrag.startX, dy=e.clientY-colDrag.startY;
    if(!colDrag.moved && (Math.abs(dx)>5||Math.abs(dy)>5)){
      colDrag.moved=true;
      const g=colDrag.el=boardEl.querySelector('.column[data-col="'+col.id+'"]');
      const clone=g.cloneNode(true);
      clone.id='coldragghost';
      clone.style.cssText='position:fixed;top:0;left:0;width:'+CFG.COL_WIDTH+'px;z-index:55;opacity:.85;pointer-events:none;box-shadow:var(--shadow-lg);transform:rotate(-1.5deg)';
      document.body.appendChild(clone); colDrag.ghost=clone;
      g.style.opacity='.35';
    }
    if(!colDrag.moved) return;
    e.preventDefault();
    colDrag.ghost.style.left=(e.clientX-16)+'px';
    colDrag.ghost.style.top=(e.clientY-16)+'px';
    boardEl.querySelectorAll('.column').forEach(c=>c.classList.remove('drag-col'));
    const under=document.elementFromPoint(e.clientX,e.clientY);
    const tgt=under&&under.closest('.column');
    if(tgt) tgt.classList.add('drag-col');
  });
  header.addEventListener('pointerup', e=>{
    if(!colDrag || colDrag.colId!==col.id) return;
    if(colDrag.moved){
      const under=document.elementFromPoint(e.clientX,e.clientY);
      const tgt=under&&under.closest('.column');
      if(tgt && tgt.dataset.col!==colDrag.colId){
        const from=board.columns.findIndex(c=>c.id===colDrag.colId);
        const to=board.columns.findIndex(c=>c.id===tgt.dataset.col);
        if(from!==-1&&to!==-1&&from!==to){
          snapshot();
          const [c]=board.columns.splice(from,1);
          board.columns.splice(to,0,c);
          save();
        }
      }
      if(colDrag.ghost) colDrag.ghost.remove();
      if(colDrag.el) colDrag.el.style.opacity='';
      render();
    }
    colDrag=null;
  });
}

/* =========================================================
   SECTION 13 — DELETE
   ========================================================= */
function deleteCard(col, card){
  snapshot();
  col.cards=col.cards.filter(c=>c.id!==card.id);
  save(); render();
  scheduleUndo('Card “'+card.title+'” deleted');
}

/* =========================================================
   SECTION 14 — INIT
   ========================================================= */
function init(){ board=load(); render(); save(); }
init();
</script>
</body>
</html>
<!-- agent-meta {"model":"bartowski/Ornith-1.5-35B-A3B","provider":"brigid","persona":"full","sessionId":"8d6dd3c6-d834-4850-8ed9-450d568238b4","tokensIn":6288554,"tokensOut":161540,"tokensTotal":6450094,"cost":0,"turns":78,"toolCalls":77,"failedToolCalls":4,"timestamp":"2026-08-24T00:05:36.837Z"} -->