Pages Hard
Self-Ordering Kiosk
Customer self-ordering kiosk — vertical layout, 72px touch targets, language switcher, animated welcome → category → item-detail → cart → pay flow.
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%;
}
body {
font-family: var(--font-body);
background: linear-gradient(180deg, var(--ink) 0%, #1a0f06 100%);
color: var(--ink);
-webkit-font-smoothing: antialiased;
user-select: none;
display: grid;
place-items: center;
padding: 24px;
}
.kiosk {
position: relative;
width: 100%;
max-width: 720px;
height: min(96vh, 1080px);
background: var(--ink);
border-radius: 36px;
padding: 18px;
box-shadow: 0 30px 80px rgba(0, 0, 0, 0.6), inset 0 0 0 1px rgba(250, 247, 241, 0.04);
}
.frame {
position: relative;
height: 100%;
background: var(--cream);
border-radius: 26px;
overflow: hidden;
display: flex;
flex-direction: column;
}
/* Topbar */
.bar {
height: 64px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 22px;
background: var(--bone);
border-bottom: 1px solid rgba(44, 26, 14, 0.08);
position: relative;
z-index: 5;
}
.bar-left {
display: flex;
align-items: baseline;
gap: 14px;
}
.brand {
font-family: var(--font-display);
font-weight: 800;
font-size: 1.15rem;
}
.bar-step {
font-size: 0.72rem;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--terracotta);
font-weight: 700;
padding-left: 14px;
border-left: 1px solid rgba(44, 26, 14, 0.16);
}
.bar-right {
display: flex;
align-items: center;
gap: 8px;
}
.lang {
width: 44px;
height: 36px;
border: 1px solid rgba(44, 26, 14, 0.16);
background: transparent;
color: var(--ink-2);
font-family: inherit;
font-size: 0.78rem;
font-weight: 700;
letter-spacing: 0.08em;
border-radius: 8px;
cursor: pointer;
}
.lang.is-active {
background: var(--forest);
color: var(--bone);
border-color: var(--forest-d);
}
.cart-btn {
position: relative;
width: 56px;
height: 48px;
background: var(--forest);
color: var(--bone);
border: none;
border-radius: 12px;
font-family: inherit;
font-size: 1.2rem;
cursor: pointer;
display: grid;
place-items: center;
}
.cart-icon {
font-size: 1.4rem;
}
.cart-count {
position: absolute;
top: -8px;
right: -8px;
background: var(--gold);
color: var(--ink);
font-family: var(--font-mono);
font-size: 0.78rem;
font-weight: 700;
border-radius: 999px;
min-width: 24px;
height: 24px;
display: grid;
place-items: center;
border: 2px solid var(--bone);
padding: 0 6px;
}
/* Screens */
.screen {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
animation: fade 0.25s ease;
}
.screen[hidden] {
display: none !important;
}
@keyframes fade {
from {
opacity: 0;
transform: translateY(6px);
}
to {
opacity: 1;
transform: none;
}
}
/* Welcome */
.screen-welcome {
text-align: center;
padding: 32px 36px 32px;
justify-content: center;
gap: 16px;
}
.welcome-art {
position: relative;
height: 200px;
display: grid;
place-items: center;
}
.welcome-art .glyph {
position: absolute;
font-size: 5rem;
filter: drop-shadow(0 12px 28px rgba(44, 26, 14, 0.25));
animation: floatY 5s ease-in-out infinite;
}
.welcome-art .g-1 {
left: 18%;
top: 30px;
animation-delay: 0s;
}
.welcome-art .g-2 {
left: 50%;
top: 0;
transform: translateX(-50%);
animation-delay: 1.2s;
}
.welcome-art .g-3 {
right: 18%;
top: 40px;
animation-delay: 2.2s;
}
@keyframes floatY {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-14px);
}
}
.welcome-art .g-2 {
/* keep horizontal centre */
}
@keyframes floatYCentre {
0%,
100% {
transform: translate(-50%, 0);
}
50% {
transform: translate(-50%, -14px);
}
}
.welcome-art .g-2 {
animation-name: floatYCentre;
}
.welcome-kicker {
font-size: 0.78rem;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--terracotta);
font-weight: 700;
}
.screen-welcome h1 {
font-family: var(--font-display);
font-weight: 800;
font-size: clamp(2.4rem, 7vw, 3.6rem);
line-height: 1.05;
letter-spacing: -0.015em;
}
.screen-welcome h1 .em {
font-style: italic;
color: var(--terracotta);
}
.welcome-sub {
font-size: 1.05rem;
color: var(--warm-gray);
margin-top: -4px;
}
.tap-cta {
margin: 22px auto 0;
display: inline-flex;
align-items: center;
justify-content: space-between;
gap: 16px;
min-height: 76px;
min-width: 320px;
background: var(--forest);
color: var(--bone);
border: none;
border-radius: 999px;
padding: 0 28px;
font-family: inherit;
font-size: 1.15rem;
font-weight: 700;
cursor: pointer;
letter-spacing: 0.04em;
transition: background 0.15s, transform 0.05s;
box-shadow: 0 12px 30px rgba(45, 74, 62, 0.35);
}
.tap-cta:hover {
background: var(--forest-d);
}
.tap-cta:active {
transform: scale(0.98);
}
.tap-arrow {
font-family: var(--font-mono);
font-size: 1.5rem;
background: rgba(250, 247, 241, 0.18);
width: 44px;
height: 44px;
display: grid;
place-items: center;
border-radius: 999px;
}
.quiet-cta {
background: transparent;
border: 1px solid rgba(44, 26, 14, 0.18);
color: var(--ink-2);
font-family: inherit;
font-size: 0.94rem;
font-weight: 600;
border-radius: 999px;
padding: 12px 22px;
cursor: pointer;
min-height: 48px;
margin-top: 4px;
}
.quiet-cta:hover {
background: var(--cream-2);
color: var(--ink);
}
/* Screen head shared */
.screen-head {
padding: 20px 28px 12px;
position: relative;
border-bottom: 1px solid rgba(44, 26, 14, 0.08);
}
.screen-head h2 {
font-family: var(--font-display);
font-weight: 800;
font-size: clamp(1.8rem, 5vw, 2.4rem);
letter-spacing: -0.015em;
}
.screen-head p {
margin-top: 4px;
font-size: 0.96rem;
color: var(--warm-gray);
}
.back {
position: absolute;
top: 18px;
left: 18px;
background: var(--cream-2);
border: none;
font-family: inherit;
font-size: 0.86rem;
font-weight: 700;
color: var(--ink-2);
padding: 8px 14px;
border-radius: 999px;
cursor: pointer;
}
.back:hover {
background: var(--cream);
}
.screen-head:has(.back) h2 {
margin-top: 30px;
}
/* Categories */
.cat-grid {
flex: 1;
overflow-y: auto;
padding: 20px 28px 12px;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 14px;
align-content: start;
}
.cat-tile {
background: var(--bone);
border: 2px solid rgba(44, 26, 14, 0.08);
border-radius: 18px;
min-height: 132px;
padding: 20px 22px;
text-align: left;
cursor: pointer;
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 8px;
font-family: inherit;
color: var(--ink);
transition: border-color 0.15s, background 0.15s, transform 0.05s;
}
.cat-tile:hover {
border-color: var(--terracotta);
}
.cat-tile:active {
transform: scale(0.98);
}
.cat-tile.is-picked {
background: var(--forest);
color: var(--bone);
border-color: var(--forest-d);
}
.cat-tile-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.cat-glyph {
font-size: 2.2rem;
}
.cat-pick-count {
font-family: var(--font-mono);
font-size: 0.78rem;
font-weight: 700;
background: var(--gold);
color: var(--ink);
border-radius: 999px;
min-width: 28px;
height: 28px;
display: grid;
place-items: center;
padding: 0 9px;
}
.cat-name {
font-family: var(--font-display);
font-weight: 800;
font-size: 1.3rem;
letter-spacing: -0.005em;
}
.cat-desc {
font-size: 0.84rem;
color: var(--warm-gray);
}
.cat-tile.is-picked .cat-desc {
color: var(--gold-light);
}
/* Items */
.item-grid {
flex: 1;
overflow-y: auto;
padding: 18px 24px 12px;
display: flex;
flex-direction: column;
gap: 12px;
}
.item-row {
display: grid;
grid-template-columns: 64px 1fr auto;
gap: 16px;
align-items: center;
background: var(--bone);
border: 1px solid rgba(44, 26, 14, 0.08);
border-radius: 16px;
padding: 14px 18px;
}
.item-art {
width: 64px;
height: 64px;
border-radius: 12px;
background: var(--cream-2);
display: grid;
place-items: center;
font-size: 1.8rem;
}
.item-body {
min-width: 0;
}
.item-name {
font-family: var(--font-display);
font-weight: 700;
font-size: 1.15rem;
line-height: 1.2;
}
.item-desc {
font-size: 0.82rem;
color: var(--warm-gray);
margin-top: 2px;
}
.item-price {
font-family: var(--font-mono);
font-weight: 700;
color: var(--terracotta-d);
margin-top: 4px;
font-size: 0.95rem;
}
.item-qty {
display: flex;
align-items: center;
background: var(--cream-2);
border-radius: 999px;
gap: 4px;
padding: 4px;
}
.item-qty button {
width: 44px;
height: 44px;
border: none;
background: var(--bone);
border-radius: 999px;
font-family: inherit;
font-size: 1.4rem;
font-weight: 700;
cursor: pointer;
display: grid;
place-items: center;
}
.item-qty button.add {
background: var(--forest);
color: var(--bone);
}
.item-qty button.add:hover {
background: var(--forest-d);
}
.item-qty button:disabled {
opacity: 0.35;
cursor: not-allowed;
}
.item-qty span {
min-width: 28px;
text-align: center;
font-weight: 700;
font-family: var(--font-mono);
font-size: 1.05rem;
}
/* Cart */
.cart-lines {
flex: 1;
overflow-y: auto;
padding: 16px 24px;
list-style: none;
display: flex;
flex-direction: column;
gap: 8px;
}
.cart-empty {
flex: 1;
display: grid;
place-items: center;
color: var(--warm-gray);
font-style: italic;
padding: 24px;
text-align: center;
}
.cart-row {
display: grid;
grid-template-columns: 40px 1fr auto auto;
gap: 14px;
align-items: center;
padding: 12px 16px;
border: 1px solid rgba(44, 26, 14, 0.08);
border-radius: 12px;
background: var(--bone);
}
.cart-row .item-art {
width: 40px;
height: 40px;
font-size: 1.3rem;
border-radius: 8px;
}
.cart-name {
font-weight: 700;
font-size: 0.98rem;
}
.cart-qty {
display: flex;
align-items: center;
background: var(--cream-2);
border-radius: 999px;
padding: 3px;
gap: 2px;
}
.cart-qty button {
width: 34px;
height: 34px;
border-radius: 999px;
border: none;
background: var(--bone);
font-family: inherit;
font-size: 1.15rem;
font-weight: 700;
cursor: pointer;
display: grid;
place-items: center;
}
.cart-qty span {
min-width: 22px;
text-align: center;
font-family: var(--font-mono);
font-weight: 700;
}
.cart-row-price {
font-family: var(--font-mono);
font-weight: 700;
font-size: 0.98rem;
color: var(--terracotta-d);
min-width: 64px;
text-align: right;
}
.cart-totals {
padding: 12px 24px 4px;
border-top: 1px dashed rgba(44, 26, 14, 0.18);
display: flex;
flex-direction: column;
gap: 4px;
}
.cart-totals div {
display: flex;
justify-content: space-between;
font-size: 0.94rem;
color: var(--ink-2);
}
.cart-totals dd {
font-family: var(--font-mono);
font-weight: 700;
}
.cart-totals .big {
margin-top: 4px;
padding-top: 8px;
border-top: 1px solid rgba(44, 26, 14, 0.16);
font-size: 1.1rem;
font-weight: 700;
color: var(--ink);
}
.cart-totals .big dd {
font-size: 1.3rem;
color: var(--ink);
}
/* Pay */
.pay-options {
flex: 1;
overflow-y: auto;
padding: 22px 24px 12px;
display: flex;
flex-direction: column;
gap: 14px;
}
.pay-card {
position: relative;
background: var(--bone);
border: 2px solid rgba(44, 26, 14, 0.1);
border-radius: 18px;
padding: 22px 24px;
text-align: left;
cursor: pointer;
display: grid;
grid-template-columns: 64px 1fr;
gap: 16px;
font-family: inherit;
color: var(--ink);
transition: border-color 0.15s, background 0.15s, transform 0.05s;
}
.pay-card:hover {
border-color: var(--forest);
}
.pay-card:active {
transform: scale(0.99);
}
.pay-icon {
width: 64px;
height: 64px;
background: var(--cream-2);
border-radius: 16px;
display: grid;
place-items: center;
font-size: 2rem;
}
.pay-card h3 {
font-family: var(--font-display);
font-weight: 800;
font-size: 1.4rem;
}
.pay-card p {
color: var(--warm-gray);
font-size: 0.92rem;
margin-top: 2px;
}
.pay-tag {
position: absolute;
top: 16px;
right: 18px;
background: var(--gold);
color: var(--ink);
font-size: 0.66rem;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
padding: 4px 10px;
border-radius: 999px;
}
/* Footer (per screen) */
.screen-foot {
padding: 16px 24px 22px;
border-top: 1px solid rgba(44, 26, 14, 0.08);
background: var(--bone);
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
}
.ghost-cta,
.primary-cta {
min-height: 64px;
border-radius: 999px;
padding: 0 26px;
font-family: inherit;
font-size: 1rem;
font-weight: 700;
cursor: pointer;
border: 1px solid transparent;
transition: background 0.15s, color 0.15s, border-color 0.15s;
}
.ghost-cta {
background: transparent;
border-color: rgba(44, 26, 14, 0.2);
color: var(--ink-2);
}
.ghost-cta:hover {
background: var(--cream-2);
color: var(--ink);
}
.primary-cta {
background: var(--forest);
color: var(--bone);
flex: 1;
}
.primary-cta:hover:not(:disabled) {
background: var(--forest-d);
}
.primary-cta:disabled {
background: var(--warm-gray);
cursor: not-allowed;
opacity: 0.7;
}
.pay-total {
font-family: var(--font-display);
font-weight: 800;
font-size: 1.5rem;
color: var(--ink);
}
/* Reveal */
.screen-reveal {
text-align: center;
padding: 40px 32px;
justify-content: center;
gap: 16px;
}
.reveal-eyebrow {
font-size: 0.78rem;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--terracotta);
font-weight: 700;
}
.reveal-no {
font-family: var(--font-display);
font-weight: 800;
font-size: clamp(4rem, 14vw, 7rem);
color: var(--forest);
line-height: 1;
letter-spacing: -0.02em;
}
.screen-reveal h2 {
font-family: var(--font-display);
font-weight: 800;
font-size: clamp(1.8rem, 5vw, 2.4rem);
letter-spacing: -0.015em;
margin-top: -8px;
}
.reveal-sub {
font-size: 1rem;
color: var(--warm-gray);
max-width: 480px;
margin: 0 auto;
}
.reveal-meta {
background: var(--bone);
border: 1px solid rgba(44, 26, 14, 0.08);
border-radius: 16px;
padding: 20px 26px;
max-width: 460px;
margin: 12px auto 0;
}
.reveal-meta dl {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px 18px;
}
.reveal-meta dt {
font-size: 0.68rem;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--warm-gray);
font-weight: 700;
}
.reveal-meta dd {
font-family: var(--font-mono);
font-weight: 700;
font-size: 1.05rem;
color: var(--ink);
margin-top: 2px;
}const TAX_RATE = 0.0825;
const CATS = [
{ id: "bowls", name: "Grain bowls", glyph: "🥗", desc: "Build your own · 6 bases" },
{ id: "tacos", name: "Tacos", glyph: "🌮", desc: "3 per order · 8 fillings" },
{ id: "sandwiches", name: "Sandwiches", glyph: "🥖", desc: "Sourdough, ciabatta, focaccia" },
{ id: "sides", name: "Sides", glyph: "🥔", desc: "Fries, salads, soups" },
{ id: "drinks", name: "Drinks", glyph: "🍹", desc: "Aguas frescas, juice, soda" },
{ id: "sweets", name: "Sweets", glyph: "🍰", desc: "Cake, ice, cookies" },
];
const ITEMS = {
bowls: [
{
id: "bowl-1",
name: "Pollo asado bowl",
desc: "Rice, beans, salsa verde, avocado.",
price: 14,
glyph: "🥗",
},
{
id: "bowl-2",
name: "Carnitas bowl",
desc: "Slow-cooked pork, pickled onion, lime.",
price: 15,
glyph: "🥑",
},
{
id: "bowl-3",
name: "Roasted veg bowl",
desc: "Seasonal vegetables, romesco, almonds.",
price: 13,
glyph: "🥕",
},
{
id: "bowl-4",
name: "Chickpea bowl",
desc: "Spiced chickpea, herb yoghurt, sumac.",
price: 12,
glyph: "🫘",
},
],
tacos: [
{
id: "taco-1",
name: "Al pastor · 3 pcs",
desc: "Achiote pork, pineapple, cilantro.",
price: 12,
glyph: "🌮",
},
{
id: "taco-2",
name: "Baja fish · 3 pcs",
desc: "Crispy cod, chipotle slaw, lime.",
price: 14,
glyph: "🐟",
},
{
id: "taco-3",
name: "Carnitas · 3 pcs",
desc: "Slow pork, salsa verde, queso fresco.",
price: 13,
glyph: "🐖",
},
{
id: "taco-4",
name: "Hongos · 3 pcs",
desc: "Wild mushroom, garlic, jalapeño.",
price: 12,
glyph: "🍄",
},
],
sandwiches: [
{
id: "s-1",
name: "Roasted veg ciabatta",
desc: "Aubergine, pepper, pesto.",
price: 11,
glyph: "🥪",
},
{
id: "s-2",
name: "Pulled pork bun",
desc: "Slow shoulder, pickled onion.",
price: 13,
glyph: "🍔",
},
{
id: "s-3",
name: "Chicken focaccia",
desc: "Aioli, rocket, sun tomato.",
price: 12,
glyph: "🥖",
},
],
sides: [
{ id: "side-1", name: "Truffle fries", desc: "Parmesan, parsley.", price: 8, glyph: "🍟" },
{ id: "side-2", name: "Charred broccoli", desc: "Lemon, chilli oil.", price: 7, glyph: "🥦" },
{ id: "side-3", name: "Tomato soup", desc: "Basil, crème fraîche.", price: 7, glyph: "🍅" },
{
id: "side-4",
name: "House salad",
desc: "Leaves, herbs, vinaigrette.",
price: 8,
glyph: "🥬",
},
],
drinks: [
{ id: "d-1", name: "Hibiscus agua fresca", desc: "Sweetened lightly.", price: 5, glyph: "🌺" },
{ id: "d-2", name: "Tamarind agua fresca", desc: "Salt-rim available.", price: 5, glyph: "🥤" },
{ id: "d-3", name: "Sparkling water", desc: "Local, on tap.", price: 3, glyph: "💧" },
{
id: "d-4",
name: "Cold brew coffee",
desc: "From Tuesday-fresh beans.",
price: 4,
glyph: "☕",
},
],
sweets: [
{ id: "sw-1", name: "Tres leches cake", desc: "Cinnamon dust.", price: 7, glyph: "🍰" },
{
id: "sw-2",
name: "Sorbete cítrico",
desc: "Citrus, mint, today's batch.",
price: 6,
glyph: "🍧",
},
{ id: "sw-3", name: "Olive oil cookie", desc: "Sea salt, two pieces.", price: 4, glyph: "🍪" },
],
};
const STEP_LABELS = {
welcome: "Welcome",
categories: "Step 1 of 3 · Categories",
items: "Step 2 of 3 · Choose dishes",
cart: "Step 3 of 3 · Review",
pay: "Pay",
reveal: "Order placed",
};
let currentScreen = "welcome";
let currentCat = null;
let cart = []; // { id, name, price, qty, glyph }
const stepLabel = document.getElementById("stepLabel");
const catGrid = document.getElementById("catGrid");
const itemGrid = document.getElementById("itemGrid");
const catTitle = document.getElementById("catTitle");
const catSub = document.getElementById("catSub");
const cartLines = document.getElementById("cartLines");
const cartEmpty = document.getElementById("cartEmpty");
const cartCountSub = document.getElementById("cartCountSub");
const cartCount = document.getElementById("cartCount");
const cartBtn = document.getElementById("cartBtn");
const subtotalEl = document.getElementById("subtotal");
const taxEl = document.getElementById("tax");
const totalEl = document.getElementById("total");
const payTotalEl = document.getElementById("payTotal");
const goPayBtn = document.getElementById("goPay");
const goCart = document.getElementById("goCart");
const goCart2 = document.getElementById("goCart2");
const clearBtn = document.getElementById("clear");
function money(v) {
return `$${v.toFixed(2)}`;
}
function go(screen) {
if (screen === currentScreen) return;
currentScreen = screen;
document.querySelectorAll("[data-screen]").forEach((el) => {
el.hidden = el.dataset.screen !== screen;
});
stepLabel.textContent = STEP_LABELS[screen] || screen;
// Cart icon only visible after at least one item is added.
cartBtn.hidden = cart.length === 0 || screen === "welcome" || screen === "reveal";
if (screen === "categories") renderCats();
if (screen === "items") renderItems();
if (screen === "cart") renderCart();
}
function totals() {
const subtotal = cart.reduce((s, l) => s + l.price * l.qty, 0);
const tax = subtotal * TAX_RATE;
return { subtotal, tax, total: subtotal + tax };
}
function renderCats() {
catGrid.innerHTML = CATS.map((c) => {
const picks = cart
.filter((line) => ITEMS[c.id]?.some((i) => i.id === line.id))
.reduce((n, l) => n + l.qty, 0);
return `<button class="cat-tile ${picks > 0 ? "is-picked" : ""}" data-cat="${c.id}">
<div class="cat-tile-row">
<span class="cat-glyph">${c.glyph}</span>
${picks > 0 ? `<span class="cat-pick-count">${picks}</span>` : ""}
</div>
<div>
<p class="cat-name">${c.name}</p>
<p class="cat-desc">${c.desc}</p>
</div>
</button>`;
}).join("");
const count = cart.reduce((n, l) => n + l.qty, 0);
goCart.disabled = count === 0;
goCart.textContent = count === 0 ? "Pick a dish to continue" : `Review order → ${count}`;
}
function renderItems() {
const cat = CATS.find((c) => c.id === currentCat);
if (!cat) return;
catTitle.textContent = cat.name;
catSub.textContent = cat.desc;
itemGrid.innerHTML = ITEMS[currentCat]
.map((i) => {
const line = cart.find((l) => l.id === i.id);
const qty = line ? line.qty : 0;
return `<article class="item-row">
<div class="item-art">${i.glyph}</div>
<div class="item-body">
<p class="item-name">${i.name}</p>
<p class="item-desc">${i.desc}</p>
<p class="item-price">${money(i.price)}</p>
</div>
<div class="item-qty" data-id="${i.id}" data-name="${i.name}" data-price="${i.price}" data-glyph="${i.glyph}">
<button data-action="dec" aria-label="One fewer" ${qty === 0 ? "disabled" : ""}>−</button>
<span>${qty}</span>
<button class="add" data-action="inc" aria-label="One more">+</button>
</div>
</article>`;
})
.join("");
}
function renderCart() {
const count = cart.reduce((n, l) => n + l.qty, 0);
cartCountSub.textContent = count === 0 ? "0 items" : `${count} ${count === 1 ? "item" : "items"}`;
cartCount.textContent = count;
cartEmpty.hidden = cart.length > 0;
cartLines.innerHTML = cart
.map(
(l) => `<li class="cart-row" data-id="${l.id}">
<div class="item-art">${l.glyph}</div>
<div>
<p class="cart-name">${l.name}</p>
</div>
<div class="cart-qty">
<button data-action="dec">−</button>
<span>${l.qty}</span>
<button data-action="inc">+</button>
</div>
<span class="cart-row-price">${money(l.price * l.qty)}</span>
</li>`
)
.join("");
const t = totals();
subtotalEl.textContent = money(t.subtotal);
taxEl.textContent = money(t.tax);
totalEl.textContent = money(t.total);
payTotalEl.textContent = money(t.total);
goPayBtn.disabled = cart.length === 0;
}
function changeQty(id, delta, src) {
const existing = cart.find((l) => l.id === id);
if (delta > 0) {
if (existing) existing.qty += 1;
else if (src)
cart.push({
id,
name: src.name,
price: Number(src.price),
glyph: src.glyph,
qty: 1,
});
} else if (existing) {
existing.qty -= 1;
if (existing.qty <= 0) cart = cart.filter((l) => l.id !== id);
}
// Reflect cart count in topbar
const count = cart.reduce((n, l) => n + l.qty, 0);
cartCount.textContent = count;
cartBtn.hidden = count === 0 || currentScreen === "welcome" || currentScreen === "reveal";
if (currentScreen === "items") renderItems();
if (currentScreen === "cart") renderCart();
if (currentScreen === "categories") renderCats();
}
// Click delegation for screen navigation
document.body.addEventListener("click", (e) => {
const goBtn = e.target.closest("[data-go]");
if (goBtn) {
if (goBtn.dataset.go === "welcome") {
cart = [];
cartCount.textContent = 0;
}
go(goBtn.dataset.go);
return;
}
// Cart button in topbar
if (e.target.closest("#cartBtn")) {
go("cart");
return;
}
});
catGrid.addEventListener("click", (e) => {
const tile = e.target.closest("[data-cat]");
if (!tile) return;
currentCat = tile.dataset.cat;
go("items");
});
itemGrid.addEventListener("click", (e) => {
const btn = e.target.closest("[data-action]");
if (!btn) return;
const wrap = btn.closest("[data-id]");
const id = wrap.dataset.id;
changeQty(id, btn.dataset.action === "inc" ? 1 : -1, {
name: wrap.dataset.name,
price: wrap.dataset.price,
glyph: wrap.dataset.glyph,
});
});
cartLines.addEventListener("click", (e) => {
const btn = e.target.closest("[data-action]");
if (!btn) return;
const row = btn.closest("[data-id]");
if (!row) return;
changeQty(row.dataset.id, btn.dataset.action === "inc" ? 1 : -1);
});
clearBtn.addEventListener("click", () => {
if (cart.length === 0) return;
cart = [];
renderCart();
cartCount.textContent = 0;
cartBtn.hidden = true;
});
[goCart, goCart2].forEach((btn) =>
btn.addEventListener("click", () => {
if (cart.length === 0) return;
go("cart");
})
);
goPayBtn.addEventListener("click", () => {
if (cart.length === 0) return;
go("pay");
});
// Pay choices
document.querySelectorAll("[data-pay]").forEach((card) =>
card.addEventListener("click", () => {
const t = totals();
const orderNo = `#${String(Math.floor(100 + Math.random() * 900))}`;
document.getElementById("orderNo").textContent = orderNo;
document.getElementById("revealItems").textContent = cart.reduce((n, l) => n + l.qty, 0);
document.getElementById("revealTotal").textContent = money(t.total);
const eta = new Date();
eta.setMinutes(eta.getMinutes() + 9);
document.getElementById("revealEta").textContent =
`${String(eta.getHours()).padStart(2, "0")}:${String(eta.getMinutes()).padStart(2, "0")}`;
const method = card.dataset.pay;
document.getElementById("revealSub").textContent =
method === "counter"
? "Take your number to the counter to pay · we'll plate while you do."
: method === "apple"
? "Payment approved · pickup at the counter when called."
: "Payment approved · enjoy.";
go("reveal");
})
);
// Language switcher (visual only)
document.querySelectorAll(".lang").forEach((b) =>
b.addEventListener("click", () => {
document.querySelectorAll(".lang").forEach((x) => x.classList.remove("is-active"));
b.classList.add("is-active");
})
);
renderCats();<!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@700;800&family=Inter:wght@500;600;700;800&family=JetBrains+Mono:wght@500;700&display=swap"
/>
<link rel="stylesheet" href="style.css" />
<title>Self-order kiosk · Casa Olivar</title>
</head>
<body>
<div class="kiosk">
<div class="frame">
<!-- Persistent topbar -->
<header class="bar" id="topbar">
<div class="bar-left">
<span class="brand">Casa Olivar</span>
<span class="bar-step" id="stepLabel">Welcome</span>
</div>
<div class="bar-right">
<button class="lang is-active" type="button" data-lang="en">EN</button>
<button class="lang" type="button" data-lang="es">ES</button>
<button class="cart-btn" id="cartBtn" type="button" aria-label="Cart" hidden>
<span class="cart-icon">🧾</span>
<span class="cart-count" id="cartCount">0</span>
</button>
</div>
</header>
<!-- Step 1 — Welcome -->
<section class="screen screen-welcome" data-screen="welcome">
<div class="welcome-art" aria-hidden="true">
<span class="glyph g-1">🥖</span>
<span class="glyph g-2">🥩</span>
<span class="glyph g-3">🍷</span>
</div>
<p class="welcome-kicker">Tap to order</p>
<h1>
Welcome to <span class="em">Casa Olivar.</span>
</h1>
<p class="welcome-sub">
Tap below to begin · 6 menu sections · payment on the next screen
</p>
<button class="tap-cta" type="button" data-go="categories">
<span>Start an order</span>
<span class="tap-arrow">→</span>
</button>
<button class="quiet-cta" type="button" data-go="categories">
I have a code · QR scan
</button>
</section>
<!-- Step 2 — Categories -->
<section class="screen screen-cats" data-screen="categories" hidden>
<header class="screen-head">
<h2>What's it going to be?</h2>
<p>Choose a category — you can mix from any of them.</p>
</header>
<div class="cat-grid" id="catGrid"></div>
<footer class="screen-foot">
<button class="ghost-cta" type="button" data-go="welcome">← Cancel</button>
<button class="primary-cta" type="button" id="goCart" disabled>Review order →</button>
</footer>
</section>
<!-- Step 3 — Items (per category) -->
<section class="screen screen-items" data-screen="items" hidden>
<header class="screen-head">
<button class="back" type="button" data-go="categories">← Back</button>
<h2 id="catTitle">—</h2>
<p id="catSub">—</p>
</header>
<div class="item-grid" id="itemGrid"></div>
<footer class="screen-foot">
<button class="ghost-cta" type="button" data-go="categories">← Other categories</button>
<button class="primary-cta" type="button" id="goCart2">Review order →</button>
</footer>
</section>
<!-- Step 4 — Cart -->
<section class="screen screen-cart" data-screen="cart" hidden>
<header class="screen-head">
<button class="back" type="button" data-go="categories">← Add more</button>
<h2>Your order</h2>
<p id="cartCountSub">0 items</p>
</header>
<ul class="cart-lines" id="cartLines"></ul>
<p class="cart-empty" id="cartEmpty">Your basket is empty — tap "Add more" to start.</p>
<dl class="cart-totals">
<div><dt>Subtotal</dt><dd id="subtotal">$0.00</dd></div>
<div><dt>Tax (8.25%)</dt><dd id="tax">$0.00</dd></div>
<div class="big"><dt>Total</dt><dd id="total">$0.00</dd></div>
</dl>
<footer class="screen-foot">
<button class="ghost-cta" type="button" id="clear">Clear basket</button>
<button class="primary-cta" type="button" id="goPay" disabled>Continue to pay →</button>
</footer>
</section>
<!-- Step 5 — Pay -->
<section class="screen screen-pay" data-screen="pay" hidden>
<header class="screen-head">
<button class="back" type="button" data-go="cart">← Back to order</button>
<h2>How would you like to pay?</h2>
<p>Choose a method below — each option is the same speed.</p>
</header>
<div class="pay-options">
<button class="pay-card" type="button" data-pay="here">
<span class="pay-icon">💳</span>
<h3>Pay here</h3>
<p>Tap or insert card on this kiosk · receipt printed.</p>
<span class="pay-tag">Fastest</span>
</button>
<button class="pay-card" type="button" data-pay="counter">
<span class="pay-icon">🧾</span>
<h3>Pay at counter</h3>
<p>Take your order number to the counter · cash accepted.</p>
</button>
<button class="pay-card" type="button" data-pay="apple">
<span class="pay-icon">📱</span>
<h3>Apple / Google Pay</h3>
<p>Hold your phone or watch to the reader.</p>
</button>
</div>
<footer class="screen-foot">
<span class="pay-total" id="payTotal">$0.00</span>
<button class="ghost-cta" type="button" data-go="cart">← Cart</button>
</footer>
</section>
<!-- Step 6 — Reveal -->
<section class="screen screen-reveal" data-screen="reveal" hidden>
<p class="reveal-eyebrow">Thank you · enjoy</p>
<p class="reveal-no" id="orderNo">#0184</p>
<h2>Your number is up.</h2>
<p class="reveal-sub" id="revealSub">
We'll call your number at the counter in about 9 minutes.
</p>
<div class="reveal-meta">
<dl>
<div><dt>Items</dt><dd id="revealItems">—</dd></div>
<div><dt>Total</dt><dd id="revealTotal">—</dd></div>
<div><dt>Pickup</dt><dd id="revealEta">—</dd></div>
</dl>
</div>
<button class="tap-cta" type="button" data-go="welcome">
<span>Start a new order</span>
<span class="tap-arrow">↻</span>
</button>
</section>
</div>
</div>
<script src="script.js"></script>
</body>
</html>Self-Ordering Kiosk
The freestanding kiosk customers walk up to in a fast-casual restaurant — vertical layout (portrait kiosk), 72px minimum touch targets, no hover-only affordances, full-screen iframe-friendly. Five-step flow: welcome → pick a category → tap items → review cart → choose pay-here vs pay-at-counter. Includes an order-number reveal screen and a language switcher (EN / ES).