Models 44

Muse Ember

Open in new window

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

How it went

Total time
11.8 s
Cost
$0.0036
Output tokens
7,174
Spent thinking
456 tokens
Speed
748.6 tok/s
Console errors
0
First token after
2.2 s
First visible text after
2.2 s
Prompt tokens
511
Code size
22.4 KB
Finish reason
stop
Served by
Meta
Model id
meta/muse-ember
Reasoning sent
{"effort":"low"}
Max output
943,718 tokens
Generated
Nov 12, 2026

What it was asked

The prompt
Build an interactive Kanban board in vanilla HTML, CSS and JavaScript (no framework, no library). Requirements: - Three columns: "To do", "In progress", "Done", each showing its card count. - Pre-populate the board with 6 realistic cards spread across the columns so the first screenshot looks alive. - Each card has a title and a priority (low / medium / high) shown as a colored badge. - Add a card through a small modal form (title required), and delete a card with a button on it. - Drag and drop cards between columns with the mouse, with a clear visual drop indicator. - A search field that filters cards live by title. - A light / dark theme toggle. - State lives in memory only (no storage APIs).
System prompt, sent with every prompt
You are generating a self-contained web demo. It will be rendered automatically in a sandboxed iframe and compared side by side with the output of other AI models given the exact same task.

Output rules:
- Reply with exactly ONE fenced code block containing the complete file: ```html (or ```svg when the task asks for an SVG). No text before or after the code block.
- Everything must be inline: CSS in <style>, JavaScript in <script>. No external files, images, fonts or network requests, except the CDN URLs explicitly allowed by the task.
- Do not use localStorage, sessionStorage, cookies, IndexedDB, alert(), confirm(), prompt() or window.open(): they are blocked in the sandbox and will throw.
- The page must look right in a 1280×800 viewport and adapt gracefully down to 375px wide. No horizontal scrollbar.
- Something meaningful must be visible on first paint: a screenshot is taken automatically about 3 seconds after load, without any interaction.
- Everything that makes the demo interesting must be visible within the first seconds: nothing hidden behind a high score, a later level or a long play session.
- Games must start in a self-playing demo mode (a simple AI plays) behind a "Click or press a key to start" message, so the game can be judged in action without playing.
- The iframe only receives keyboard events after the user clicks it: if the demo uses the keyboard, display a clear "Click or press a key to start" message.
- The code must run with zero errors in the console of the latest Chrome.

