UI Components Hard
Cocktail Builder
Interactive cocktail builder: pick a spirit base, choose mixers from a visual grid, add garnish — get the cocktail name, recipe steps, flavor profile radar, and a price estimate.
Open in Lab
MCP
html css vanilla-js
Targets: JS HTML
Code
/* ========================================
Cocktail Builder — Phase 27 Restaurant Theme
======================================== */
:root {
--cream: #FAF7F1;
--ink: #2C1A0E;
--forest: #345F40;
--forest-d: #213D29;
--terracotta: #C4622D;
--gold: #D4A853;
--warm-gray: #8A7D72;
--bone: #F0EBE0;
--radius-sm: 8px;
--radius-md: 12px;
--radius-lg: 16px;
--radius-pill: 100px;
--shadow-card: 0 2px 8px rgba(44, 26, 14, 0.08);
--shadow-hover: 0 6px 20px rgba(44, 26, 14, 0.14);
--shadow-panel: 0 4px 24px rgba(44, 26, 14, 0.10);
--transition: 160ms ease;
}
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Inter', sans-serif;
background: var(--cream);
color: var(--ink);
min-height: 100vh;
line-height: 1.5;
}
/* ========================================
Layout
======================================== */
.app {
display: grid;
grid-template-columns: 55% 45%;
min-height: 100vh;
gap: 0;
}
/* Builder Column */
.builder-col {
background: var(--cream);
padding: 40px 36px 40px 44px;
overflow-y: auto;
border-right: 1px solid var(--bone);
}
/* Result Column */
.result-col {
background: var(--bone);
padding: 40px 36px;
position: sticky;
top: 0;
height: 100vh;
overflow-y: auto;
}
/* ========================================
Builder Header
======================================== */
.builder-header {
margin-bottom: 36px;
}
.builder-title {
font-family: 'Playfair Display', serif;
font-size: 2.2rem;
font-weight: 800;
color: var(--ink);
letter-spacing: -0.02em;
line-height: 1.1;
}
.builder-subtitle {
font-size: 0.95rem;
color: var(--warm-gray);
margin-top: 6px;
font-weight: 400;
}
/* ========================================
Step Sections
======================================== */
.step-section {
margin-bottom: 36px;
}
.step-label {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 16px;
}
.step-number {
font-family: 'Playfair Display', serif;
font-size: 0.75rem;
font-weight: 700;
color: var(--terracotta);
background: rgba(196, 98, 45, 0.10);
padding: 3px 8px;
border-radius: var(--radius-pill);
letter-spacing: 0.06em;
}
.step-text {
font-size: 0.95rem;
font-weight: 700;
color: var(--ink);
letter-spacing: -0.01em;
}
.step-hint {
font-weight: 400;
color: var(--warm-gray);
font-size: 0.85rem;
}
/* ========================================
Spirit Cards Grid
======================================== */
.spirits-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
}
.spirit-card {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
height: 90px;
background: white;
border: 2px solid transparent;
border-radius: var(--radius-md);
cursor: pointer;
transition: border-color var(--transition), background var(--transition), transform var(--transition), box-shadow var(--transition);
box-shadow: var(--shadow-card);
padding: 10px 8px;
}
.spirit-card:hover {
border-color: var(--forest);
transform: translateY(-2px);
box-shadow: var(--shadow-hover);
}
.spirit-card.selected {
border-color: var(--forest);
background: var(--bone);
box-shadow: 0 0 0 1px var(--forest), var(--shadow-card);
}
.spirit-emoji {
font-size: 1.6rem;
line-height: 1;
}
.spirit-name {
font-size: 0.78rem;
font-weight: 600;
color: var(--ink);
letter-spacing: 0.02em;
text-transform: uppercase;
}
/* ========================================
Mixer Chips
======================================== */
.mixers-grid {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.mixer-chip {
padding: 7px 14px;
border-radius: var(--radius-pill);
border: 1.5px solid var(--bone);
background: white;
font-size: 0.82rem;
font-weight: 500;
color: var(--ink);
cursor: pointer;
transition: border-color var(--transition), background var(--transition), color var(--transition), transform var(--transition);
font-family: 'Inter', sans-serif;
white-space: nowrap;
}
.mixer-chip:hover {
border-color: var(--forest);
transform: translateY(-1px);
}
.mixer-chip.selected {
background: var(--forest);
border-color: var(--forest);
color: white;
font-weight: 600;
}
.mixer-chip.disabled {
opacity: 0.4;
cursor: not-allowed;
pointer-events: none;
}
.mixer-count {
margin-top: 10px;
font-size: 0.78rem;
color: var(--warm-gray);
font-weight: 500;
}
/* ========================================
Garnish Chips
======================================== */
.garnish-grid {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.garnish-chip {
padding: 7px 14px;
border-radius: var(--radius-pill);
border: 1.5px solid var(--bone);
background: white;
font-size: 0.82rem;
font-weight: 500;
color: var(--ink);
cursor: pointer;
transition: border-color var(--transition), background var(--transition), color var(--transition), transform var(--transition);
font-family: 'Inter', sans-serif;
white-space: nowrap;
}
.garnish-chip:hover {
border-color: var(--gold);
transform: translateY(-1px);
}
.garnish-chip.selected {
background: var(--gold);
border-color: var(--gold);
color: var(--ink);
font-weight: 600;
}
/* ========================================
Result Panel
======================================== */
.result-panel {
display: flex;
flex-direction: column;
gap: 24px;
}
/* Glass */
.glass-wrapper {
display: flex;
justify-content: center;
}
.glass-svg {
width: 120px;
height: 165px;
filter: drop-shadow(0 4px 12px rgba(52, 95, 64, 0.18));
transition: filter var(--transition);
}
/* Cocktail Name */
.cocktail-name-wrapper {
text-align: center;
}
.cocktail-name {
font-family: 'Playfair Display', serif;
font-size: 1.9rem;
font-weight: 800;
color: var(--ink);
letter-spacing: -0.02em;
line-height: 1.15;
transition: opacity 200ms ease;
}
.cocktail-tagline {
font-size: 0.85rem;
color: var(--warm-gray);
margin-top: 4px;
}
/* Radar Chart */
.radar-wrapper {
display: flex;
justify-content: center;
background: white;
border-radius: var(--radius-md);
padding: 16px;
box-shadow: var(--shadow-card);
}
.radar-svg {
width: 160px;
height: 160px;
}
#radar-poly {
transition: points 300ms ease;
}
/* Recipe */
.recipe-wrapper {
background: white;
border-radius: var(--radius-md);
padding: 20px;
box-shadow: var(--shadow-card);
}
.recipe-title {
font-family: 'Playfair Display', serif;
font-size: 1rem;
font-weight: 700;
color: var(--ink);
margin-bottom: 12px;
}
.recipe-steps {
padding-left: 18px;
display: flex;
flex-direction: column;
gap: 8px;
}
.recipe-step {
font-size: 0.85rem;
color: var(--ink);
line-height: 1.4;
}
.recipe-step.muted {
color: var(--warm-gray);
list-style: none;
padding-left: 0;
margin-left: -18px;
}
/* Price + CTA */
.price-cta {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
background: white;
border-radius: var(--radius-md);
padding: 16px 20px;
box-shadow: var(--shadow-card);
}
.price-badge {
display: flex;
flex-direction: column;
gap: 2px;
}
.price-label {
font-size: 0.72rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--warm-gray);
}
.price-value {
font-family: 'Playfair Display', serif;
font-size: 1.3rem;
font-weight: 700;
color: var(--forest);
}
.cta-btn {
padding: 10px 22px;
background: var(--forest);
color: white;
border: none;
border-radius: var(--radius-pill);
font-family: 'Inter', sans-serif;
font-size: 0.88rem;
font-weight: 600;
cursor: pointer;
transition: background var(--transition), transform var(--transition), opacity var(--transition), box-shadow var(--transition);
letter-spacing: 0.02em;
}
.cta-btn:hover:not(:disabled) {
background: var(--forest-d);
transform: translateY(-2px);
box-shadow: 0 4px 14px rgba(52, 95, 64, 0.3);
}
.cta-btn:active:not(:disabled) {
transform: translateY(0);
}
.cta-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
.cta-btn.added {
background: var(--gold);
color: var(--ink);
}
/* ========================================
Responsive
======================================== */
@media (max-width: 700px) {
.app {
grid-template-columns: 1fr;
min-height: auto;
}
.builder-col {
padding: 28px 20px;
border-right: none;
border-bottom: 1px solid var(--bone);
}
.result-col {
position: static;
height: auto;
padding: 28px 20px;
}
.builder-title {
font-size: 1.7rem;
}
.spirits-grid {
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.spirit-card {
height: 80px;
}
.spirit-emoji {
font-size: 1.4rem;
}
}/* ========================================
Cocktail Builder — Script
Phase 27 Restaurant Theme
======================================== */
'use strict';
// ── Data ─────────────────────────────────────────────────────────────────────
const SPIRITS = {
gin: { emoji: '🫙', color: '#a8d8b9', flavors: { sweet: 2, sour: 3, bitter: 4, strong: 4, fresh: 5 } },
rum: { emoji: '🍹', color: '#d4956a', flavors: { sweet: 5, sour: 2, bitter: 1, strong: 4, fresh: 2 } },
tequila: { emoji: '🌵', color: '#f0c060', flavors: { sweet: 3, sour: 4, bitter: 2, strong: 5, fresh: 3 } },
vodka: { emoji: '🧊', color: '#b8d4e8', flavors: { sweet: 1, sour: 2, bitter: 1, strong: 4, fresh: 3 } },
whiskey: { emoji: '🥃', color: '#c8843a', flavors: { sweet: 4, sour: 1, bitter: 4, strong: 5, fresh: 1 } },
mezcal: { emoji: '💨', color: '#8fad91', flavors: { sweet: 2, sour: 2, bitter: 5, strong: 5, fresh: 2 } },
};
const MIXER_LABELS = {
'lime': 'Lime juice',
'lemon': 'Lemon juice',
'simple-syrup': 'Simple syrup',
'agave': 'Agave syrup',
'tonic': 'Tonic',
'soda': 'Soda',
'dry-vermouth': 'Dry vermouth',
'sweet-vermouth':'Sweet vermouth',
'ginger-beer': 'Ginger beer',
'bitters': 'Bitters',
'coconut': 'Coconut cream',
'pineapple': 'Pineapple juice',
};
// Cocktail name resolution: spirit + up to 2 key mixers → name
// Entries checked top-to-bottom; first match wins.
const COCKTAIL_RULES = [
// Gin combos
{ spirit: 'gin', mixers: ['lime', 'tonic'], name: 'Casa Tónica', tagline: 'Crisp & botanical' },
{ spirit: 'gin', mixers: ['dry-vermouth'], name: 'Martini Casa', tagline: 'Elegant & dry' },
{ spirit: 'gin', mixers: ['lemon', 'simple-syrup'], name: 'Tom Collins Verde', tagline: 'Bright & refreshing' },
{ spirit: 'gin', mixers: ['lime', 'ginger-beer'], name: 'Buck del Jardín', tagline: 'Spicy & herbal' },
// Rum combos
{ spirit: 'rum', mixers: ['coconut', 'pineapple'], name: 'Colada del Mar', tagline: 'Tropical & creamy' },
{ spirit: 'rum', mixers: ['lime', 'simple-syrup'], name: 'Daiquiri Suave', tagline: 'Balanced & citrusy' },
{ spirit: 'rum', mixers: ['lime', 'soda'], name: 'Mojito Fresco', tagline: 'Light & invigorating' },
{ spirit: 'rum', mixers: ['ginger-beer', 'lime'], name: 'Mule Caribeño', tagline: 'Zesty & tropical' },
// Tequila combos
{ spirit: 'tequila', mixers: ['lime', 'agave'], name: 'Margarita Huerta', tagline: 'Classic & vibrant' },
{ spirit: 'tequila', mixers: ['lime', 'simple-syrup'], name: 'Margarita Clásica', tagline: 'Tart & lively' },
{ spirit: 'tequila', mixers: ['ginger-beer'], name: 'Burro Mexicano', tagline: 'Bold & spicy' },
{ spirit: 'tequila', mixers: ['pineapple', 'coconut'], name: 'Playa Escondida', tagline: 'Tropical & sweet' },
// Vodka combos
{ spirit: 'vodka', mixers: ['lemon', 'soda'], name: 'Spritz Blanco', tagline: 'Light & effervescent' },
{ spirit: 'vodka', mixers: ['lime', 'ginger-beer'], name: 'Moscow Mule Rosa', tagline: 'Crisp & spicy' },
{ spirit: 'vodka', mixers: ['pineapple', 'coconut'], name: 'Tropical Blanco', tagline: 'Sweet & fruity' },
{ spirit: 'vodka', mixers: ['lemon', 'simple-syrup'], name: 'Lemon Drop Suave', tagline: 'Tart & sweet' },
// Whiskey combos
{ spirit: 'whiskey', mixers: ['bitters', 'simple-syrup'], name: 'Old Fashioned Suave', tagline: 'Rich & complex' },
{ spirit: 'whiskey', mixers: ['sweet-vermouth', 'bitters'],name: 'Manhattan Reserve', tagline: 'Bold & sophisticated' },
{ spirit: 'whiskey', mixers: ['lemon', 'simple-syrup'], name: 'Whiskey Sour Artisan', tagline: 'Balanced & bright' },
{ spirit: 'whiskey', mixers: ['ginger-beer'], name: 'Kentucky Mule', tagline: 'Warm & spicy' },
// Mezcal combos
{ spirit: 'mezcal', mixers: ['lime', 'agave'], name: 'Mezcal Sour', tagline: 'Smoky & tangy' },
{ spirit: 'mezcal', mixers: ['sweet-vermouth'], name: 'Oaxacan Negroni', tagline: 'Smoky & bitter' },
{ spirit: 'mezcal', mixers: ['pineapple', 'lime'], name: 'Tepache Ahumado', tagline: 'Tropical & smoky' },
{ spirit: 'mezcal', mixers: ['ginger-beer', 'lime'], name: 'Burro Ahumado', tagline: 'Fiery & complex' },
];
// Radar axes in order (maps to pentagon points)
const FLAVOR_KEYS = ['sweet', 'sour', 'bitter', 'strong', 'fresh'];
// Pentagon: 5 outer points, radius 80, center 100,100
// Angles start at top (-90°) and go clockwise
const RADAR_CENTER = { x: 100, y: 100 };
const RADAR_R = 80;
const PENTAGON_ANGLES = FLAVOR_KEYS.map((_, i) => ((-Math.PI / 2) + (2 * Math.PI * i) / 5));
// ── State ─────────────────────────────────────────────────────────────────────
let state = {
spirit: null, // string key
mixers: [], // array of string keys (max 3)
garnish: 'none', // string key
};
// ── DOM refs ──────────────────────────────────────────────────────────────────
const spiritsGrid = document.getElementById('spirits-grid');
const mixersGrid = document.getElementById('mixers-grid');
const garnishGrid = document.getElementById('garnish-grid');
const mixerCount = document.getElementById('mixer-count');
const cocktailName = document.getElementById('cocktail-name');
const cocktailTag = document.getElementById('cocktail-tagline');
const recipeSteps = document.getElementById('recipe-steps');
const priceValue = document.getElementById('price-value');
const ctaBtn = document.getElementById('cta-btn');
const radarPoly = document.getElementById('radar-poly');
const glassLiquid = document.getElementById('glass-liquid');
// ── Utilities ─────────────────────────────────────────────────────────────────
function clamp(v, min, max) {
return Math.min(max, Math.max(min, v));
}
/**
* Convert a flavor score (0-5) to an SVG point on the radar.
* axis: 0=Sweet (top), 1=Sour (right), 2=Bitter (bottom-right), 3=Strong (bottom-left), 4=Fresh (left)
*/
function flavorToPoint(score, axisIndex) {
const t = clamp(score, 0, 5) / 5;
const angle = PENTAGON_ANGLES[axisIndex];
const r = t * RADAR_R;
return {
x: RADAR_CENTER.x + r * Math.cos(angle),
y: RADAR_CENTER.y + r * Math.sin(angle),
};
}
function pointsAttr(pts) {
return pts.map(p => `${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(' ');
}
// ── Cocktail resolution ───────────────────────────────────────────────────────
function resolveCocktail() {
if (!state.spirit) return null;
const spritDef = SPIRITS[state.spirit];
const mixers = state.mixers;
// Check rules: spirit must match, and all rule.mixers must be present in state.mixers
for (const rule of COCKTAIL_RULES) {
if (rule.spirit !== state.spirit) continue;
const allPresent = rule.mixers.every(m => mixers.includes(m));
if (allPresent) return rule;
}
// Fallback
const spiritLabel = state.spirit.charAt(0).toUpperCase() + state.spirit.slice(1);
return {
name: `${spiritLabel} Especial`,
tagline: 'Your custom creation',
};
}
/**
* Blend spirit base flavors with mixer modifiers.
*/
function computeFlavors() {
if (!state.spirit) return { sweet: 0, sour: 0, bitter: 0, strong: 0, fresh: 0 };
const base = { ...SPIRITS[state.spirit].flavors };
const MIXER_MODS = {
'lime': { sour: +1.5, fresh: +1 },
'lemon': { sour: +1.5, fresh: +0.5 },
'simple-syrup': { sweet: +2 },
'agave': { sweet: +1.5 },
'tonic': { bitter: +1, fresh: +0.5 },
'soda': { fresh: +1 },
'dry-vermouth': { bitter: +1, sour: +0.5 },
'sweet-vermouth': { sweet: +1, bitter: +0.5 },
'ginger-beer': { sour: +0.5, fresh: +1.5, strong: +0.5 },
'bitters': { bitter: +2 },
'coconut': { sweet: +2, fresh: +0.5 },
'pineapple': { sweet: +1.5, sour: +0.5, fresh: +1 },
};
const result = { ...base };
for (const m of state.mixers) {
const mod = MIXER_MODS[m] || {};
for (const [k, v] of Object.entries(mod)) {
result[k] = (result[k] || 0) + v;
}
}
// Clamp all to 0-5
for (const k of FLAVOR_KEYS) {
result[k] = clamp(result[k] || 0, 0, 5);
}
return result;
}
/**
* Generate recipe steps based on current state.
*/
function buildRecipe() {
if (!state.spirit) return [];
const spiritLabel = SPIRITS[state.spirit].emoji + ' ' + (state.spirit.charAt(0).toUpperCase() + state.spirit.slice(1));
const mixerCount = state.mixers.length;
const steps = [];
steps.push('Add ice to a cocktail shaker or glass.');
steps.push(`Pour 2 oz ${spiritLabel} over the ice.`);
if (mixerCount > 0) {
const mixerNames = state.mixers.slice(0, 3).map(m => MIXER_LABELS[m] || m).join(', ');
steps.push(`Add ${mixerNames} and stir well.`);
} else {
steps.push('Stir gently to chill.');
}
if (state.garnish !== 'none') {
const garnishLabels = {
'lime-wheel': 'lime wheel',
'orange-twist': 'orange twist',
'cherry': 'cherry',
'mint': 'mint sprig',
'salt-rim': 'salt rim',
};
steps.push(`Finish with a ${garnishLabels[state.garnish] || state.garnish} and serve.`);
} else {
steps.push('Strain into a chilled glass and serve immediately.');
}
return steps;
}
/**
* Compute price: base $10 + $1 per mixer, capped at $18.
*/
function computePrice() {
if (!state.spirit) return null;
const base = 10;
const perMixer = state.mixers.length;
const low = clamp(base + perMixer, 10, 18);
const high = clamp(low + 4, 12, 22);
return `$${low} – $${high}`;
}
// ── Render ─────────────────────────────────────────────────────────────────────
function renderRadar(flavors) {
const points = FLAVOR_KEYS.map((k, i) => flavorToPoint(flavors[k], i));
radarPoly.setAttribute('points', pointsAttr(points));
}
function renderResult() {
const cocktail = resolveCocktail();
const flavors = computeFlavors();
const steps = buildRecipe();
const price = computePrice();
// Name & tagline
if (cocktail) {
cocktailName.textContent = cocktail.name;
cocktailTag.textContent = cocktail.tagline || '';
} else {
cocktailName.textContent = '—';
cocktailTag.textContent = 'Select a spirit to begin';
}
// Glass liquid color
if (state.spirit) {
glassLiquid.setAttribute('fill', SPIRITS[state.spirit].color);
glassLiquid.setAttribute('opacity', '0.55');
} else {
glassLiquid.setAttribute('fill', '#a8d8b9');
glassLiquid.setAttribute('opacity', '0.2');
}
// Radar
renderRadar(flavors);
// Recipe steps
if (steps.length) {
recipeSteps.innerHTML = steps.map(s => `<li class="recipe-step">${s}</li>`).join('');
} else {
recipeSteps.innerHTML = '<li class="recipe-step muted">Select your ingredients to see the recipe</li>';
}
// Price
priceValue.textContent = price || '—';
// CTA
ctaBtn.disabled = !state.spirit;
}
// ── Event handlers ────────────────────────────────────────────────────────────
// Spirit cards
spiritsGrid.addEventListener('click', (e) => {
const card = e.target.closest('.spirit-card');
if (!card) return;
const spirit = card.dataset.spirit;
// Toggle off if already selected
if (state.spirit === spirit) {
state.spirit = null;
} else {
state.spirit = spirit;
}
// Update UI
document.querySelectorAll('.spirit-card').forEach(c => {
c.classList.toggle('selected', c.dataset.spirit === state.spirit);
});
renderResult();
});
// Mixer chips
mixersGrid.addEventListener('click', (e) => {
const chip = e.target.closest('.mixer-chip');
if (!chip || chip.classList.contains('disabled')) return;
const mixer = chip.dataset.mixer;
if (state.mixers.includes(mixer)) {
// Deselect
state.mixers = state.mixers.filter(m => m !== mixer);
chip.classList.remove('selected');
} else if (state.mixers.length < 3) {
// Select
state.mixers.push(mixer);
chip.classList.add('selected');
}
// Update disabled state for unselected chips
const count = state.mixers.length;
document.querySelectorAll('.mixer-chip').forEach(c => {
const isSelected = state.mixers.includes(c.dataset.mixer);
c.classList.toggle('disabled', count >= 3 && !isSelected);
});
mixerCount.textContent = `${count} / 3 selected`;
renderResult();
});
// Garnish chips
garnishGrid.addEventListener('click', (e) => {
const chip = e.target.closest('.garnish-chip');
if (!chip) return;
state.garnish = chip.dataset.garnish;
document.querySelectorAll('.garnish-chip').forEach(c => {
c.classList.toggle('selected', c.dataset.garnish === state.garnish);
});
renderResult();
});
// CTA Button
ctaBtn.addEventListener('click', () => {
if (!state.spirit || ctaBtn.disabled) return;
const cocktail = resolveCocktail();
const name = cocktail ? cocktail.name : 'your cocktail';
ctaBtn.textContent = 'Added!';
ctaBtn.classList.add('added');
setTimeout(() => {
ctaBtn.textContent = 'Add to order';
ctaBtn.classList.remove('added');
}, 2000);
});
// ── Init ───────────────────────────────────────────────────────────────────────
// Set garnish "None" as default selected
const defaultGarnish = document.querySelector('.garnish-chip[data-garnish="none"]');
if (defaultGarnish) defaultGarnish.classList.add('selected');
// Initial render (empty state)
renderResult();<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Cocktail Builder</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@700;800&family=Inter:wght@400;500;600;700&display=swap" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="app">
<!-- Left: Builder Column -->
<div class="builder-col">
<div class="builder-header">
<h1 class="builder-title">Cocktail Builder</h1>
<p class="builder-subtitle">Craft your perfect drink</p>
</div>
<!-- Step 1: Spirit -->
<section class="step-section">
<div class="step-label">
<span class="step-number">01</span>
<span class="step-text">Choose your spirit</span>
</div>
<div class="spirits-grid" id="spirits-grid">
<button class="spirit-card" data-spirit="gin">
<span class="spirit-emoji">🫙</span>
<span class="spirit-name">Gin</span>
</button>
<button class="spirit-card" data-spirit="rum">
<span class="spirit-emoji">🍹</span>
<span class="spirit-name">Rum</span>
</button>
<button class="spirit-card" data-spirit="tequila">
<span class="spirit-emoji">🌵</span>
<span class="spirit-name">Tequila</span>
</button>
<button class="spirit-card" data-spirit="vodka">
<span class="spirit-emoji">🧊</span>
<span class="spirit-name">Vodka</span>
</button>
<button class="spirit-card" data-spirit="whiskey">
<span class="spirit-emoji">🥃</span>
<span class="spirit-name">Whiskey</span>
</button>
<button class="spirit-card" data-spirit="mezcal">
<span class="spirit-emoji">💨</span>
<span class="spirit-name">Mezcal</span>
</button>
</div>
</section>
<!-- Step 2: Mixers -->
<section class="step-section">
<div class="step-label">
<span class="step-number">02</span>
<span class="step-text">Add mixers <span class="step-hint">(up to 3)</span></span>
</div>
<div class="mixers-grid" id="mixers-grid">
<button class="mixer-chip" data-mixer="lime">Lime juice</button>
<button class="mixer-chip" data-mixer="lemon">Lemon juice</button>
<button class="mixer-chip" data-mixer="simple-syrup">Simple syrup</button>
<button class="mixer-chip" data-mixer="agave">Agave syrup</button>
<button class="mixer-chip" data-mixer="tonic">Tonic</button>
<button class="mixer-chip" data-mixer="soda">Soda</button>
<button class="mixer-chip" data-mixer="dry-vermouth">Dry vermouth</button>
<button class="mixer-chip" data-mixer="sweet-vermouth">Sweet vermouth</button>
<button class="mixer-chip" data-mixer="ginger-beer">Ginger beer</button>
<button class="mixer-chip" data-mixer="bitters">Bitters</button>
<button class="mixer-chip" data-mixer="coconut">Coconut cream</button>
<button class="mixer-chip" data-mixer="pineapple">Pineapple juice</button>
</div>
<div class="mixer-count" id="mixer-count">0 / 3 selected</div>
</section>
<!-- Step 3: Garnish -->
<section class="step-section">
<div class="step-label">
<span class="step-number">03</span>
<span class="step-text">Garnish</span>
</div>
<div class="garnish-grid" id="garnish-grid">
<button class="garnish-chip" data-garnish="lime-wheel">🍋 Lime wheel</button>
<button class="garnish-chip" data-garnish="orange-twist">🍊 Orange twist</button>
<button class="garnish-chip" data-garnish="cherry">🍒 Cherry</button>
<button class="garnish-chip" data-garnish="mint">🌿 Mint sprig</button>
<button class="garnish-chip" data-garnish="salt-rim">🧂 Salt rim</button>
<button class="garnish-chip selected" data-garnish="none">— None</button>
</div>
</section>
</div>
<!-- Right: Result Column -->
<div class="result-col" id="result-col">
<div class="result-panel">
<!-- Glass SVG -->
<div class="glass-wrapper">
<svg class="glass-svg" id="glass-svg" viewBox="0 0 160 220" xmlns="http://www.w3.org/2000/svg">
<!-- Liquid fill -->
<clipPath id="glass-clip">
<path d="M30 30 L20 190 Q80 210 140 190 L130 30 Z" />
</clipPath>
<rect id="glass-liquid" x="0" y="80" width="160" height="130" clip-path="url(#glass-clip)" fill="#a8d8b9" opacity="0.45" />
<!-- Glass outline -->
<path d="M30 30 L20 190 Q80 210 140 190 L130 30 Z" fill="none" stroke="#345F40" stroke-width="2.5" stroke-linejoin="round"/>
<!-- Stem -->
<line x1="80" y1="190" x2="80" y2="210" stroke="#345F40" stroke-width="2.5"/>
<!-- Base -->
<line x1="55" y1="210" x2="105" y2="210" stroke="#345F40" stroke-width="3"/>
<!-- Rim highlight -->
<line x1="30" y1="30" x2="130" y2="30" stroke="#345F40" stroke-width="2" stroke-linecap="round"/>
<!-- Ice cubes (decorative) -->
<g id="glass-ice" opacity="0.6">
<rect x="50" y="100" width="18" height="18" rx="3" fill="white" opacity="0.7" transform="rotate(-10 59 109)"/>
<rect x="78" y="110" width="18" height="18" rx="3" fill="white" opacity="0.7" transform="rotate(8 87 119)"/>
<rect x="96" y="95" width="16" height="16" rx="3" fill="white" opacity="0.6" transform="rotate(-5 104 103)"/>
</g>
</svg>
</div>
<!-- Cocktail Name -->
<div class="cocktail-name-wrapper">
<h2 class="cocktail-name" id="cocktail-name">—</h2>
<p class="cocktail-tagline" id="cocktail-tagline">Select a spirit to begin</p>
</div>
<!-- Flavor Radar -->
<div class="radar-wrapper">
<svg class="radar-svg" id="radar-svg" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
<!-- Background grid (pentagon) -->
<g id="radar-grid" stroke="#8A7D72" stroke-width="0.8" fill="none" opacity="0.4">
<!-- outer -->
<polygon id="pg-5" points="100,20 176,74 148,162 52,162 24,74" />
<!-- 4/5 -->
<polygon points="100,36 164.8,82.8 140.4,157.6 59.6,157.6 35.2,82.8" />
<!-- 3/5 -->
<polygon points="100,52 153.6,91.6 132.8,153.2 67.2,153.2 46.4,91.6" />
<!-- 2/5 -->
<polygon points="100,68 142.4,100.4 125.2,148.8 74.8,148.8 57.6,100.4" />
<!-- 1/5 -->
<polygon points="100,84 131.2,109.2 117.6,144.4 82.4,144.4 68.8,109.2" />
</g>
<!-- Axis lines -->
<g stroke="#8A7D72" stroke-width="0.8" opacity="0.5">
<line x1="100" y1="100" x2="100" y2="20"/>
<line x1="100" y1="100" x2="176" y2="74"/>
<line x1="100" y1="100" x2="148" y2="162"/>
<line x1="100" y1="100" x2="52" y2="162"/>
<line x1="100" y1="100" x2="24" y2="74"/>
</g>
<!-- Filled polygon -->
<polygon id="radar-poly" points="100,100 100,100 100,100 100,100 100,100" fill="#C4622D" opacity="0.45" stroke="#C4622D" stroke-width="1.5"/>
<!-- Axis labels -->
<text x="100" y="14" text-anchor="middle" font-size="9" fill="#2C1A0E" font-family="Inter" font-weight="600">Sweet</text>
<text x="184" y="76" text-anchor="start" font-size="9" fill="#2C1A0E" font-family="Inter" font-weight="600">Sour</text>
<text x="152" y="174" text-anchor="middle" font-size="9" fill="#2C1A0E" font-family="Inter" font-weight="600">Bitter</text>
<text x="48" y="174" text-anchor="middle" font-size="9" fill="#2C1A0E" font-family="Inter" font-weight="600">Strong</text>
<text x="16" y="76" text-anchor="end" font-size="9" fill="#2C1A0E" font-family="Inter" font-weight="600">Fresh</text>
</svg>
</div>
<!-- Recipe Steps -->
<div class="recipe-wrapper">
<h3 class="recipe-title">Recipe</h3>
<ol class="recipe-steps" id="recipe-steps">
<li class="recipe-step muted">Select your ingredients to see the recipe</li>
</ol>
</div>
<!-- Price + CTA -->
<div class="price-cta">
<div class="price-badge">
<span class="price-label">Estimated price</span>
<span class="price-value" id="price-value">—</span>
</div>
<button class="cta-btn" id="cta-btn" disabled>Add to order</button>
</div>
</div>
</div>
</div>
<script src="script.js"></script>
</body>
</html>Cocktail Builder
Three-step builder: (1) Spirit base selector (cards: Gin / Rum / Tequila / Vodka / Whiskey / Mezcal); (2) Mixer grid (multi-select: citrus, soda, vermouth, tonic, syrup, bitters, juice variants); (3) Garnish picker (lime wheel, orange twist, cherry, mint, salt rim, none). Right panel shows: the generated cocktail name, SVG glass illustration, flavor profile radar (sweet/sour/bitter/strong/fresh as polygon chart), recipe steps, and price. Selecting different combos generates different named cocktails.