Pages Medium
Tableside Tablet Ordering
Landscape tableside tablet: table header with party + server, category strip, menu grid, live ticket, call-server pill and a 'how it's going' progress bar.
Open in Lab
MCP
html css vanilla-js
Targets: JS HTML
Code
:root {
--cream: #f5f0e8;
--cream-2: #ece4d4;
--bone: #faf7f1;
--terracotta: #c1714a;
--terracotta-d: #a05a38;
--forest: #2d4a3e;
--forest-d: #1e3329;
--gold: #c9a84c;
--gold-light: #e6c97a;
--ink: #2c1a0e;
--ink-2: #4a3828;
--warm-gray: #7a6a58;
--success: #4f7a3a;
--danger: #b3432a;
--warning: #d99020;
--font-display: "Playfair Display", Georgia, serif;
--font-body: "Inter", system-ui, sans-serif;
--font-mono: "JetBrains Mono", ui-monospace, monospace;
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html,
body {
height: 100%;
}
[hidden] {
display: none !important;
}
body {
font-family: var(--font-body);
background: #1a0f06;
-webkit-font-smoothing: antialiased;
display: grid;
place-items: center;
padding: 16px;
user-select: none;
}
.tablet {
width: 100%;
max-width: 1180px;
height: min(96vh, 760px);
background: #2a2017;
border-radius: 26px;
padding: 14px;
box-shadow: 0 26px 70px rgba(0, 0, 0, 0.55);
}
.screen {
position: relative;
height: 100%;
background: var(--cream);
border-radius: 18px;
overflow: hidden;
display: flex;
flex-direction: column;
color: var(--ink);
}
/* Top */
.top {
display: grid;
grid-template-columns: auto auto 1fr;
align-items: center;
gap: 24px;
padding: 14px 22px;
background: var(--bone);
border-bottom: 1px solid rgba(44, 26, 14, 0.08);
}
.brand {
display: flex;
flex-direction: column;
}
.brand-name {
font-family: var(--font-display);
font-weight: 800;
font-size: 1.05rem;
}
.brand-tag {
font-size: 0.66rem;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--terracotta);
font-weight: 700;
}
.table-info {
border-left: 1px solid rgba(44, 26, 14, 0.16);
padding-left: 18px;
}
.ti-table {
font-family: var(--font-display);
font-weight: 800;
font-size: 1.1rem;
}
.ti-server {
font-size: 0.78rem;
color: var(--warm-gray);
margin-top: 2px;
}
.progress {
justify-self: end;
}
.progress ol {
list-style: none;
display: flex;
gap: 8px;
background: var(--cream);
border: 1px solid rgba(44, 26, 14, 0.08);
border-radius: 999px;
padding: 4px;
}
.progress li {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 12px 4px 4px;
font-size: 0.78rem;
font-weight: 600;
color: var(--warm-gray);
border-radius: 999px;
}
.progress li.is-done {
color: var(--success);
}
.progress li.is-current {
background: var(--forest);
color: var(--bone);
}
.progress .pill {
width: 22px;
height: 22px;
border-radius: 999px;
background: var(--cream-2);
color: var(--ink-2);
font-family: var(--font-mono);
font-size: 0.72rem;
display: grid;
place-items: center;
font-weight: 700;
}
.progress li.is-done .pill {
background: var(--success);
color: var(--bone);
}
.progress li.is-current .pill {
background: var(--gold);
color: var(--ink);
}
/* Catbar */
.catbar {
display: flex;
gap: 6px;
padding: 10px 22px;
background: var(--bone);
border-bottom: 1px solid rgba(44, 26, 14, 0.08);
overflow-x: auto;
flex-shrink: 0;
}
.cat-chip {
background: transparent;
border: 1px solid transparent;
font-family: inherit;
font-size: 0.86rem;
font-weight: 600;
color: var(--ink-2);
padding: 9px 16px;
border-radius: 999px;
cursor: pointer;
white-space: nowrap;
display: inline-flex;
align-items: center;
gap: 8px;
}
.cat-chip:hover {
background: var(--cream-2);
}
.cat-chip.is-active {
background: var(--forest);
color: var(--bone);
}
.cat-chip-icon {
font-size: 1rem;
}
/* Board */
.board {
flex: 1;
display: grid;
grid-template-columns: 1fr 340px;
min-height: 0;
}
@media (max-width: 880px) {
.board {
grid-template-columns: 1fr;
grid-template-rows: 1fr auto;
}
}
/* Menu */
.menu {
display: flex;
flex-direction: column;
min-height: 0;
border-right: 1px solid rgba(44, 26, 14, 0.08);
}
.menu-head {
display: flex;
justify-content: space-between;
align-items: baseline;
padding: 16px 22px 8px;
}
.menu-head h1 {
font-family: var(--font-display);
font-weight: 700;
font-size: 1.5rem;
letter-spacing: -0.01em;
}
.menu-hint {
font-size: 0.78rem;
color: var(--warm-gray);
font-style: italic;
}
.grid {
flex: 1;
overflow-y: auto;
padding: 8px 22px 22px;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 10px;
align-content: start;
}
.tile {
background: var(--bone);
border: 1px solid rgba(44, 26, 14, 0.08);
border-radius: 12px;
padding: 14px 14px 12px;
cursor: pointer;
display: flex;
flex-direction: column;
gap: 4px;
text-align: left;
font-family: inherit;
color: var(--ink);
min-height: 110px;
transition: border-color 0.15s, background 0.15s, transform 0.05s;
}
.tile:hover {
border-color: var(--terracotta);
}
.tile:active {
transform: scale(0.97);
background: var(--cream-2);
}
.tile.is-flash {
animation: flash 0.5s ease;
}
@keyframes flash {
0% {
background: var(--gold);
border-color: var(--gold);
}
100% {
background: var(--bone);
border-color: rgba(44, 26, 14, 0.08);
}
}
.tile-glyph {
font-size: 1.5rem;
}
.tile-name {
font-family: var(--font-display);
font-weight: 700;
font-size: 1rem;
line-height: 1.15;
}
.tile-desc {
font-size: 0.74rem;
color: var(--warm-gray);
flex: 1;
}
.tile-price {
font-family: var(--font-mono);
font-weight: 700;
font-size: 0.86rem;
color: var(--terracotta-d);
margin-top: 2px;
}
/* Ticket */
.ticket {
background: var(--bone);
display: flex;
flex-direction: column;
min-height: 0;
}
.ticket-head {
padding: 16px 22px 10px;
border-bottom: 1px dashed rgba(44, 26, 14, 0.18);
}
.ticket-eyebrow {
font-size: 0.66rem;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--terracotta);
font-weight: 700;
}
.ticket-meta {
font-family: var(--font-display);
font-weight: 800;
font-size: 1.1rem;
margin-top: 4px;
}
.lines {
list-style: none;
flex: 1;
overflow-y: auto;
padding: 8px 22px;
}
.lines li {
display: grid;
grid-template-columns: 1fr auto auto;
align-items: center;
gap: 12px;
padding: 10px 0;
border-bottom: 1px dashed rgba(44, 26, 14, 0.1);
font-size: 0.92rem;
}
.lines li:last-child {
border-bottom: none;
}
.l-name {
font-weight: 600;
}
.l-name small {
display: block;
color: var(--warm-gray);
font-weight: 500;
font-size: 0.74rem;
font-style: italic;
margin-top: 2px;
}
.l-qty {
display: inline-flex;
align-items: center;
background: var(--cream);
border: 1px solid rgba(44, 26, 14, 0.12);
border-radius: 999px;
padding: 2px;
gap: 2px;
}
.l-qty button {
width: 26px;
height: 26px;
border-radius: 999px;
border: none;
background: transparent;
color: var(--ink-2);
font-size: 1rem;
font-weight: 700;
cursor: pointer;
display: grid;
place-items: center;
}
.l-qty button:hover {
background: var(--cream-2);
color: var(--terracotta-d);
}
.l-qty span {
min-width: 22px;
text-align: center;
font-family: var(--font-mono);
font-weight: 700;
font-size: 0.82rem;
}
.l-price {
font-family: var(--font-mono);
font-weight: 700;
font-size: 0.92rem;
}
.empty {
flex: 1;
display: grid;
place-items: center;
padding: 22px;
color: var(--warm-gray);
font-style: italic;
font-size: 0.86rem;
text-align: center;
}
.totals {
padding: 12px 22px 8px;
border-top: 1px dashed rgba(44, 26, 14, 0.18);
display: flex;
flex-direction: column;
gap: 3px;
}
.totals div {
display: flex;
justify-content: space-between;
font-size: 0.86rem;
color: var(--ink-2);
}
.totals dd {
font-family: var(--font-mono);
font-weight: 700;
}
.totals .big {
margin-top: 4px;
padding-top: 8px;
border-top: 1px solid rgba(44, 26, 14, 0.16);
font-size: 1rem;
color: var(--ink);
font-weight: 700;
}
.totals .big dd {
font-size: 1.15rem;
}
.ticket-actions {
display: flex;
gap: 8px;
padding: 8px 22px;
}
.ghost,
.primary,
.bill-btn,
.bill-confirm {
border-radius: 999px;
font-family: inherit;
font-size: 0.88rem;
font-weight: 700;
cursor: pointer;
transition: background 0.15s, color 0.15s, border-color 0.15s;
padding: 11px 16px;
border: 1px solid transparent;
}
.ghost {
background: transparent;
border-color: rgba(44, 26, 14, 0.2);
color: var(--ink-2);
display: inline-flex;
align-items: center;
gap: 8px;
}
.ghost .dot {
width: 8px;
height: 8px;
background: var(--terracotta);
border-radius: 999px;
}
.ghost.is-called {
background: var(--terracotta);
color: var(--bone);
border-color: var(--terracotta-d);
}
.ghost.is-called .dot {
background: var(--bone);
animation: blink 1s ease-in-out infinite;
}
@keyframes blink {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.4;
}
}
.primary {
background: var(--forest);
color: var(--bone);
flex: 1;
}
.primary:hover:not(:disabled) {
background: var(--forest-d);
}
.primary:disabled {
background: var(--warm-gray);
cursor: not-allowed;
opacity: 0.7;
}
.bill-btn {
margin: 4px 22px 18px;
background: var(--gold);
color: var(--ink);
border: none;
border-radius: 999px;
padding: 13px 16px;
font-size: 0.92rem;
}
.bill-btn:hover {
background: var(--gold-light);
}
/* Toast */
.toast {
position: absolute;
bottom: 26px;
left: 50%;
transform: translateX(-50%);
background: var(--forest-d);
color: var(--bone);
padding: 10px 18px;
border-radius: 999px;
font-weight: 600;
font-size: 0.86rem;
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.35);
z-index: 6;
}
/* Bill overlay */
.bill-overlay {
position: absolute;
inset: 0;
background: rgba(44, 26, 14, 0.55);
display: grid;
place-items: center;
z-index: 8;
padding: 24px;
}
.bill-card {
width: 100%;
max-width: 480px;
background: var(--bone);
border-radius: 16px;
padding: 28px 30px 22px;
display: flex;
flex-direction: column;
gap: 16px;
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.4);
}
.kicker {
font-size: 0.7rem;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--terracotta);
font-weight: 700;
}
.bill-card h2 {
font-family: var(--font-display);
font-weight: 700;
font-size: 1.5rem;
margin-top: 2px;
letter-spacing: -0.01em;
}
.bill-options {
display: flex;
flex-direction: column;
gap: 8px;
}
.bill-opt {
background: var(--cream);
border: 1px solid rgba(44, 26, 14, 0.12);
border-radius: 12px;
padding: 14px 18px;
display: flex;
justify-content: space-between;
align-items: center;
font-family: inherit;
font-size: 0.96rem;
font-weight: 600;
cursor: pointer;
color: var(--ink);
}
.bill-opt:hover {
border-color: var(--terracotta);
}
.bill-opt.is-picked {
background: var(--forest);
color: var(--bone);
border-color: var(--forest-d);
}
.bill-opt .m {
font-family: var(--font-mono);
font-weight: 700;
color: var(--terracotta-d);
}
.bill-opt.is-picked .m {
color: var(--gold);
}
.tip-row {
display: flex;
align-items: center;
gap: 6px;
}
.tip-label {
font-size: 0.78rem;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--ink-2);
font-weight: 700;
margin-right: 8px;
}
.tip-btn {
background: var(--cream);
border: 1px solid rgba(44, 26, 14, 0.12);
border-radius: 999px;
padding: 7px 13px;
font-family: inherit;
font-size: 0.82rem;
font-weight: 700;
color: var(--ink-2);
cursor: pointer;
}
.tip-btn.is-active {
background: var(--forest);
color: var(--bone);
border-color: var(--forest-d);
}
.bill-confirm {
background: var(--gold);
color: var(--ink);
display: inline-flex;
justify-content: center;
gap: 8px;
padding: 14px 16px;
}
.bill-confirm:hover {
background: var(--gold-light);
}
.ghost-link {
background: transparent;
border: none;
font-family: inherit;
font-size: 0.84rem;
color: var(--warm-gray);
cursor: pointer;
text-decoration: underline;
}const TAX_RATE = 0.0825;
const CATS = {
apps: {
title: "Para empezar",
icon: "🥖",
items: [
{ id: "pan", name: "Pan masa madre", desc: "Sourdough, smoked oil", price: 8, glyph: "🥖" },
{
id: "burrata",
name: "Burrata huerta",
desc: "Heirloom tomato, basil",
price: 16,
glyph: "🍅",
},
{ id: "pulpo", name: "Pulpo brasa", desc: "Smoked paprika potato", price: 19, glyph: "🐙" },
{ id: "croq", name: "Croquetas jamón", desc: "Six pieces", price: 14, glyph: "🥟" },
],
},
mains: {
title: "Principales",
icon: "🥩",
items: [
{
id: "ribeye",
name: "Ribeye 14oz",
desc: "Marrow butter, chimichurri",
price: 48,
glyph: "🥩",
},
{ id: "branzino", name: "Branzino", desc: "Fennel, preserved lemon", price: 38, glyph: "🐟" },
{
id: "risotto",
name: "Risotto hongos",
desc: "Wild mushroom, parmesan",
price: 26,
glyph: "🍚",
},
{ id: "pollo", name: "Pollo carbón", desc: "Charred lemon, garlic", price: 28, glyph: "🍗" },
{
id: "pappardelle",
name: "Pappardelle ragú",
desc: "Lamb shoulder, gremolata",
price: 24,
glyph: "🍝",
},
{
id: "huerto",
name: "Plato del huerto",
desc: "Seasonal vegetables",
price: 22,
glyph: "🥗",
},
],
},
drinks: {
title: "Bebidas",
icon: "🍷",
items: [
{ id: "vermut", name: "Vermut casa", desc: "On tap", price: 9, glyph: "🥃" },
{
id: "negroni",
name: "Negroni sbagliato",
desc: "Campari, prosecco",
price: 14,
glyph: "🍸",
},
{
id: "tinto",
name: "Tinto natural",
desc: "Glass, ask your server",
price: 12,
glyph: "🍷",
},
{ id: "agua", name: "Agua mineral", desc: "Still or sparkling", price: 5, glyph: "💧" },
{ id: "cafe", name: "Espresso", desc: "Single shot", price: 4, glyph: "☕" },
],
},
dessert: {
title: "Postres",
icon: "🍰",
items: [
{
id: "tarta",
name: "Tarta de queso",
desc: "Basque burnt, salted caramel",
price: 11,
glyph: "🍰",
},
{
id: "olive",
name: "Olive oil cake",
desc: "Crème fraîche, citrus",
price: 10,
glyph: "🍋",
},
{
id: "ganache",
name: "Chocolate ganache",
desc: "Hazelnut praline",
price: 12,
glyph: "🍫",
},
{ id: "sorbete", name: "Sorbete cítrico", desc: "Citrus, mint", price: 9, glyph: "🍧" },
],
},
extras: {
title: "Extras",
icon: "🌿",
items: [
{ id: "fries", name: "Patatas truffle", desc: "Parmesan, parsley", price: 8, glyph: "🍟" },
{
id: "verduras",
name: "Verduras a la brasa",
desc: "Charred, lemon oil",
price: 9,
glyph: "🥦",
},
{ id: "pan-extra", name: "Pan extra", desc: "Plus butter", price: 4, glyph: "🥯" },
],
},
};
const CAT_ORDER = ["apps", "mains", "drinks", "dessert", "extras"];
let activeCat = "mains";
let ticket = []; // { id, name, price, qty, glyph }
const catBar = document.getElementById("catBar");
const menuGrid = document.getElementById("menuGrid");
const catTitle = document.getElementById("catTitle");
const ticketLines = document.getElementById("ticketLines");
const ticketEmpty = document.getElementById("ticketEmpty");
const ticketMeta = document.getElementById("ticketMeta");
const subtotalEl = document.getElementById("subtotal");
const taxEl = document.getElementById("tax");
const totalEl = document.getElementById("total");
const sendBtn = document.getElementById("sendBtn");
const callBtn = document.getElementById("callBtn");
const billBtn = document.getElementById("billBtn");
const toast = document.getElementById("toast");
const billOverlay = document.getElementById("billOverlay");
const billCancel = document.getElementById("billCancel");
const billConfirm = document.getElementById("billConfirm");
const billTotal = document.getElementById("billTotal");
const splitEven = document.getElementById("splitEven");
const splitOne = document.getElementById("splitOne");
function money(v) {
return `$${v.toFixed(2)}`;
}
function renderCatBar() {
catBar.innerHTML = CAT_ORDER.map(
(id) => `<button class="cat-chip ${id === activeCat ? "is-active" : ""}" data-cat="${id}">
<span class="cat-chip-icon">${CATS[id].icon}</span><span>${CATS[id].title}</span>
</button>`
).join("");
}
function renderMenu() {
const cat = CATS[activeCat];
catTitle.textContent = cat.title;
menuGrid.innerHTML = cat.items
.map(
(
i
) => `<button class="tile" data-id="${i.id}" data-name="${i.name}" data-price="${i.price}" data-glyph="${i.glyph}">
<span class="tile-glyph">${i.glyph}</span>
<span class="tile-name">${i.name}</span>
<span class="tile-desc">${i.desc}</span>
<span class="tile-price">${money(i.price)}</span>
</button>`
)
.join("");
}
function renderTicket() {
const count = ticket.reduce((n, l) => n + l.qty, 0);
ticketMeta.textContent = count === 0 ? "0 items" : `${count} ${count === 1 ? "item" : "items"}`;
ticketEmpty.hidden = ticket.length > 0;
ticketLines.innerHTML = ticket
.map(
(l) => `<li data-id="${l.id}">
<div class="l-name">${l.name}</div>
<div class="l-qty">
<button data-action="dec">−</button>
<span>${l.qty}</span>
<button data-action="inc">+</button>
</div>
<span class="l-price">${money(l.price * l.qty)}</span>
</li>`
)
.join("");
const subtotal = ticket.reduce((s, l) => s + l.price * l.qty, 0);
const tax = subtotal * TAX_RATE;
const total = subtotal + tax;
subtotalEl.textContent = money(subtotal);
taxEl.textContent = money(tax);
totalEl.textContent = money(total);
sendBtn.disabled = ticket.length === 0;
billTotal.textContent = money(total * 1.18);
splitEven.textContent = `${money((total * 1.18) / 2)} each`;
splitOne.textContent = money(total * 1.18);
}
function addItem(id, name, price, glyph, tile) {
const existing = ticket.find((l) => l.id === id);
if (existing) existing.qty += 1;
else ticket.push({ id, name, price: Number(price), glyph, qty: 1 });
renderTicket();
if (tile) {
tile.classList.remove("is-flash");
void tile.offsetWidth;
tile.classList.add("is-flash");
}
}
function showToast(msg) {
toast.textContent = msg;
toast.hidden = false;
clearTimeout(showToast._t);
showToast._t = setTimeout(() => (toast.hidden = true), 2200);
}
catBar.addEventListener("click", (e) => {
const btn = e.target.closest("[data-cat]");
if (!btn) return;
activeCat = btn.dataset.cat;
renderCatBar();
renderMenu();
});
menuGrid.addEventListener("click", (e) => {
const tile = e.target.closest("[data-id]");
if (!tile) return;
addItem(tile.dataset.id, tile.dataset.name, tile.dataset.price, tile.dataset.glyph, tile);
});
ticketLines.addEventListener("click", (e) => {
const btn = e.target.closest("[data-action]");
if (!btn) return;
const li = btn.closest("[data-id]");
if (!li) return;
const line = ticket.find((l) => l.id === li.dataset.id);
if (!line) return;
if (btn.dataset.action === "inc") line.qty += 1;
if (btn.dataset.action === "dec") {
line.qty -= 1;
if (line.qty <= 0) ticket = ticket.filter((l) => l.id !== line.id);
}
renderTicket();
});
sendBtn.addEventListener("click", () => {
if (ticket.length === 0) return;
const items = ticket.reduce((n, l) => n + l.qty, 0);
showToast(`Sent ${items} item${items > 1 ? "s" : ""} to the kitchen.`);
// Visually mark mains complete after second send for the demo
document.querySelector(".progress .is-current")?.classList.remove("is-current");
});
callBtn.addEventListener("click", () => {
callBtn.classList.add("is-called");
showToast("We let your server know — they're on their way.");
setTimeout(() => callBtn.classList.remove("is-called"), 4000);
});
billBtn.addEventListener("click", () => {
billOverlay.hidden = false;
});
billCancel.addEventListener("click", () => {
billOverlay.hidden = true;
});
billOverlay.addEventListener("click", (e) => {
if (e.target === billOverlay) billOverlay.hidden = true;
});
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") billOverlay.hidden = true;
});
document.querySelectorAll(".bill-opt").forEach((b) =>
b.addEventListener("click", () => {
document.querySelectorAll(".bill-opt").forEach((x) => x.classList.remove("is-picked"));
b.classList.add("is-picked");
})
);
document.querySelectorAll(".tip-btn").forEach((b) =>
b.addEventListener("click", () => {
document.querySelectorAll(".tip-btn").forEach((x) => x.classList.remove("is-active"));
b.classList.add("is-active");
const tip = Number(b.dataset.tip) / 100;
const subtotal = ticket.reduce((s, l) => s + l.price * l.qty, 0);
const tax = subtotal * TAX_RATE;
const total = subtotal + tax + subtotal * tip;
billTotal.textContent = money(total);
splitEven.textContent = `${money(total / 2)} each`;
splitOne.textContent = money(total);
})
);
billConfirm.addEventListener("click", () => {
billOverlay.hidden = true;
showToast("Your server is bringing the bill.");
});
// Seed an in-progress order so the ticket has something
ticket = [
{ id: "burrata", name: "Burrata huerta", price: 16, qty: 1, glyph: "🍅" },
{ id: "pulpo", name: "Pulpo brasa", price: 19, qty: 1, glyph: "🐙" },
{ id: "ribeye", name: "Ribeye 14oz", price: 48, qty: 1, glyph: "🥩" },
{ id: "tinto", name: "Tinto natural", price: 12, qty: 2, glyph: "🍷" },
];
renderCatBar();
renderMenu();
renderTicket();<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@600;700&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@500;700&display=swap"
/>
<link rel="stylesheet" href="style.css" />
<title>Tableside · Table 7</title>
</head>
<body>
<div class="tablet">
<div class="screen">
<header class="top">
<div class="brand">
<span class="brand-name">Casa Olivar</span>
<span class="brand-tag">Tableside</span>
</div>
<div class="table-info">
<p class="ti-table">Table 7 · 2 guests</p>
<p class="ti-server">Server · Lina · seated 19:42</p>
</div>
<div class="progress" aria-label="Meal progress">
<ol>
<li class="is-done"><span class="pill">1</span><span>Apps</span></li>
<li class="is-current"><span class="pill">2</span><span>Mains</span></li>
<li><span class="pill">3</span><span>Dessert</span></li>
<li><span class="pill">4</span><span>Paid</span></li>
</ol>
</div>
</header>
<nav class="catbar" id="catBar"></nav>
<main class="board">
<section class="menu">
<header class="menu-head">
<h1 id="catTitle">—</h1>
<p class="menu-hint">Tap to add to your ticket</p>
</header>
<div class="grid" id="menuGrid"></div>
</section>
<aside class="ticket">
<header class="ticket-head">
<p class="ticket-eyebrow">Your ticket</p>
<p class="ticket-meta" id="ticketMeta">— items</p>
</header>
<ul class="lines" id="ticketLines"></ul>
<p class="empty" id="ticketEmpty">Tap something on the left to add it.</p>
<dl class="totals">
<div><dt>Subtotal</dt><dd id="subtotal">$0.00</dd></div>
<div><dt>Tax</dt><dd id="tax">$0.00</dd></div>
<div class="big"><dt>Total</dt><dd id="total">$0.00</dd></div>
</dl>
<div class="ticket-actions">
<button class="ghost" type="button" id="callBtn">
<span class="dot"></span> Call server
</button>
<button class="primary" type="button" id="sendBtn" disabled>
Send to kitchen
</button>
</div>
<button class="bill-btn" type="button" id="billBtn">
Ready for the bill →
</button>
</aside>
</main>
<!-- Toast -->
<div class="toast" id="toast" hidden role="status" aria-live="polite"></div>
<!-- Bill overlay -->
<div class="bill-overlay" id="billOverlay" hidden>
<div class="bill-card">
<header>
<p class="kicker">Time for the bill?</p>
<h2>How would you like to split it?</h2>
</header>
<div class="bill-options">
<button class="bill-opt" type="button" data-split="even">
Split evenly
<span class="m" id="splitEven">—</span>
</button>
<button class="bill-opt" type="button" data-split="one">
One card for the table
<span class="m" id="splitOne">—</span>
</button>
<button class="bill-opt" type="button" data-split="own">
Each pays own
</button>
</div>
<div class="tip-row">
<span class="tip-label">Tip</span>
<button class="tip-btn" data-tip="15">15%</button>
<button class="tip-btn is-active" data-tip="18">18%</button>
<button class="tip-btn" data-tip="20">20%</button>
<button class="tip-btn" data-tip="25">25%</button>
</div>
<button class="primary bill-confirm" type="button" id="billConfirm">
<span>Bring the bill ·</span><span id="billTotal">$0.00</span>
</button>
<button class="ghost-link" type="button" id="billCancel">Not yet — keep ordering</button>
</div>
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>Tableside Tablet
The tablet diners use mid-meal at the table — landscape, simpler than the POS, customer-readable copy. Header shows the table number, party size, server, and “how it’s going” progress (apps · mains · dessert · paid). Category strip across the top, menu grid in the centre, live ticket on the right with a call-server pill, request-the-bill button, and a tip card.