Code run-1.html · 422 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Flowboard — Kanban Board</title>
<style>
  *{box-sizing:border-box;margin:0;padding:0}
  :root{
    --bg:#eef2ff; --bg2:#f8fafc; --surface:#ffffff; --surface2:#f1f5f9;
    --text:#0f172a; --muted:#64748b; --border:#e2e8f0; --border2:#cbd5e1;
    --primary:#6366f1; --primary2:#4f46e5; --primary-soft:#e0e7ff;
    --shadow:0 10px 30px rgba(15,23,42,.08); --shadow2:0 4px 14px rgba(15,23,42,.07);
    --col-bg:#f8fafc; --card:#ffffff; --input:#ffffff;
    --high:#ef4444; --high-bg:#fef2f2; --med:#f59e0b; --med-bg:#fffbeb; --low:#10b981; --low-bg:#ecfdf5;
    --drop:#6366f1;
  }
  html[data-theme="dark"]{
    --bg:#0b1020; --bg2:#111827; --surface:#1e293b; --surface2:#334155;
    --text:#f1f5f9; --muted:#94a3b8; --border:#334155; --border2:#475569;
    --primary:#818cf8; --primary2:#6366f1; --primary-soft:#312e81;
    --shadow:0 10px 30px rgba(0,0,0,.35); --shadow2:0 4px 14px rgba(0,0,0,.3);
    --col-bg:#151e2e; --card:#1e293b; --input:#1e293b;
    --high-bg:#450a0a; --med-bg:#451a03; --low-bg:#022c22;
  }
  body{
    font-family:Inter,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;
    background: linear-gradient(180deg,var(--bg) 0%, var(--bg2) 55%, var(--bg) 100%);
    color:var(--text); min-height:100vh; transition:background .3s,color .3s;
  }
  .wrap{max-width:1240px;margin:0 auto;padding:22px 22px 28px}
  /* Header */
  header{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;flex-wrap:wrap}
  .brand{display:flex;gap:13px;align-items:center}
  .logo{width:46px;height:46px;border-radius:13px;background:linear-gradient(135deg,#6366f1,#8b5cf6 55%,#ec4899);display:grid;place-items:center;color:#fff;font-weight:900;font-size:22px;box-shadow:0 8px 20px rgba(99,102,241,.35);flex-shrink:0}
  h1{font-size:28px;line-height:1;letter-spacing:-.02em}
  .subtitle{color:var(--muted);font-size:13.5px;margin-top:5px}
  .actions{display:flex;gap:10px;align-items:center;flex-wrap:wrap}
  .search{position:relative}
  .search svg{position:absolute;left:12px;top:50%;transform:translateY(-50%);opacity:.55}
  .search input{
    width:260px;height:42px;border-radius:10px;border:1.5px solid var(--border);background:var(--input);color:var(--text);
    padding:0 14px 0 38px;font-size:14px;outline:none;transition:.2s;box-shadow:var(--shadow2)
  }
  .search input:focus{border-color:var(--primary);box-shadow:0 0 0 4px var(--primary-soft)}
  .search input::placeholder{color:var(--muted)}
  .btn{height:42px;border:none;border-radius:10px;padding:0 17px;font-size:14px;font-weight:700;cursor:pointer;display:inline-flex;align-items:center;gap:8px;transition:.2s;white-space:nowrap}
  .btn-primary{background:var(--primary2);color:#fff;box-shadow:0 6px 16px rgba(99,102,241,.35)}
  .btn-primary:hover{transform:translateY(-1px);background:var(--primary)}
  .btn-primary:active{transform:translateY(0)}
  .icon-btn{width:42px;height:42px;border-radius:10px;border:1.5px solid var(--border);background:var(--surface);color:var(--text);cursor:pointer;font-size:18px;display:grid;place-items:center;box-shadow:var(--shadow2);transition:.2s}
  .icon-btn:hover{border-color:var(--primary);transform:translateY(-1px)}
  /* Stats */
  .stats{display:flex;gap:12px;margin:20px 0 18px;flex-wrap:wrap}
  .stat{flex:1;min-width:150px;background:var(--surface);border:1px solid var(--border);border-radius:14px;padding:13px 15px;display:flex;justify-content:space-between;align-items:center;box-shadow:var(--shadow2)}
  .stat-label{font-size:12px;font-weight:700;letter-spacing:.07em;text-transform:uppercase;color:var(--muted)}
  .stat-value{font-size:24px;font-weight:900;margin-top:2px}
  .stat-icon{width:38px;height:38px;border-radius:10px;display:grid;place-items:center;font-size:18px}
  .hint{font-size:12.5px;color:var(--muted);margin-bottom:14px;display:flex;align-items:center;gap:7px}
  .hint b{color:var(--text)}
  /* Board */
  .board{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;align-items:start}
  .column{
    background:var(--col-bg);border:1.5px solid var(--border);border-radius:16px;padding:14px;min-height:380px;
    transition:border-color .2s, background .2s, box-shadow .2s; position:relative;
  }
  .column.drag-over{border-color:var(--drop);box-shadow:0 0 0 4px var(--primary-soft);background:color-mix(in srgb, var(--col-bg) 88%, var(--primary-soft))}
  .col-head{display:flex;justify-content:space-between;align-items:center;margin-bottom:13px}
  .col-title{display:flex;align-items:center;gap:8px;font-weight:800;font-size:15px}
  .dot{width:10px;height:10px;border-radius:50%}
  .dot-todo{background:#94a3b8}.dot-progress{background:#f59e0b}.dot-done{background:#10b981}
  .count{background:var(--surface);border:1px solid var(--border);color:var(--muted);font-size:12px;font-weight:800;min-width:28px;height:26px;padding:0 8px;border-radius:999px;display:inline-grid;place-items:center}
  .cards{display:flex;flex-direction:column;gap:11px;min-height:170px}
  .empty{border:1.5px dashed var(--border2);border-radius:12px;padding:26px 14px;text-align:center;color:var(--muted);font-size:13px;line-height:1.5}
  .empty strong{display:block;color:var(--text);font-size:14px;margin-bottom:3px}
  /* Card */
  .card{
    background:var(--card);border:1px solid var(--border);border-radius:13px;padding:14px 14px 12px;cursor:grab;
    box-shadow:var(--shadow2);position:relative;transition:transform .18s, box-shadow .18s, border-color .18s, opacity .2s;
    animation:pop .25s ease both; border-left:4px solid var(--border2);
  }
  .card:hover{transform:translateY(-2px);box-shadow:var(--shadow);border-color:var(--border2)}
  .card:active{cursor:grabbing}
  .card.dragging{opacity:.42;transform:rotate(1.5deg) scale(.98);box-shadow:none}
  .card.p-high{border-left-color:var(--high)} .card.p-medium{border-left-color:var(--med)} .card.p-low{border-left-color:var(--low)}
  .card-top{display:flex;justify-content:space-between;gap:10px;align-items:flex-start}
  .card h3{font-size:14.5px;line-height:1.35;font-weight:700;letter-spacing:-.01em;padding-right:4px}
  .delete-btn{
    flex-shrink:0;width:28px;height:28px;border:none;border-radius:8px;background:transparent;color:var(--muted);cursor:pointer;
    font-size:15px;display:grid;place-items:center;transition:.15s;line-height:1
  }
  .delete-btn:hover{background:#fef2f2;color:#ef4444}
  html[data-theme="dark"] .delete-btn:hover{background:#450a0a}
  .desc{font-size:12.5px;color:var(--muted);line-height:1.5;margin-top:6px}
  .card-foot{display:flex;justify-content:space-between;align-items:center;margin-top:12px;gap:8px}
  .badge{font-size:11px;font-weight:800;letter-spacing:.05em;text-transform:uppercase;padding:5px 9px;border-radius:999px;border:1px solid}
  .badge.high{color:var(--high);background:var(--high-bg);border-color:#fecaca}
  .badge.medium{color:#b45309;background:var(--med-bg);border-color:#fde68a}
  html[data-theme="dark"] .badge.medium{color:#fbbf24}
  .badge.low{color:#047857;background:var(--low-bg);border-color:#a7f3d0}
  html[data-theme="dark"] .badge.low{color:#34d399}
  .drag-hint{font-size:11px;color:var(--muted);display:flex;align-items:center;gap:4px}
  .drop-indicator{height:4px;border-radius:999px;background:var(--drop);box-shadow:0 0 0 3px var(--primary-soft);margin:2px 2px;flex-shrink:0}
  @keyframes pop{from{opacity:0;transform:translateY(6px) scale(.98)}to{opacity:1;transform:translateY(0) scale(1)}}
  /* Modal */
  .overlay{position:fixed;inset:0;background:rgba(2,6,23,.55);backdrop-filter:blur(4px);display:none;place-items:center;padding:16px;z-index:50}
  .overlay.open{display:grid}
  .modal{background:var(--surface);width:100%;max-width:440px;border-radius:18px;padding:24px;box-shadow:0 24px 60px rgba(0,0,0,.3);border:1px solid var(--border);animation:pop .22s ease both}
  .modal h2{font-size:19px;letter-spacing:-.02em}
  .modal p{color:var(--muted);font-size:13px;margin:6px 0 18px}
  label{display:block;font-size:12px;font-weight:800;letter-spacing:.06em;text-transform:uppercase;color:var(--muted);margin:14px 0 7px}
  label span{color:var(--high)}
  .field{width:100%;height:44px;border:1.5px solid var(--border);border-radius:10px;background:var(--input);color:var(--text);padding:0 13px;font-size:14px;outline:none}
  textarea.field{height:auto;padding:11px 13px;resize:vertical;min-height:72px;font-family:inherit}
  .field:focus{border-color:var(--primary);box-shadow:0 0 0 4px var(--primary-soft)}
  .field.error{border-color:var(--high);box-shadow:0 0 0 4px #fee2e2}
  .error-msg{color:var(--high);font-size:12.5px;margin-top:6px;display:none}
  .error-msg.show{display:block}
  .prio-row{display:flex;gap:8px}
  .prio-btn{flex:1;height:40px;border-radius:10px;border:1.5px solid var(--border);background:var(--surface2);color:var(--text);font-weight:700;font-size:13px;cursor:pointer;transition:.15s}
  .prio-btn.selected{border-color:currentColor;box-shadow:0 0 0 3px var(--primary-soft)}
  .prio-btn.sel-high{background:var(--high-bg);color:var(--high);border-color:var(--high)}
  .prio-btn.sel-medium{background:var(--med-bg);color:#b45309;border-color:var(--med)}
  .prio-btn.sel-low{background:var(--low-bg);color:#047857;border-color:var(--low)}
  html[data-theme="dark"] .prio-btn.sel-medium{color:#fbbf24} html[data-theme="dark"] .prio-btn.sel-low{color:#34d399}
  .modal-actions{display:flex;gap:10px;justify-content:flex-end;margin-top:22px}
  .btn-ghost{background:var(--surface2);color:var(--text);border:1px solid var(--border)}
  .clear-btn{background:none;border:none;color:var(--primary);font-weight:700;font-size:13px;cursor:pointer;padding:6px}
  .no-results{grid-column:1/-1;text-align:center;padding:28px;color:var(--muted);font-size:14px;display:none}
  footer{text-align:center;color:var(--muted);font-size:12px;margin-top:18px}
  @media(max-width:820px){
    .board{grid-template-columns:1fr}
    .column{min-height:auto}
    .search input{width:100%}
    .search{flex:1;min-width:200px}
    .actions{width:100%}
    h1{font-size:24px}
    .wrap{padding:16px 14px 22px}
  }
</style>
</head>
<body>
<div class="wrap">
  <header>
    <div class="brand">
      <div class="logo">≋</div>
      <div>
        <h1>Flowboard</h1>
        <p class="subtitle">Your team's workflow, beautifully organized.</p>
      </div>
    </div>
    <div class="actions">
      <div class="search">
        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg>
        <input id="searchInput" type="text" placeholder="Search cards by title…" aria-label="Search cards by title">
      </div>
      <button class="icon-btn" id="themeToggle" title="Toggle light / dark theme" aria-label="Toggle theme">🌙</button>
      <button class="btn btn-primary" id="addBtn">+ Add Card</button>
    </div>
  </header>

  <div class="stats">
    <div class="stat"><div><div class="stat-label">Total cards</div><div class="stat-value" id="statTotal">6</div></div><div class="stat-icon" style="background:#e0e7ff;color:#4f46e5">▦</div></div>
    <div class="stat"><div><div class="stat-label">In progress</div><div class="stat-value" id="statDoing">2</div></div><div class="stat-icon" style="background:#fef3c7;color:#d97706">◐</div></div>
    <div class="stat"><div><div class="stat-label">Completed</div><div class="stat-value" id="statDone">2</div></div><div class="stat-icon" style="background:#d1fae5;color:#059669">✓</div></div>
    <div class="stat"><div><div class="stat-label">High priority</div><div class="stat-value" id="statHigh">2</div></div><div class="stat-icon" style="background:#fee2e2;color:#dc2626">▲</div></div>
  </div>

  <p class="hint">💡 <span><b>Drag &amp; drop</b> cards between columns &nbsp;·&nbsp; Use <b>search</b> to filter live &nbsp;·&nbsp; Click <b>+ Add Card</b> to create one</span></p>

  <main class="board" id="board">
    <section class="column" data-col="todo">
      <div class="col-head"><div class="col-title"><span class="dot dot-todo"></span>To do</div><span class="count" id="count-todo">0</span></div>
      <div class="cards" id="cards-todo"></div>
    </section>
    <section class="column" data-col="doing">
      <div class="col-head"><div class="col-title"><span class="dot dot-progress"></span>In progress</div><span class="count" id="count-doing">0</span></div>
      <div class="cards" id="cards-doing"></div>
    </section>
    <section class="column" data-col="done">
      <div class="col-head"><div class="col-title"><span class="dot dot-done"></span>Done</div><span class="count" id="count-done">0</span></div>
      <div class="cards" id="cards-done"></div>
    </section>
  </main>
  <div class="no-results" id="noResults">No cards match your search. <button class="clear-btn" id="clearSearch">Clear search</button></div>

  <footer>Flowboard · Built with vanilla HTML, CSS &amp; JavaScript · State kept in memory only</footer>
</div>

<!-- Modal -->
<div class="overlay" id="overlay" role="dialog" aria-modal="true" aria-labelledby="modalTitle">
  <div class="modal">
    <h2 id="modalTitle">Add a new card</h2>
    <p>Fill in the details and choose where it should go.</p>
    <form id="cardForm" novalidate>
      <label for="cardTitle">Card title <span>*</span></label>
      <input class="field" id="cardTitle" type="text" placeholder="e.g. Design the new pricing page" maxlength="90" autocomplete="off">
      <div class="error-msg" id="titleError">Please enter a title — it's required.</div>

      <label for="cardDesc">Description <span style="text-transform:none;letter-spacing:0;font-weight:600">(optional)</span></label>
      <textarea class="field" id="cardDesc" placeholder="Add a short description…"></textarea>

      <label>Priority</label>
      <div class="prio-row" id="prioRow">
        <button type="button" class="prio-btn" data-p="low">● Low</button>
        <button type="button" class="prio-btn selected sel-medium" data-p="medium">● Medium</button>
        <button type="button" class="prio-btn" data-p="high">● High</button>
      </div>

      <label for="cardCol">Column</label>
      <select class="field" id="cardCol">
        <option value="todo">To do</option>
        <option value="doing">In progress</option>
        <option value="done">Done</option>
      </select>

      <div class="modal-actions">
        <button type="button" class="btn btn-ghost" id="cancelBtn">Cancel</button>
        <button type="submit" class="btn btn-primary">Add card</button>
      </div>
    </form>
  </div>
</div>

<script>
  // ---- State (memory only) ----
  let cards = [
    { id:1, title:"Design new landing page hero", desc:"Create 3 concepts for the homepage hero section with the updated brand palette.", priority:"high",   col:"todo"  },
    { id:2, title:"Write API documentation",        desc:"Document the new authentication endpoints and add code examples.",        priority:"medium", col:"todo"  },
    { id:3, title:"Fix checkout payment bug",       desc:"Payments fail on Safari when using Apple Pay — needs urgent fix.",        priority:"high",   col:"doing" },
    { id:4, title:"Refactor dashboard components",  desc:"Break the dashboard into smaller reusable components and add tests.",     priority:"medium", col:"doing" },
    { id:5, title:"Set up CI/CD pipeline",          desc:"Automated builds and deploys to staging on every pull request.",          priority:"low",    col:"done"  },
    { id:6, title:"User interviews — round 2",      desc:"Synthesize feedback from 8 interviews into the research report.",         priority:"low",    col:"done"  },
  ];
  let nextId = 7;
  let selectedPriority = "medium";
  let dragId = null;

  const COLS = ["todo","doing","done"];
  const $ = id => document.getElementById(id);

  // ---- Rendering ----
  function render(){
    const q = $("searchInput").value.trim().toLowerCase();
    let visible = 0;
    COLS.forEach(col=>{
      const list = $("cards-"+col);
      list.innerHTML = "";
      const inCol = cards.filter(c=>c.col===col && c.title.toLowerCase().includes(q));
      visible += inCol.length;
      $("count-"+col).textContent = inCol.length;
      if(inCol.length===0){
        const e=document.createElement("div");
        e.className="empty";
        e.innerHTML = q ? `<strong>No matches</strong>Try a different search term.` :
          (col==="todo" ? `<strong>Nothing here yet</strong>Drag a card here or add a new one.` :
           col==="doing" ? `<strong>All clear</strong>Move a card here to start work.` :
                           `<strong>Nothing done yet</strong>Finished cards land here.`);
        list.appendChild(e);
      } else {
        inCol.forEach(card=>{
          const el=document.createElement("article");
          el.className="card p-"+card.priority;
          el.draggable=true;
          el.dataset.id=card.id;
          el.innerHTML = `
            <div class="card-top">
              <h3>${escapeHtml(card.title)}</h3>
              <button class="delete-btn" title="Delete card" aria-label="Delete card">✕</button>
            </div>
            ${card.desc?`<p class="desc">${escapeHtml(card.desc)}</p>`:""}
            <div class="card-foot">
              <span class="badge ${card.priority}">${card.priority}</span>
              <span class="drag-hint">⠿ drag</span>
            </div>`;
          bindCardEvents(el, card.id);
          list.appendChild(el);
        });
      }
    });
    $("noResults").style.display = (q && visible===0) ? "block" : "none";
    $("statTotal").textContent = cards.length;
    $("statDoing").textContent = cards.filter(c=>c.col==="doing").length;
    $("statDone").textContent  = cards.filter(c=>c.col==="done").length;
    $("statHigh").textContent  = cards.filter(c=>c.priority==="high").length;
  }
  function escapeHtml(s){ return s.replace(/[&<>"']/g, m=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"}[m])); }

  // ---- Card events: delete + drag ----
  function bindCardEvents(el, id){
    el.querySelector(".delete-btn").addEventListener("click", e=>{
      e.stopPropagation();
      cards = cards.filter(c=>c.id!==id);
      render();
    });
    el.addEventListener("dragstart", e=>{
      dragId = id;
      el.classList.add("dragging");
      e.dataTransfer.effectAllowed = "move";
      try{ e.dataTransfer.setData("text/plain", String(id)); }catch(_){}
    });
    el.addEventListener("dragend", ()=>{
      el.classList.remove("dragging");
      clearIndicators();
    });
  }

  // ---- Drop logic with indicator ----
  let indicator = document.createElement("div");
  indicator.className = "drop-indicator";

  function clearIndicators(){ indicator.remove(); document.querySelectorAll(".column").forEach(c=>c.classList.remove("drag-over")); }

  function getDropTarget(list, y){
    const els = [...list.querySelectorAll(".card:not(.dragging)")];
    for(const el of els){
      const r = el.getBoundingClientRect();
      if(y < r.top + r.height/2) return el;
    }
    return null;
  }

  document.querySelectorAll(".column").forEach(column=>{
    const list = column.querySelector(".cards");
    column.addEventListener("dragover", e=>{
      if(dragId===null) return;
      e.preventDefault();
      column.classList.add("drag-over");
      const before = getDropTarget(list, e.clientY);
      if(before) list.insertBefore(indicator, before);
      else list.appendChild(indicator);
    });
    column.addEventListener("dragleave", e=>{
      if(!column.contains(e.relatedTarget)) { column.classList.remove("drag-over"); }
    });
    column.addEventListener("drop", e=>{
      e.preventDefault();
      const col = column.dataset.col;
      const before = getDropTarget(list, e.clientY);
      const card = cards.find(c=>c.id===dragId);
      if(card){
        // move card in array to reflect drop position
        cards = cards.filter(c=>c.id!==dragId);
        card.col = col;
        if(before){
          const beforeId = +before.dataset.id;
          const idx = cards.findIndex(c=>c.id===beforeId);
          cards.splice(idx, 0, card);
        } else {
          // append at end of this column's group
          const colCards = cards.filter(c=>c.col===col);
          if(colCards.length){
            const lastIdx = cards.lastIndexOf(colCards[colCards.length-1]);
            cards.splice(lastIdx+1, 0, card);
          } else cards.push(card);
        }
      }
      dragId = null;
      clearIndicators();
      render();
    });
  });

  // ---- Search ----
  $("searchInput").addEventListener("input", render);
  $("clearSearch").addEventListener("click", ()=>{ $("searchInput").value=""; render(); $("searchInput").focus(); });

  // ---- Theme toggle ----
  $("themeToggle").addEventListener("click", ()=>{
    const root = document.documentElement;
    const dark = root.getAttribute("data-theme")==="dark";
    root.setAttribute("data-theme", dark ? "light" : "dark");
    $("themeToggle").textContent = dark ? "🌙" : "☀️";
  });

  // ---- Modal ----
  const overlay = $("overlay");
  function openModal(){ overlay.classList.add("open"); setTimeout(()=>$("cardTitle").focus(),50); }
  function closeModal(){ overlay.classList.remove("open"); $("cardForm").reset(); setPriority("medium"); $("titleError").classList.remove("show"); $("cardTitle").classList.remove("error"); }
  function setPriority(p){
    selectedPriority = p;
    document.querySelectorAll(".prio-btn").forEach(b=>{
      b.className = "prio-btn" + (b.dataset.p===p ? " selected sel-"+p : "");
    });
  }
  $("addBtn").addEventListener("click", openModal);
  $("cancelBtn").addEventListener("click", closeModal);
  overlay.addEventListener("click", e=>{ if(e.target===overlay) closeModal(); });
  document.addEventListener("keydown", e=>{ if(e.key==="Escape" && overlay.classList.contains("open")) closeModal(); });
  $("prioRow").addEventListener("click", e=>{
    const b = e.target.closest(".prio-btn");
    if(b) setPriority(b.dataset.p);
  });
  $("cardForm").addEventListener("submit", e=>{
    e.preventDefault();
    const title = $("cardTitle").value.trim();
    if(!title){
      $("cardTitle").classList.add("error");
      $("titleError").classList.add("show");
      $("cardTitle").focus();
      return;
    }
    cards.push({
      id: nextId++,
      title,
      desc: $("cardDesc").value.trim(),
      priority: selectedPriority,
      col: $("cardCol").value
    });
    closeModal();
    render();
    // flash the new card's column
    const colEl = document.querySelector(`[data-col="${cards[cards.length-1].col}"]`);
    colEl.style.boxShadow = "0 0 0 4px var(--primary-soft)";
    setTimeout(()=>colEl.style.boxShadow="", 700);
  });
  $("cardTitle").addEventListener("input", ()=>{ $("cardTitle").classList.remove("error"); $("titleError").classList.remove("show"); });

  render();
</script>
</body>
</html>