<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Seating Chart Maker</title><style>  @import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@300;400;500;600&family=DM+Mono:wght@400;500&display=swap');   *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }   :root {    --bg: #F7F5F0;    --surface: #FFFFFF;    --surface2: #F0EDE6;    --border: #E2DDD5;    --border2: #C8C2B8;    --text: #1A1714;    --text2: #6B635A;    --text3: #9E9690;    --accent: #2D6A4F;    --accent-light: #D8EFE4;    --accent2: #B5451B;    --accent2-light: #FDEEE8;    --warn: #8B5E00;    --warn-light: #FEF3DC;    --radius: 10px;    --radius-sm: 6px;    --shadow: 0 1px 3px rgba(0,0,0,0.08), 0 1px 2px rgba(0,0,0,0.04);    --shadow-md: 0 4px 12px rgba(0,0,0,0.1), 0 2px 4px rgba(0,0,0,0.06);  }   body {    font-family: 'DM Sans', sans-serif;    background: var(--bg);    color: var(--text);    min-height: 100vh;    font-size: 14px;    line-height: 1.5;  }   /* ── Layout ── */  .app { display: grid; grid-template-rows: 56px 1fr; height: 100vh; overflow: hidden; }   .topbar {    background: var(--surface);    border-bottom: 1px solid var(--border);    display: flex; align-items: center; gap: 12px;    padding: 0 20px;    box-shadow: var(--shadow);    z-index: 10;  }   .topbar-logo { font-weight: 600; font-size: 15px; letter-spacing: -0.3px; }  .topbar-logo span { color: var(--accent); }   .topbar-spacer { flex: 1; }   .topbar select {    font-family: inherit;    font-size: 13px;    background: var(--surface2);    border: 1px solid var(--border);    border-radius: var(--radius-sm);    padding: 5px 10px;    color: var(--text);    cursor: pointer;    max-width: 200px;  }   .main { display: grid; grid-template-columns: 300px 1fr 280px; overflow: hidden; }   /* ── Panels ── */  .panel {    background: var(--surface);    border-right: 1px solid var(--border);    display: flex; flex-direction: column;    overflow: hidden;  }  .panel:last-child { border-right: none; border-left: 1px solid var(--border); }   .panel-header {    padding: 14px 16px 10px;    border-bottom: 1px solid var(--border);    display: flex; align-items: center; justify-content: space-between;    flex-shrink: 0;  }  .panel-title { font-size: 11px; font-weight: 600; letter-spacing: 0.8px; text-transform: uppercase; color: var(--text2); }   .panel-body { flex: 1; overflow-y: auto; padding: 12px; }  .panel-body::-webkit-scrollbar { width: 4px; }  .panel-body::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 2px; }   /* ── Canvas ── */  .canvas-area {    background: var(--bg);    position: relative;    overflow: auto;    display: flex; align-items: flex-start; justify-content: center;    padding: 24px;  }   #chart-canvas {    background: var(--surface);    border-radius: 12px;    border: 1px solid var(--border);    box-shadow: var(--shadow-md);    position: relative;    min-width: 600px;    min-height: 500px;  }   /* ── Buttons ── */  .btn {    display: inline-flex; align-items: center; gap: 6px;    font-family: inherit; font-size: 13px; font-weight: 500;    padding: 6px 12px; border-radius: var(--radius-sm);    cursor: pointer; border: none; transition: all 0.15s;    white-space: nowrap;  }  .btn-primary { background: var(--accent); color: #fff; }  .btn-primary:hover { background: #235A41; }  .btn-secondary { background: var(--surface2); color: var(--text); border: 1px solid var(--border); }  .btn-secondary:hover { background: var(--border); }  .btn-danger { background: var(--accent2-light); color: var(--accent2); }  .btn-danger:hover { background: #FAD9CE; }  .btn-sm { padding: 4px 8px; font-size: 12px; }  .btn-icon { padding: 5px 7px; }   /* ── Input ── */  input[type=text], input[type=number] {    font-family: inherit; font-size: 13px;    background: var(--surface2); color: var(--text);    border: 1px solid var(--border); border-radius: var(--radius-sm);    padding: 6px 10px; width: 100%;    transition: border-color 0.15s;    outline: none;  }  input[type=text]:focus, input[type=number]:focus {    border-color: var(--accent); background: var(--surface);  }   /* ── Students ── */  .student-add { display: flex; gap: 6px; margin-bottom: 10px; }  .student-add input { flex: 1; }   .student-list { display: flex; flex-direction: column; gap: 4px; }   .student-chip {    display: flex; align-items: center; gap: 8px;    padding: 7px 10px; border-radius: var(--radius-sm);    background: var(--surface2); border: 1px solid transparent;    cursor: grab; transition: all 0.12s; user-select: none;  }  .student-chip:hover { border-color: var(--border2); background: var(--border); }  .student-chip.seated { opacity: 0.4; }   .student-avatar {    width: 26px; height: 26px; border-radius: 50%;    display: flex; align-items: center; justify-content: center;    font-size: 11px; font-weight: 600; flex-shrink: 0;    font-family: 'DM Mono', monospace;  }   .student-name { flex: 1; font-size: 13px; font-weight: 500; }   .student-actions { display: flex; gap: 4px; opacity: 0; transition: opacity 0.12s; }  .student-chip:hover .student-actions { opacity: 1; }   /* ── Seats on Canvas ── */  .desk {    position: absolute;    width: 80px; height: 70px;    background: var(--surface);    border: 2px solid var(--border);    border-radius: var(--radius);    display: flex; flex-direction: column; align-items: center; justify-content: center;    gap: 4px; cursor: pointer;    transition: border-color 0.15s, box-shadow 0.15s;    box-shadow: var(--shadow);    z-index: 1;    user-select: none;  }  .desk:hover { border-color: var(--border2); box-shadow: var(--shadow-md); z-index: 2; }  .desk.occupied { border-color: var(--accent); background: var(--accent-light); }  .desk.occupied:hover { border-color: #235A41; }  .desk.conflict { border-color: var(--accent2) !important; background: var(--accent2-light) !important; }  .desk.selected { border-color: #2563EB !important; box-shadow: 0 0 0 3px rgba(37,99,235,0.2) !important; z-index: 3; }  .desk.drag-over { border-color: var(--accent) !important; border-style: dashed !important; }  .desk.empty-drag-over { background: var(--accent-light); }   .desk-label {    font-size: 10px; font-family: 'DM Mono', monospace;    color: var(--text3); font-weight: 500;  }  .desk-student { font-size: 11px; font-weight: 600; color: var(--text); text-align: center; line-height: 1.2; max-width: 72px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }  .desk-avatar {    width: 28px; height: 28px; border-radius: 50%;    display: flex; align-items: center; justify-content: center;    font-size: 10px; font-weight: 700; flex-shrink: 0;    font-family: 'DM Mono', monospace;  }  .desk-icon { font-size: 20px; color: var(--border2); }   /* ── Canvas toolbar ── */  .canvas-toolbar {    position: sticky; top: 0; z-index: 5;    display: flex; align-items: center; gap: 8px;    padding: 10px 16px;    background: var(--surface);    border-bottom: 1px solid var(--border);    flex-shrink: 0;  }   .canvas-wrap { display: flex; flex-direction: column; flex: 1; overflow: hidden; }   /* ── Right Panel: Constraints ── */  .constraint-section { margin-bottom: 16px; }  .constraint-label { font-size: 11px; font-weight: 600; color: var(--text2); letter-spacing: 0.5px; text-transform: uppercase; margin-bottom: 8px; display: flex; align-items: center; gap: 6px; }  .badge { display: inline-block; font-size: 10px; font-weight: 600; padding: 2px 6px; border-radius: 20px; }  .badge-green { background: var(--accent-light); color: var(--accent); }  .badge-red { background: var(--accent2-light); color: var(--accent2); }   .constraint-pair {    display: flex; align-items: center; gap: 6px;    padding: 6px 8px; border-radius: var(--radius-sm);    margin-bottom: 4px; font-size: 12px; font-weight: 500;  }  .constraint-pair.keep { background: var(--accent-light); color: var(--accent); }  .constraint-pair.separate { background: var(--accent2-light); color: var(--accent2); }  .constraint-pair span { flex: 1; }  .constraint-pair button { background: none; border: none; cursor: pointer; opacity: 0.5; font-size: 14px; padding: 0 2px; }  .constraint-pair button:hover { opacity: 1; }   .constraint-adder { display: flex; flex-direction: column; gap: 6px; }  .constraint-adder select {    font-family: inherit; font-size: 12px; background: var(--surface2);    border: 1px solid var(--border); border-radius: var(--radius-sm);    padding: 5px 8px; color: var(--text); width: 100%; outline: none;  }  .constraint-adder select:focus { border-color: var(--accent); }  .constraint-adder-row { display: flex; gap: 6px; }  .constraint-adder-row select { flex: 1; }   /* ── Modal ── */  .modal-overlay {    position: fixed; inset: 0; background: rgba(0,0,0,0.35);    display: flex; align-items: center; justify-content: center; z-index: 100;    opacity: 0; pointer-events: none; transition: opacity 0.2s;  }  .modal-overlay.open { opacity: 1; pointer-events: all; }  .modal {    background: var(--surface); border-radius: 14px;    padding: 24px; width: 380px; max-width: 90vw;    box-shadow: 0 20px 60px rgba(0,0,0,0.2);    transform: translateY(8px); transition: transform 0.2s;  }  .modal-overlay.open .modal { transform: translateY(0); }  .modal h3 { font-size: 16px; font-weight: 600; margin-bottom: 16px; }  .modal-row { margin-bottom: 12px; }  .modal-row label { font-size: 12px; font-weight: 500; color: var(--text2); display: block; margin-bottom: 4px; }  .modal-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 20px; }   /* ── Classes manager ── */  .class-list { display: flex; flex-direction: column; gap: 4px; margin-bottom: 12px; }  .class-item {    display: flex; align-items: center; gap: 8px;    padding: 8px 10px; border-radius: var(--radius-sm);    background: var(--surface2); cursor: pointer; transition: background 0.12s;  }  .class-item:hover { background: var(--border); }  .class-item.active { background: var(--accent-light); }  .class-item-name { flex: 1; font-size: 13px; font-weight: 500; }  .class-item-count { font-size: 11px; color: var(--text3); font-family: 'DM Mono', monospace; }   /* ── Toast ── */  .toast {    position: fixed; bottom: 20px; right: 20px;    background: var(--text); color: var(--bg);    padding: 10px 16px; border-radius: var(--radius);    font-size: 13px; font-weight: 500;    box-shadow: var(--shadow-md);    transform: translateY(60px); opacity: 0;    transition: all 0.25s; z-index: 200; pointer-events: none;  }  .toast.show { transform: translateY(0); opacity: 1; }   /* ── Empty state ── */  .empty { text-align: center; padding: 40px 20px; color: var(--text3); }  .empty-icon { font-size: 32px; margin-bottom: 8px; }  .empty p { font-size: 13px; }   /* ── Conflict indicator ── */  .conflict-line {    position: absolute; pointer-events: none;    border-top: 2px dashed #B5451B; z-index: 0; transform-origin: left center;  }   /* ── Stats bar ── */  .stats-bar {    display: flex; gap: 12px; padding: 8px 16px;    border-top: 1px solid var(--border);    font-size: 11px; color: var(--text3); flex-shrink: 0;    background: var(--surface);  }  .stats-bar b { color: var(--text); }   .divider { height: 1px; background: var(--border); margin: 10px 0; }   /* ── Grid lines hint ── */  .canvas-hint {    position: absolute; inset: 0; pointer-events: none;    display: flex; align-items: center; justify-content: center;    flex-direction: column; gap: 8px;  }  .canvas-hint p { font-size: 13px; color: var(--text3); }  .canvas-hint .hint-icon { font-size: 28px; }   .row-group {    position: absolute; left: 0; right: 0;    height: 1px; background: transparent;    border-top: 1px dashed var(--border);    pointer-events: none;  }   /* auto-layout grid preview */  #chart-canvas { transition: none; } </style></head><body> <div class="app">  <!-- TOP BAR -->  <header class="topbar">    <div class="topbar-logo">Seat<span>Plan</span></div>    <select id="class-switcher" onchange="switchClass(this.value)">      <option value="">— select a class —</option>    </select>    <button class="btn btn-secondary btn-sm" onclick="openClassModal()">+ New class</button>    <div class="topbar-spacer"></div>    <button class="btn btn-secondary btn-sm" onclick="autoArrange()">Auto-arrange</button>    <button class="btn btn-primary btn-sm" onclick="saveCurrentClass()">Save</button>  </header>   <div class="main">    <!-- LEFT: Students -->    <aside class="panel">      <div class="panel-header">        <span class="panel-title">Students</span>        <span id="student-count" style="font-size:11px;color:var(--text3);font-family:'DM Mono',monospace;">0</span>      </div>      <div class="panel-body">        <div class="student-add">          <input type="text" id="student-input" placeholder="Student name…" onkeydown="if(event.key==='Enter')addStudent()">          <button class="btn btn-primary btn-sm" onclick="addStudent()">Add</button>        </div>        <div class="student-list" id="student-list"></div>        <div id="student-empty" class="empty" style="display:none">          <div class="empty-icon">✏️</div>          <p>Add students to get started</p>        </div>      </div>      <div class="stats-bar">        <span><b id="stat-seated">0</b> seated</span>        <span><b id="stat-unseated">0</b> unseated</span>      </div>    </aside>     <!-- CENTER: Canvas -->    <div class="canvas-wrap">      <div class="canvas-toolbar">        <button class="btn btn-secondary btn-sm" onclick="addRow()">+ Row</button>        <button class="btn btn-secondary btn-sm" onclick="addDesk()">+ Desk</button>        <div style="flex:1"></div>        <label style="font-size:12px;color:var(--text2)">Grid</label>        <input type="number" id="grid-cols" value="5" min="1" max="12" style="width:52px" onchange="rebuildGrid()">        <span style="font-size:12px;color:var(--text2)">×</span>        <input type="number" id="grid-rows" value="4" min="1" max="10" style="width:52px" onchange="rebuildGrid()">        <button class="btn btn-secondary btn-sm" onclick="rebuildGrid()">Apply</button>        <button class="btn btn-danger btn-sm" onclick="clearSeats()">Clear seats</button>      </div>      <div class="canvas-area" id="canvas-area">        <div id="chart-canvas">          <div class="canvas-hint" id="canvas-hint">            <div class="hint-icon">🪑</div>            <p>Create or load a class to begin</p>          </div>        </div>      </div>    </div>     <!-- RIGHT: Constraints -->    <aside class="panel" style="border-left:1px solid var(--border)">      <div class="panel-header">        <span class="panel-title">Constraints</span>      </div>      <div class="panel-body">        <!-- Keep Together -->        <div class="constraint-section">          <div class="constraint-label">            Keep together            <span class="badge badge-green" id="keep-count">0</span>          </div>          <div id="keep-list"></div>        </div>        <div class="divider"></div>        <!-- Separate -->        <div class="constraint-section">          <div class="constraint-label">            Keep apart            <span class="badge badge-red" id="sep-count">0</span>          </div>          <div id="sep-list"></div>        </div>        <div class="divider"></div>        <!-- Add constraint -->        <div class="constraint-adder">          <div style="font-size:11px;font-weight:600;letter-spacing:0.5px;text-transform:uppercase;color:var(--text2);margin-bottom:6px">Add rule</div>          <div class="constraint-adder-row">            <select id="c-s1"><option value="">Student A</option></select>            <select id="c-s2"><option value="">Student B</option></select>          </div>          <div class="constraint-adder-row">            <button class="btn btn-secondary" style="flex:1;font-size:12px" onclick="addConstraint('keep')">✓ Keep together</button>            <button class="btn btn-danger" style="flex:1;font-size:12px" onclick="addConstraint('sep')">✗ Keep apart</button>          </div>        </div>        <div class="divider"></div>        <!-- Violations -->        <div id="violations-area" style="display:none">          <div class="constraint-label" style="color:var(--accent2)">            ⚠ Violations          </div>          <div id="violations-list" style="font-size:12px;color:var(--accent2)"></div>        </div>      </div>    </aside>  </div></div> <!-- New Class Modal --><div class="modal-overlay" id="class-modal">  <div class="modal">    <h3>New class</h3>    <div class="modal-row">      <label>Class name</label>      <input type="text" id="new-class-name" placeholder="e.g. Period 3 Math" onkeydown="if(event.key==='Enter')createClass()">    </div>    <div class="modal-row">      <label>Rows</label>      <input type="number" id="new-rows" value="4" min="1" max="10">    </div>    <div class="modal-row">      <label>Columns</label>      <input type="number" id="new-cols" value="5" min="1" max="12">    </div>    <div class="modal-actions">      <button class="btn btn-secondary" onclick="closeModal('class-modal')">Cancel</button>      <button class="btn btn-primary" onclick="createClass()">Create class</button>    </div>  </div></div> <!-- Student rename modal --><div class="modal-overlay" id="rename-modal">  <div class="modal">    <h3>Rename student</h3>    <div class="modal-row">      <label>Name</label>      <input type="text" id="rename-input" onkeydown="if(event.key==='Enter')confirmRename()">    </div>    <div class="modal-actions">      <button class="btn btn-secondary" onclick="closeModal('rename-modal')">Cancel</button>      <button class="btn btn-primary" onclick="confirmRename()">Save</button>    </div>  </div></div> <div class="toast" id="toast"></div> <script>// ── State ──────────────────────────────────────────────────────────────const STORAGE_KEY = 'seatplan_v2'; let state = {  classes: {},       // { id: { name, students:[], seats:[], constraints:[] } }  activeClass: null}; let renameTarget = null;let dragStudentId = null; // ── Init ───────────────────────────────────────────────────────────────function loadState() {  try {    const raw = localStorage.getItem(STORAGE_KEY);    if (raw) state = JSON.parse(raw);  } catch(e) {}  renderClassSwitcher();  if (state.activeClass && state.classes[state.activeClass]) {    loadClass(state.activeClass);  }} function persistState() {  localStorage.setItem(STORAGE_KEY, JSON.stringify(state));} // ── Classes ────────────────────────────────────────────────────────────function renderClassSwitcher() {  const sel = document.getElementById('class-switcher');  const cur = sel.value;  sel.innerHTML = '<option value="">— select a class —</option>';  Object.entries(state.classes).forEach(([id, cls]) => {    const opt = document.createElement('option');    opt.value = id; opt.textContent = cls.name;    if (id === state.activeClass) opt.selected = true;    sel.appendChild(opt);  });} function openClassModal() {  document.getElementById('new-class-name').value = '';  document.getElementById('new-rows').value = 4;  document.getElementById('new-cols').value = 5;  openModal('class-modal');  setTimeout(() => document.getElementById('new-class-name').focus(), 100);} function createClass() {  const name = document.getElementById('new-class-name').value.trim();  if (!name) { toast('Please enter a class name'); return; }  const rows = parseInt(document.getElementById('new-rows').value) || 4;  const cols = parseInt(document.getElementById('new-cols').value) || 5;   const id = 'cls_' + Date.now();  const seats = generateGrid(rows, cols);   state.classes[id] = { name, students: [], seats, constraints: [] };  state.activeClass = id;  persistState();  renderClassSwitcher();  loadClass(id);  closeModal('class-modal');  toast(`Class "${name}" created`);} function switchClass(id) {  if (!id) return;  state.activeClass = id;  persistState();  loadClass(id);} function loadClass(id) {  const cls = state.classes[id];  if (!cls) return;  state.activeClass = id;  document.getElementById('class-switcher').value = id;   const rows = getGridDimensions(cls.seats).rows;  const cols = getGridDimensions(cls.seats).cols;  document.getElementById('grid-rows').value = rows;  document.getElementById('grid-cols').value = cols;   renderStudents();  renderCanvas();  renderConstraints();  checkViolations();} function getClsData() {  return state.classes[state.activeClass];} function saveCurrentClass() {  persistState();  toast('Saved!');} // ── Grid ───────────────────────────────────────────────────────────────function generateGrid(rows, cols) {  const seats = [];  const padding = 20;  const dw = 90, dh = 80, gapX = 16, gapY = 14;  for (let r = 0; r < rows; r++) {    for (let c = 0; c < cols; c++) {      seats.push({        id: `seat_${r}_${c}`,        row: r, col: c,        x: padding + c * (dw + gapX),        y: padding + r * (dh + gapY),        studentId: null      });    }  }  return seats;} function getGridDimensions(seats) {  if (!seats || !seats.length) return { rows: 4, cols: 5 };  const rows = Math.max(...seats.map(s => s.row)) + 1;  const cols = Math.max(...seats.map(s => s.col)) + 1;  return { rows, cols };} function rebuildGrid() {  const cls = getClsData();  if (!cls) { toast('Please select or create a class first'); return; }  const rows = parseInt(document.getElementById('grid-rows').value) || 4;  const cols = parseInt(document.getElementById('grid-cols').value) || 5;   // Preserve assignments  const oldAssign = {};  cls.seats.forEach(s => { if (s.studentId) oldAssign[`${s.row}_${s.col}`] = s.studentId; });   cls.seats = generateGrid(rows, cols);  cls.seats.forEach(s => {    const key = `${s.row}_${s.col}`;    if (oldAssign[key]) s.studentId = oldAssign[key];  });   persistState();  renderCanvas();  renderStudents();  checkViolations();} // ── Students ───────────────────────────────────────────────────────────const COLORS = ['#2D6A4F','#1D4ED8','#7C3AED','#B45309','#0F766E','#BE185D','#9F1239','#065F46'];function studentColor(name) {  let hash = 0;  for (let i = 0; i < name.length; i++) hash = name.charCodeAt(i) + ((hash << 5) - hash);  return COLORS[Math.abs(hash) % COLORS.length];}function initials(name) {  return name.split(' ').map(w => w[0]).join('').toUpperCase().slice(0, 2);} function addStudent() {  const cls = getClsData();  if (!cls) { toast('Create or select a class first'); return; }  const input = document.getElementById('student-input');  const name = input.value.trim();  if (!name) return;  if (cls.students.find(s => s.name.toLowerCase() === name.toLowerCase())) {    toast('Student already exists'); return;  }  cls.students.push({ id: 'stu_' + Date.now() + Math.random(), name });  input.value = '';  persistState();  renderStudents();  renderConstraintSelects();  checkViolations();} function removeStudent(id) {  const cls = getClsData();  cls.students = cls.students.filter(s => s.id !== id);  cls.seats.forEach(seat => { if (seat.studentId === id) seat.studentId = null; });  cls.constraints = cls.constraints.filter(c => c.a !== id && c.b !== id);  persistState();  renderStudents();  renderCanvas();  renderConstraints();  renderConstraintSelects();  checkViolations();} function openRename(id) {  const cls = getClsData();  const stu = cls.students.find(s => s.id === id);  if (!stu) return;  renameTarget = id;  document.getElementById('rename-input').value = stu.name;  openModal('rename-modal');  setTimeout(() => document.getElementById('rename-input').focus(), 100);} function confirmRename() {  const cls = getClsData();  const stu = cls.students.find(s => s.id === renameTarget);  const newName = document.getElementById('rename-input').value.trim();  if (!newName || !stu) return;  stu.name = newName;  persistState();  renderStudents();  renderCanvas();  renderConstraints();  renderConstraintSelects();  closeModal('rename-modal');  toast('Renamed!');} function renderStudents() {  const cls = getClsData();  const list = document.getElementById('student-list');  const empty = document.getElementById('student-empty');   if (!cls || !cls.students.length) {    list.innerHTML = '';    empty.style.display = 'block';    document.getElementById('student-count').textContent = '0';    updateStats();    return;  }  empty.style.display = 'none';  document.getElementById('student-count').textContent = cls.students.length;   const seatedIds = new Set(cls.seats.filter(s => s.studentId).map(s => s.studentId));   list.innerHTML = '';  cls.students.forEach(stu => {    const seated = seatedIds.has(stu.id);    const color = studentColor(stu.name);    const chip = document.createElement('div');    chip.className = 'student-chip' + (seated ? ' seated' : '');    chip.draggable = !seated;    chip.dataset.id = stu.id;    chip.innerHTML = `      <div class="student-avatar" style="background:${color}22;color:${color}">${initials(stu.name)}</div>      <span class="student-name">${esc(stu.name)}</span>      <div class="student-actions">        <button class="btn btn-secondary btn-sm btn-icon" title="Rename" onclick="event.stopPropagation();openRename('${stu.id}')">✎</button>        <button class="btn btn-danger btn-sm btn-icon" title="Remove" onclick="event.stopPropagation();removeStudent('${stu.id}')">✕</button>      </div>`;     chip.addEventListener('dragstart', e => {      dragStudentId = stu.id;      e.dataTransfer.effectAllowed = 'move';    });    chip.addEventListener('dragend', () => { dragStudentId = null; });    list.appendChild(chip);  });   updateStats();} function updateStats() {  const cls = getClsData();  if (!cls) {    document.getElementById('stat-seated').textContent = 0;    document.getElementById('stat-unseated').textContent = 0;    return;  }  const seated = cls.seats.filter(s => s.studentId).length;  document.getElementById('stat-seated').textContent = seated;  document.getElementById('stat-unseated').textContent = cls.students.length - seated;} // ── Canvas ─────────────────────────────────────────────────────────────function renderCanvas() {  const canvas = document.getElementById('chart-canvas');  const hint = document.getElementById('canvas-hint');  const cls = getClsData();   // Clear desks (not hint)  canvas.querySelectorAll('.desk').forEach(d => d.remove());   if (!cls || !cls.seats.length) {    hint.style.display = 'flex';    canvas.style.width = '600px'; canvas.style.height = '500px';    return;  }  hint.style.display = 'none';   const dw = 90, dh = 80, gapX = 16, gapY = 14, pad = 20;  const { rows, cols } = getGridDimensions(cls.seats);  const w = pad * 2 + cols * (dw + gapX) - gapX;  const h = pad * 2 + rows * (dh + gapY) - gapY;  canvas.style.width = w + 'px';  canvas.style.height = h + 'px';   const violations = getViolationSet();   cls.seats.forEach(seat => {    const stu = seat.studentId ? cls.students.find(s => s.id === seat.studentId) : null;    const div = document.createElement('div');    div.className = 'desk' + (stu ? ' occupied' : '');    if (stu && violations.has(stu.id)) div.classList.add('conflict');    div.style.left = seat.x + 'px';    div.style.top = seat.y + 'px';    div.style.width = dw + 'px';    div.style.height = dh + 'px';    div.dataset.seatId = seat.id;     if (stu) {      const color = studentColor(stu.name);      div.innerHTML = `        <div class="desk-avatar" style="background:${color}22;color:${color}">${initials(stu.name)}</div>        <span class="desk-student">${esc(stu.name)}</span>        <span class="desk-label">${seat.row + 1},${seat.col + 1}</span>`;    } else {      div.innerHTML = `<div class="desk-icon">+</div><span class="desk-label">${seat.row + 1},${seat.col + 1}</span>`;    }     // Click to unassign    div.addEventListener('click', () => {      if (seat.studentId) {        seat.studentId = null;        persistState();        renderCanvas();        renderStudents();        checkViolations();      }    });     // Drop zone    div.addEventListener('dragover', e => {      e.preventDefault();      div.classList.add('drag-over');      if (!seat.studentId) div.classList.add('empty-drag-over');    });    div.addEventListener('dragleave', () => {      div.classList.remove('drag-over','empty-drag-over');    });    div.addEventListener('drop', e => {      e.preventDefault();      div.classList.remove('drag-over','empty-drag-over');      if (!dragStudentId) return;       // Remove student from any existing seat      cls.seats.forEach(s => { if (s.studentId === dragStudentId) s.studentId = null; });       // If seat has someone, swap      const prev = seat.studentId;      seat.studentId = dragStudentId;       // Put evicted student somewhere else (first empty seat)      if (prev && prev !== dragStudentId) {        const empty = cls.seats.find(s => !s.studentId);        if (empty) empty.studentId = prev;      }       dragStudentId = null;      persistState();      renderCanvas();      renderStudents();      checkViolations();    });     canvas.appendChild(div);  });} function clearSeats() {  const cls = getClsData();  if (!cls) return;  cls.seats.forEach(s => s.studentId = null);  persistState();  renderCanvas();  renderStudents();  checkViolations();  toast('Seats cleared');} // ── Add Row / Desk ─────────────────────────────────────────────────────function addRow() {  const cls = getClsData();  if (!cls) { toast('Create a class first'); return; }  const { rows, cols } = getGridDimensions(cls.seats);  const dw = 90, dh = 80, gapX = 16, gapY = 14, pad = 20;  for (let c = 0; c < cols; c++) {    cls.seats.push({      id: `seat_${rows}_${c}_${Date.now()}`,      row: rows, col: c,      x: pad + c * (dw + gapX),      y: pad + rows * (dh + gapY),      studentId: null    });  }  document.getElementById('grid-rows').value = rows + 1;  persistState();  renderCanvas();} function addDesk() {  const cls = getClsData();  if (!cls) { toast('Create a class first'); return; }  const { rows, cols } = getGridDimensions(cls.seats);  const dw = 90, dh = 80, gapX = 16, gapY = 14, pad = 20;  for (let r = 0; r < rows; r++) {    cls.seats.push({      id: `seat_${r}_${cols}_${Date.now()}`,      row: r, col: cols,      x: pad + cols * (dw + gapX),      y: pad + r * (dh + gapY),      studentId: null    });  }  document.getElementById('grid-cols').value = cols + 1;  persistState();  renderCanvas();} // ── Auto-arrange ───────────────────────────────────────────────────────function autoArrange() {  const cls = getClsData();  if (!cls || !cls.students.length) { toast('No students to arrange'); return; }   cls.seats.forEach(s => s.studentId = null);   // Simple greedy approach respecting constraints  const students = [...cls.students];  shuffle(students);   students.forEach(stu => {    const available = cls.seats.filter(s => !s.studentId);    if (!available.length) return;     // Score each seat: fewer violations = better    const scores = available.map(seat => {      let score = 0;      const neighbors = getNeighbors(seat, cls.seats);      const neighborStudentIds = neighbors.map(n => n.studentId).filter(Boolean);       cls.constraints.forEach(c => {        if (c.a !== stu.id && c.b !== stu.id) return;        const other = c.a === stu.id ? c.b : c.a;        const otherSeat = cls.seats.find(s => s.studentId === other);        if (!otherSeat) return;         const isNeighbor = neighborStudentIds.includes(other);        if (c.type === 'keep') score += isNeighbor ? -10 : 5;        if (c.type === 'sep') score += isNeighbor ? 20 : -2;      });      return { seat, score };    });     scores.sort((a, b) => a.score - b.score);    scores[0].seat.studentId = stu.id;  });   persistState();  renderCanvas();  renderStudents();  checkViolations();  toast('Arranged!');} function getNeighbors(seat, seats) {  return seats.filter(s =>    s.id !== seat.id &&    Math.abs(s.row - seat.row) <= 1 &&    Math.abs(s.col - seat.col) <= 1  );} function shuffle(arr) {  for (let i = arr.length - 1; i > 0; i--) {    const j = Math.floor(Math.random() * (i + 1));    [arr[i], arr[j]] = [arr[j], arr[i]];  }} // ── Constraints ────────────────────────────────────────────────────────function renderConstraintSelects() {  const cls = getClsData();  ['c-s1','c-s2'].forEach(id => {    const sel = document.getElementById(id);    const cur = sel.value;    sel.innerHTML = `<option value="">${id === 'c-s1' ? 'Student A' : 'Student B'}</option>`;    if (cls) cls.students.forEach(s => {      const opt = document.createElement('option');      opt.value = s.id; opt.textContent = s.name;      if (s.id === cur) opt.selected = true;      sel.appendChild(opt);    });  });} function addConstraint(type) {  const cls = getClsData();  const a = document.getElementById('c-s1').value;  const b = document.getElementById('c-s2').value;  if (!a || !b || a === b) { toast('Select two different students'); return; }  // Avoid duplicate  const dup = cls.constraints.find(c =>    (c.a === a && c.b === b) || (c.a === b && c.b === a)  );  if (dup) {    dup.type = type;  } else {    cls.constraints.push({ a, b, type });  }  persistState();  renderConstraints();  checkViolations();  toast(type === 'keep' ? 'Will keep together' : 'Will keep apart');} function removeConstraint(idx) {  const cls = getClsData();  cls.constraints.splice(idx, 1);  persistState();  renderConstraints();  checkViolations();} function renderConstraints() {  const cls = getClsData();  const keepList = document.getElementById('keep-list');  const sepList = document.getElementById('sep-list');   keepList.innerHTML = '';  sepList.innerHTML = '';   if (!cls) {    document.getElementById('keep-count').textContent = 0;    document.getElementById('sep-count').textContent = 0;    return;  }   const keeps = cls.constraints.filter(c => c.type === 'keep');  const seps = cls.constraints.filter(c => c.type === 'sep');   document.getElementById('keep-count').textContent = keeps.length;  document.getElementById('sep-count').textContent = seps.length;   cls.constraints.forEach((c, idx) => {    const sA = cls.students.find(s => s.id === c.a);    const sB = cls.students.find(s => s.id === c.b);    if (!sA || !sB) return;    const el = document.createElement('div');    el.className = 'constraint-pair ' + (c.type === 'keep' ? 'keep' : 'separate');    el.innerHTML = `<span>${esc(sA.name)} & ${esc(sB.name)}</span><button onclick="removeConstraint(${idx})">✕</button>`;    (c.type === 'keep' ? keepList : sepList).appendChild(el);  });   renderConstraintSelects();} // ── Violations ─────────────────────────────────────────────────────────function getViolationSet() {  const cls = getClsData();  const violating = new Set();  if (!cls) return violating;   cls.constraints.forEach(c => {    const seatA = cls.seats.find(s => s.studentId === c.a);    const seatB = cls.seats.find(s => s.studentId === c.b);    if (!seatA || !seatB) return;     const isNeighbor = Math.abs(seatA.row - seatB.row) <= 1 && Math.abs(seatA.col - seatB.col) <= 1;     if (c.type === 'sep' && isNeighbor) {      violating.add(c.a); violating.add(c.b);    }    if (c.type === 'keep' && !isNeighbor) {      violating.add(c.a); violating.add(c.b);    }  });  return violating;} function checkViolations() {  const cls = getClsData();  const area = document.getElementById('violations-area');  const list = document.getElementById('violations-list');   if (!cls) { area.style.display = 'none'; return; }   const msgs = [];  cls.constraints.forEach(c => {    const seatA = cls.seats.find(s => s.studentId === c.a);    const seatB = cls.seats.find(s => s.studentId === c.b);    if (!seatA || !seatB) return;     const sA = cls.students.find(s => s.id === c.a);    const sB = cls.students.find(s => s.id === c.b);    const isNeighbor = Math.abs(seatA.row - seatB.row) <= 1 && Math.abs(seatA.col - seatB.col) <= 1;     if (c.type === 'sep' && isNeighbor)      msgs.push(`${sA.name} & ${sB.name} are seated together`);    if (c.type === 'keep' && !isNeighbor)      msgs.push(`${sA.name} & ${sB.name} are not near each other`);  });   if (msgs.length) {    area.style.display = 'block';    list.innerHTML = msgs.map(m => `<div style="margin-bottom:4px">⚠ ${esc(m)}</div>`).join('');  } else {    area.style.display = 'none';    list.innerHTML = '';  }} // ── Utilities ──────────────────────────────────────────────────────────function esc(str) {  return str.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');} function openModal(id) { document.getElementById(id).classList.add('open'); }function closeModal(id) { document.getElementById(id).classList.remove('open'); } let toastTimer;function toast(msg) {  const el = document.getElementById('toast');  el.textContent = msg;  el.classList.add('show');  clearTimeout(toastTimer);  toastTimer = setTimeout(() => el.classList.remove('show'), 2200);} // close modals on overlay clickdocument.querySelectorAll('.modal-overlay').forEach(overlay => {  overlay.addEventListener('click', e => {    if (e.target === overlay) overlay.classList.remove('open');  });}); // Canvas drop support for no-classdocument.getElementById('chart-canvas').addEventListener('dragover', e => e.preventDefault()); // ── Boot ───────────────────────────────────────────────────────────────loadState();</script></body></html