Wedding — Seating Chart
An editorial blush-and-gold seating planner where you drag guest chips from an unassigned pool onto round banquet tables. Each table shows its capacity, fills seat by seat, and gently rejects overbooking. Remove a guest to send them back to the pool, watch live counts update, and shuffle a plus-one into place with soft toasts, keyboard support, and thin gold rules throughout.
MCP
Code
:root {
--blush: #e8b7b0;
--blush-50: #fbeeec;
--rose: #c98a86;
--gold: #c9a24b;
--gold-d: #a8862f;
--ink: #3a2b2b;
--ink-2: #6b5555;
--muted: #9a8585;
--bg: #fbf7f4;
--cream: #f4ece4;
--white: #fff;
--line: rgba(58, 43, 43, 0.12);
--r-sm: 8px;
--r-md: 16px;
--r-lg: 26px;
--shadow: 0 18px 40px -22px rgba(58, 43, 43, 0.4);
}
* { box-sizing: border-box; }
html { -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; }
body {
margin: 0;
font-family: "Inter", system-ui, -apple-system, sans-serif;
line-height: 1.5;
color: var(--ink);
background:
radial-gradient(1100px 520px at 80% -10%, var(--blush-50), transparent 60%),
radial-gradient(900px 480px at -5% 0%, var(--cream), transparent 55%),
var(--bg);
min-height: 100vh;
}
.sr-only {
position: absolute;
width: 1px; height: 1px;
padding: 0; margin: -1px;
overflow: hidden; clip: rect(0 0 0 0);
white-space: nowrap; border: 0;
}
.page {
max-width: 1180px;
margin: 0 auto;
padding: 48px 24px 72px;
}
/* Masthead */
.masthead { text-align: center; margin-bottom: 40px; }
.eyebrow {
font-size: 12px;
letter-spacing: 0.32em;
text-transform: uppercase;
color: var(--gold-d);
font-weight: 600;
margin: 0 0 10px;
}
.masthead h1 {
font-family: "Cormorant Garamond", Georgia, serif;
font-weight: 500;
font-size: clamp(40px, 7vw, 68px);
line-height: 1.02;
margin: 0;
color: var(--ink);
}
.masthead h1::after {
content: "";
display: block;
width: 84px;
height: 1px;
margin: 18px auto 0;
background: linear-gradient(90deg, transparent, var(--gold), transparent);
}
.sub {
max-width: 560px;
margin: 18px auto 0;
color: var(--ink-2);
font-size: 15px;
}
.stats {
display: inline-flex;
align-items: center;
gap: 14px;
margin-top: 22px;
padding: 10px 20px;
background: var(--white);
border: 1px solid var(--line);
border-radius: 999px;
box-shadow: var(--shadow);
font-size: 13.5px;
color: var(--ink-2);
}
.stats strong { color: var(--ink); font-weight: 700; }
.stats .dot {
width: 4px; height: 4px;
border-radius: 50%;
background: var(--blush);
}
/* Board layout */
.board {
display: grid;
grid-template-columns: 288px 1fr;
gap: 28px;
align-items: start;
}
/* Pool */
.pool {
position: sticky;
top: 20px;
background: var(--white);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 20px;
box-shadow: var(--shadow);
}
.pool.drag-over {
border-color: var(--rose);
box-shadow: 0 0 0 3px var(--blush-50), var(--shadow);
}
.pool-head {
display: flex;
align-items: baseline;
justify-content: space-between;
margin-bottom: 14px;
}
.pool-head h2 {
font-family: "Cormorant Garamond", Georgia, serif;
font-weight: 600;
font-size: 24px;
margin: 0;
}
.badge {
min-width: 26px;
padding: 2px 9px;
text-align: center;
font-size: 12px;
font-weight: 700;
color: var(--gold-d);
background: var(--blush-50);
border: 1px solid var(--line);
border-radius: 999px;
}
.pool-search input {
width: 100%;
padding: 10px 14px;
font: inherit;
font-size: 14px;
color: var(--ink);
background: var(--bg);
border: 1px solid var(--line);
border-radius: var(--r-sm);
margin-bottom: 16px;
}
.pool-search input::placeholder { color: var(--muted); }
.pool-search input:focus-visible {
outline: none;
border-color: var(--rose);
box-shadow: 0 0 0 3px var(--blush-50);
}
.chip-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-wrap: wrap;
gap: 8px;
min-height: 60px;
}
.pool .chip-list {
max-height: 46vh;
overflow-y: auto;
padding-right: 4px;
}
.chip-list:empty::after {
content: "Everyone is seated ✦";
display: block;
width: 100%;
text-align: center;
color: var(--muted);
font-size: 13px;
padding: 18px 0;
}
/* Chip */
.chip {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 7px 10px 7px 8px;
background: var(--white);
border: 1px solid var(--line);
border-radius: 999px;
font-size: 13px;
font-weight: 500;
color: var(--ink);
cursor: grab;
user-select: none;
transition: transform 0.14s ease, box-shadow 0.14s ease, border-color 0.14s ease;
}
.chip:hover {
border-color: var(--rose);
box-shadow: 0 8px 18px -12px rgba(58, 43, 43, 0.5);
transform: translateY(-1px);
}
.chip:focus-visible {
outline: none;
border-color: var(--gold);
box-shadow: 0 0 0 3px var(--blush-50);
}
.chip.dragging {
opacity: 0.5;
cursor: grabbing;
transform: scale(0.96);
}
.chip .avatar {
width: 24px; height: 24px;
flex: none;
border-radius: 50%;
display: grid;
place-items: center;
font-size: 11px;
font-weight: 700;
color: var(--white);
background: linear-gradient(135deg, var(--rose), var(--gold));
}
.chip .side {
font-size: 10px;
color: var(--muted);
font-weight: 600;
}
.reset {
margin-top: 18px;
width: 100%;
padding: 10px;
font: inherit;
font-size: 13px;
font-weight: 600;
color: var(--ink-2);
background: var(--bg);
border: 1px solid var(--line);
border-radius: var(--r-sm);
cursor: pointer;
transition: background 0.15s ease, color 0.15s ease;
}
.reset:hover { background: var(--blush-50); color: var(--ink); }
.reset:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--blush-50); }
/* Tables grid */
.tables {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(232px, 1fr));
gap: 24px;
}
.table-card {
background: var(--white);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 20px 18px 22px;
box-shadow: var(--shadow);
text-align: center;
transition: border-color 0.18s ease, box-shadow 0.18s ease;
}
.table-card.full {
border-color: var(--gold);
box-shadow: 0 0 0 1px var(--gold), var(--shadow);
}
.table-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 14px;
}
.table-head h3 {
font-family: "Cormorant Garamond", Georgia, serif;
font-weight: 600;
font-size: 21px;
margin: 0;
color: var(--ink);
}
.cap {
font-size: 11.5px;
font-weight: 700;
padding: 3px 9px;
border-radius: 999px;
background: var(--blush-50);
color: var(--gold-d);
border: 1px solid var(--line);
}
.table-card.full .cap { background: var(--gold); color: var(--white); border-color: var(--gold); }
/* Round table with seats around it */
.round {
position: relative;
width: 180px;
height: 180px;
margin: 8px auto 0;
}
.centerpiece {
position: absolute;
inset: 34px;
border-radius: 50%;
background:
radial-gradient(circle at 50% 38%, var(--blush-50), var(--cream));
border: 1px solid var(--gold);
box-shadow: inset 0 0 0 4px var(--white), inset 0 0 0 5px var(--line);
display: grid;
place-items: center;
color: var(--gold-d);
font-family: "Cormorant Garamond", Georgia, serif;
font-size: 15px;
}
.centerpiece span { font-weight: 600; letter-spacing: 0.04em; }
.table-card.full .centerpiece {
animation: bloom 0.5s ease;
}
@keyframes bloom {
0% { transform: scale(1); }
50% { transform: scale(1.06); box-shadow: inset 0 0 0 4px var(--white), 0 0 24px -6px var(--gold); }
100% { transform: scale(1); }
}
.seat {
position: absolute;
width: 38px; height: 38px;
margin: -19px 0 0 -19px;
border-radius: 50%;
border: 1.5px dashed var(--line);
background: var(--bg);
display: grid;
place-items: center;
font-size: 10px;
font-weight: 700;
color: var(--muted);
cursor: pointer;
padding: 0;
transition: transform 0.14s ease, border-color 0.14s ease, background 0.14s ease;
}
.seat:hover,
.seat:focus-visible {
outline: none;
border-color: var(--rose);
transform: scale(1.12);
}
.seat.drag-over {
border-style: solid;
border-color: var(--gold);
background: var(--blush-50);
transform: scale(1.15);
}
.seat.occupied {
border-style: solid;
border-color: transparent;
background: linear-gradient(135deg, var(--rose), var(--gold));
color: var(--white);
cursor: grab;
}
.seat.occupied:hover { transform: scale(1.14); box-shadow: 0 8px 16px -8px rgba(58,43,43,0.6); }
.seat.occupied.dragging { opacity: 0.5; }
.seat .remove {
position: absolute;
top: -6px; right: -6px;
width: 16px; height: 16px;
border-radius: 50%;
border: none;
background: var(--ink);
color: var(--white);
font-size: 11px;
line-height: 1;
display: none;
place-items: center;
cursor: pointer;
padding: 0;
}
.seat.occupied:hover .remove,
.seat.occupied:focus-within .remove { display: grid; }
.table-foot {
margin-top: 14px;
font-size: 12px;
color: var(--muted);
}
.table-foot strong { color: var(--ink-2); }
/* Toast */
.toast-wrap {
position: fixed;
left: 50%;
bottom: 26px;
transform: translateX(-50%);
display: flex;
flex-direction: column;
gap: 10px;
z-index: 60;
pointer-events: none;
}
.toast {
background: var(--ink);
color: var(--white);
padding: 11px 18px;
border-radius: 999px;
font-size: 13.5px;
font-weight: 500;
box-shadow: var(--shadow);
display: flex;
align-items: center;
gap: 9px;
opacity: 0;
transform: translateY(10px);
animation: toast-in 0.28s forwards;
}
.toast.gold { background: linear-gradient(135deg, var(--gold-d), var(--gold)); }
.toast::before {
content: "✦";
color: var(--gold);
}
.toast.gold::before { color: var(--white); }
.toast.out { animation: toast-out 0.28s forwards; }
@keyframes toast-in { to { opacity: 1; transform: translateY(0); } }
@keyframes toast-out { to { opacity: 0; transform: translateY(10px); } }
@media (max-width: 520px) {
.page { padding: 32px 16px 56px; }
.board { grid-template-columns: 1fr; gap: 20px; }
.pool { position: static; }
.pool .chip-list { max-height: none; }
.tables { grid-template-columns: 1fr; }
.stats { flex-wrap: wrap; justify-content: center; }
}(function () {
"use strict";
/* ---------- Data ---------- */
var GUESTS = [
{ id: "g1", name: "Amara Voss", side: "Bride" },
{ id: "g2", name: "Theo Bright", side: "Groom" },
{ id: "g3", name: "Nadia Fell", side: "Bride" },
{ id: "g4", name: "Marcus Lyle", side: "Groom" },
{ id: "g5", name: "Priya Anand", side: "Bride" },
{ id: "g6", name: "Ivo Kessler", side: "Groom" },
{ id: "g7", name: "Delphine Ray", side: "Bride" },
{ id: "g8", name: "Otto Crane", side: "Groom" },
{ id: "g9", name: "Selma Wren", side: "Bride" },
{ id: "g10", name: "Cyrus Bell", side: "Groom" },
{ id: "g11", name: "Talia Moss", side: "Bride" },
{ id: "g12", name: "Rafe Dunmore", side: "Groom" },
{ id: "g13", name: "Odessa Kim", side: "Bride" },
{ id: "g14", name: "Julian Frost", side: "Groom" },
{ id: "g15", name: "Bianca Serel", side: "Bride" },
{ id: "g16", name: "Emmett Cole", side: "Groom" },
{ id: "g17", name: "Wren Aldous", side: "Bride" },
{ id: "g18", name: "Silas Vaughn", side: "Groom" },
{ id: "g19", name: "Cora Nightingale", side: "Bride" },
{ id: "g20", name: "Dante Fiore", side: "Groom" }
];
var TABLES = [
{ id: "t1", name: "Peony", seats: 6 },
{ id: "t2", name: "Laurel", seats: 6 },
{ id: "t3", name: "Ranunculus", seats: 4 },
{ id: "t4", name: "Sweet Pea", seats: 4 }
];
/* state: guestId -> { table, seat } | null (in pool) */
var placement = {};
GUESTS.forEach(function (g) { placement[g.id] = null; });
var byId = {};
GUESTS.forEach(function (g) { byId[g.id] = g; });
/* ---------- DOM refs ---------- */
var poolEl = document.getElementById("pool");
var tablesEl = document.getElementById("tables");
var searchEl = document.getElementById("search");
var resetEl = document.getElementById("reset");
var toastWrap = document.getElementById("toastWrap");
var liveEl = document.getElementById("live");
var poolBadge = document.getElementById("poolBadge");
var selectedChipId = null; /* keyboard: chip awaiting a seat */
/* ---------- Helpers ---------- */
function initials(name) {
return name.split(" ").map(function (p) { return p[0]; }).join("").slice(0, 2).toUpperCase();
}
function seatedCountFor(tableId) {
var n = 0;
for (var id in placement) {
if (placement[id] && placement[id].table === tableId) n++;
}
return n;
}
function firstOpenSeat(tableId) {
var t = tableFor(tableId);
var taken = {};
for (var id in placement) {
if (placement[id] && placement[id].table === tableId) taken[placement[id].seat] = true;
}
for (var s = 0; s < t.seats; s++) { if (!taken[s]) return s; }
return -1;
}
function tableFor(id) {
return TABLES.filter(function (t) { return t.id === id; })[0];
}
function guestAt(tableId, seat) {
for (var id in placement) {
if (placement[id] && placement[id].table === tableId && placement[id].seat === seat) return id;
}
return null;
}
function announce(msg) { liveEl.textContent = msg; }
function toast(msg, gold) {
var el = document.createElement("div");
el.className = "toast" + (gold ? " gold" : "");
el.textContent = msg;
toastWrap.appendChild(el);
setTimeout(function () {
el.classList.add("out");
setTimeout(function () { el.remove(); }, 300);
}, 2400);
}
/* ---------- Placement actions ---------- */
function assign(guestId, tableId, seat) {
var t = tableFor(tableId);
if (seatedCountFor(tableId) >= t.seats && (!placement[guestId] || placement[guestId].table !== tableId)) {
toast("Table " + t.name + " is full", true);
return false;
}
var occupant = guestAt(tableId, seat);
if (occupant && occupant !== guestId) {
/* swap seat target: send occupant to pool if no room, else pick next open */
var open = firstOpenSeat(tableId);
if (open === -1 || open === seat) {
placement[occupant] = null;
} else {
placement[occupant] = { table: tableId, seat: open };
}
}
placement[guestId] = { table: tableId, seat: seat };
render();
var willBeFull = seatedCountFor(tableId) >= t.seats;
announce(byId[guestId].name + " seated at Table " + t.name);
if (willBeFull) toast("Table " + t.name + " is complete", true);
else toast(byId[guestId].name + " → Table " + t.name);
return true;
}
function unassign(guestId) {
if (!placement[guestId]) return;
placement[guestId] = null;
render();
announce(byId[guestId].name + " returned to the pool");
toast(byId[guestId].name + " back to pool");
}
/* ---------- Chip factory ---------- */
function makeChip(guest, context) {
var chip = document.createElement(context === "pool" ? "li" : "span");
chip.className = "chip";
chip.setAttribute("draggable", "true");
chip.dataset.guest = guest.id;
chip.tabIndex = 0;
chip.setAttribute("role", "button");
chip.setAttribute("aria-label", guest.name + ", " + guest.side + " side" +
(context === "pool" ? ". Press Enter then choose a seat." : ""));
var av = document.createElement("span");
av.className = "avatar";
av.textContent = initials(guest.name);
av.setAttribute("aria-hidden", "true");
var label = document.createElement("span");
label.textContent = guest.name;
var side = document.createElement("span");
side.className = "side";
side.textContent = guest.side === "Bride" ? "B" : "G";
chip.appendChild(av);
chip.appendChild(label);
chip.appendChild(side);
chip.addEventListener("dragstart", function (e) {
e.dataTransfer.setData("text/plain", guest.id);
e.dataTransfer.effectAllowed = "move";
chip.classList.add("dragging");
});
chip.addEventListener("dragend", function () { chip.classList.remove("dragging"); });
chip.addEventListener("keydown", function (e) {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
if (context === "pool") {
selectedChipId = selectedChipId === guest.id ? null : guest.id;
if (selectedChipId) {
toast("Choose an open seat for " + guest.name);
announce("Selected " + guest.name + ". Now activate an open seat.");
}
} else {
unassign(guest.id);
}
}
});
return chip;
}
/* ---------- Render ---------- */
function render() {
/* Pool */
var q = (searchEl.value || "").trim().toLowerCase();
poolEl.innerHTML = "";
var poolGuests = GUESTS.filter(function (g) { return placement[g.id] === null; });
var visible = poolGuests.filter(function (g) {
return !q || g.name.toLowerCase().indexOf(q) !== -1 || g.side.toLowerCase().indexOf(q) !== -1;
});
visible.forEach(function (g) { poolEl.appendChild(makeChip(g, "pool")); });
poolBadge.textContent = poolGuests.length;
/* Tables */
tablesEl.innerHTML = "";
TABLES.forEach(function (t) {
tablesEl.appendChild(makeTableCard(t));
});
/* Stats */
var seated = GUESTS.filter(function (g) { return placement[g.id] !== null; }).length;
document.getElementById("seatedCount").textContent = seated;
document.getElementById("poolCount").textContent = GUESTS.length - seated;
document.getElementById("tableCount").textContent = TABLES.length;
}
function makeTableCard(t) {
var card = document.createElement("section");
card.className = "table-card";
var count = seatedCountFor(t.id);
var full = count >= t.seats;
if (full) card.className += " full";
var head = document.createElement("div");
head.className = "table-head";
var h3 = document.createElement("h3");
h3.textContent = "Table " + t.name;
var cap = document.createElement("span");
cap.className = "cap";
cap.textContent = count + " / " + t.seats;
head.appendChild(h3);
head.appendChild(cap);
card.appendChild(head);
var round = document.createElement("div");
round.className = "round";
var center = document.createElement("div");
center.className = "centerpiece";
var cspan = document.createElement("span");
cspan.textContent = full ? "Complete" : t.name;
center.appendChild(cspan);
round.appendChild(center);
/* seats around a circle */
var radius = 90;
for (var s = 0; s < t.seats; s++) {
var angle = (s / t.seats) * Math.PI * 2 - Math.PI / 2;
var x = 90 + Math.cos(angle) * (radius - 20);
var y = 90 + Math.sin(angle) * (radius - 20);
round.appendChild(makeSeat(t, s, x, y));
}
card.appendChild(round);
var foot = document.createElement("p");
foot.className = "table-foot";
foot.innerHTML = full
? "Seating <strong>complete</strong> ✦"
: "<strong>" + (t.seats - count) + "</strong> seat" + (t.seats - count === 1 ? "" : "s") + " remaining";
card.appendChild(foot);
return card;
}
function makeSeat(t, seatIndex, x, y) {
var seat = document.createElement("button");
seat.type = "button";
seat.className = "seat";
seat.style.left = x + "px";
seat.style.top = y + "px";
seat.dataset.table = t.id;
seat.dataset.seat = seatIndex;
var guestId = guestAt(t.id, seatIndex);
if (guestId) {
var g = byId[guestId];
seat.className += " occupied";
seat.setAttribute("draggable", "true");
seat.setAttribute("aria-label", g.name + " at Table " + t.name + ", seat " + (seatIndex + 1) + ". Activate to remove.");
seat.textContent = initials(g.name);
var rm = document.createElement("span");
rm.className = "remove";
rm.setAttribute("role", "button");
rm.setAttribute("aria-hidden", "true");
rm.textContent = "×";
seat.appendChild(rm);
seat.addEventListener("dragstart", function (e) {
e.dataTransfer.setData("text/plain", guestId);
e.dataTransfer.effectAllowed = "move";
seat.classList.add("dragging");
});
seat.addEventListener("dragend", function () { seat.classList.remove("dragging"); });
seat.addEventListener("click", function () { unassign(guestId); });
} else {
seat.setAttribute("aria-label", "Open seat " + (seatIndex + 1) + " at Table " + t.name);
seat.textContent = seatIndex + 1;
seat.addEventListener("click", function () {
if (selectedChipId) {
var gid = selectedChipId;
selectedChipId = null;
assign(gid, t.id, seatIndex);
} else {
toast("Select a guest from the pool first");
}
});
}
/* drop handling on every seat */
seat.addEventListener("dragover", function (e) {
e.preventDefault();
e.dataTransfer.dropEffect = "move";
seat.classList.add("drag-over");
});
seat.addEventListener("dragleave", function () { seat.classList.remove("drag-over"); });
seat.addEventListener("drop", function (e) {
e.preventDefault();
seat.classList.remove("drag-over");
var gid = e.dataTransfer.getData("text/plain");
if (gid) assign(gid, t.id, seatIndex);
});
return seat;
}
/* ---------- Pool as drop target (unassign) ---------- */
var poolAside = poolEl.closest(".pool");
poolAside.addEventListener("dragover", function (e) {
e.preventDefault();
e.dataTransfer.dropEffect = "move";
poolAside.classList.add("drag-over");
});
poolAside.addEventListener("dragleave", function (e) {
if (!poolAside.contains(e.relatedTarget)) poolAside.classList.remove("drag-over");
});
poolAside.addEventListener("drop", function (e) {
e.preventDefault();
poolAside.classList.remove("drag-over");
var gid = e.dataTransfer.getData("text/plain");
if (gid && placement[gid]) unassign(gid);
});
/* ---------- Controls ---------- */
searchEl.addEventListener("input", render);
resetEl.addEventListener("click", function () {
GUESTS.forEach(function (g) { placement[g.id] = null; });
selectedChipId = null;
render();
toast("All guests returned to the pool");
announce("Seating chart reset.");
});
/* ---------- Init ---------- */
render();
toast("Drag guests onto a seat to begin ✦", true);
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Wedding — Seating Chart</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@400;500;600&family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="page">
<header class="masthead">
<p class="eyebrow">Celeste & Rowan · 14 September</p>
<h1>The Seating Chart</h1>
<p class="sub">Drag each guest from the pool onto an open seat. Tables gently refuse once full — send a guest back to the pool anytime.</p>
<div class="stats" role="status" aria-live="polite">
<span class="stat"><strong id="seatedCount">0</strong> seated</span>
<span class="dot" aria-hidden="true"></span>
<span class="stat"><strong id="poolCount">0</strong> waiting</span>
<span class="dot" aria-hidden="true"></span>
<span class="stat"><strong id="tableCount">0</strong> tables</span>
</div>
</header>
<div class="board">
<aside class="pool" aria-label="Unassigned guests">
<div class="pool-head">
<h2>Unassigned</h2>
<span class="badge" id="poolBadge">0</span>
</div>
<div class="pool-search">
<input type="search" id="search" placeholder="Find a guest…" aria-label="Search guests" autocomplete="off" />
</div>
<ul class="chip-list" id="pool" aria-label="Guest pool, drop here to unassign"></ul>
<button class="reset" id="reset" type="button">Reset all seats</button>
</aside>
<main class="tables" id="tables" aria-label="Reception tables"></main>
</div>
</div>
<div class="toast-wrap" id="toastWrap" aria-live="polite" aria-atomic="true"></div>
<p class="sr-only" id="live" aria-live="assertive"></p>
<script src="script.js"></script>
</body>
</html>A romantic, print-inspired seating chart for planning a wedding reception. Guests wait in an unassigned pool as pill-shaped chips; each round table renders its seats as a ring around a gold-rimmed centerpiece, with a live count and a soft capacity badge. Drag a chip onto any open seat to assign the guest, and drag them back — or click the small remove control — to return them to the pool.
The layout leans on a display serif for headings, thin gold hairlines, and a warm blush-cream palette so the tool feels like a keepsake rather than a spreadsheet. Tables that reach capacity glow and refuse new drops with a gentle toast, keeping the plan honest. Micro-interactions — chip lift on grab, seat pulse on hover, and a subtle bloom when a table fills — make the assignment flow feel tactile.
Everything runs on vanilla JavaScript using the native HTML Drag and Drop API, with keyboard fallbacks: focus a chip, press a seat, and the guest slots in. Live regions announce assignments for assistive tech, counts stay in sync, and the whole board reflows into a single readable column on narrow screens.