/* ── Reserve Nav Link ── */ .nav-reserve-link { font-family: 'Barlow Condensed', sans-serif; font-size: 11px; font-weight: 700; letter-spacing: .15em; text-transform: uppercase; color: #C9A84C !important; text-decoration: none; transition: color .2s; } .nav-reserve-link:hover { color: #e8c97a !important; } /* ── Reserve Gate (on product pages) ── */ .reserve-member-badge { display: inline-block; background: #C9A84C; color: #1a120a; font-family: 'Barlow Condensed', sans-serif; font-size: 10px; font-weight: 700; letter-spacing: .15em; text-transform: uppercase; padding: 5px 12px; margin-bottom: 12px; } .reserve-gate { border: 1.5px solid #C9A84C; padding: 24px; text-align: center; background: rgba(201,168,76,.06); margin: 8px 0 12px; } .reserve-gate-lock { font-size: 28px; margin-bottom: 8px; } .reserve-gate-title { font-family: 'Playfair Display', serif; font-size: 20px; font-weight: 700; color: var(--ink); margin-bottom: 8px; } .reserve-gate-desc { font-family: 'EB Garamond', serif; font-size: 15px; color: var(--ink-mid); line-height: 1.55; margin-bottom: 20px; } .reserve-gate-btns { display: flex; flex-direction: column; gap: 8px; } .reserve-gate-join { display: block; padding: 13px 24px; background: #C9A84C; color: #1a120a; font-family: 'Barlow Condensed', sans-serif; font-size: 11px; font-weight: 700; letter-spacing: .15em; text-transform: uppercase; text-decoration: none; transition: background .2s; } .reserve-gate-join:hover { background: #e8c97a; } .reserve-gate-login { display: block; padding: 11px 24px; background: transparent; color: var(--ink); font-family: 'Barlow Condensed', sans-serif; font-size: 11px; font-weight: 600; letter-spacing: .12em; text-transform: uppercase; text-decoration: none; border: 1.5px solid var(--rule-hvy); transition: all .2s; } .reserve-gate-login:hover { border-color: var(--ink); }
Est. 1887 Complimentary shipping on orders over $99 · Age Verification Required on Delivery · Currently accepting waitlist for No. 7
Vol. VII, Issue 2026 The Reserve Collection — Single Malt Scottish Whisky Since 1887 · Speyside, Scotland

SpeakSpirits

Fine Spirits & The Art of Distillation
Members Only

The Reserve

A private back-room for serious spirits enthusiasts. Exclusive bottles. Early access to drops. Prices the general public never sees.

01

What You Get

🔒

Exclusive Bottles

Access bottles that never appear in our public shop. Allocated, limited, and hand-selected by our tasting panel.

First on the Drop

When a rare bottle lands, Reserve members get 48 hours of exclusive access before anyone else even knows it exists.

💰

Member Pricing

10–15% off everything in our public shop, plus exclusive member-only pricing on Reserve bottles. Pays for itself fast.

02

Choose Your Plan

Monthly
$19.99
per month
  • All Reserve benefits
  • Cancel anytime
  • Instant access
Join Monthly
03

How It Works

Who gets access to Reserve bottles?

Only active Reserve members. Once you join, your account is tagged and you'll see Reserve-exclusive bottles across the site. Non-members see the bottle but can't purchase.

How do I get member pricing?

It's automatic. Log in as a Reserve member and member prices appear at checkout. No codes needed.

When do drops happen?

Drops are unannounced by design — that's the point. Members get an email 48 hours before the bottle goes public. Check your inbox.

Can I cancel?

Monthly members can cancel any time. Annual memberships are non-refundable but you'll be notified before renewal.

What if I'm already a customer?

Your existing order history stays. Just join The Reserve and your account is upgraded immediately.

*/ (function () { "use strict"; const SCRIPT = document.currentScript; const API_URL = "https://ai-bartender-production.up.railway.app"; const STARTERS = [ "🥃 What are you in the mood for tonight?", "🍽️ Planning a dinner party? Tell me about the menu.", "🎁 Looking for a gift? Let me help.", ]; const WELCOME = "Hey there! I'm Jack — your friendly neighborhood barkeep at SpeakSpirits. What can I pour for you tonight?"; /* ── Inject CSS ── */ function injectStyles() { const base = SCRIPT ? SCRIPT.src.replace(/bartender-widget\.js.*$/, "") : ""; const link = document.createElement("link"); link.rel = "stylesheet"; link.href = base + "bartender-widget.css"; document.head.appendChild(link); } /* ── Build DOM ── */ function buildWidget() { const wrapper = document.createElement("div"); wrapper.id = "ss-bartender-widget"; wrapper.innerHTML = `
🥃

Jack the Barkeep

SpeakSpirits.com

`; document.body.appendChild(wrapper); } /* ── State ── */ let messages = []; // {role, content} let isOpen = false; let isStreaming = false; /* ── DOM refs (set after build) ── */ let panel, bubble, msgContainer, input, sendBtn, startersDiv; /* ── Helpers ── */ function scrollBottom() { msgContainer.scrollTop = msgContainer.scrollHeight; } function autoResize() { input.style.height = "auto"; input.style.height = Math.min(input.scrollHeight, 80) + "px"; } /** Parse product JSON blocks from assistant text and return HTML */ function renderContent(text) { // Replace ```product {...} ``` blocks with product cards const productRe = /```product\s*(\{[\s\S]*?\})\s*```/g; let html = text; html = html.replace(productRe, function (_, jsonStr) { try { const p = JSON.parse(jsonStr); return `

${esc(p.name)}

${esc(p.tasting_note || "")}
`; } catch { return ""; } }); // Basic markdown: **bold**, *italic*, line breaks html = html.replace(/\*\*(.+?)\*\*/g, "$1"); html = html.replace(/\*(.+?)\*/g, "$1"); html = html.replace(/\n/g, "
"); return html; } function esc(s) { const d = document.createElement("div"); d.textContent = s; return d.innerHTML; } function addMessage(role, content, streaming) { const div = document.createElement("div"); div.className = "ss-msg " + (role === "user" ? "ss-msg-user" : "ss-msg-bot"); div.innerHTML = renderContent(content); if (streaming) div.setAttribute("data-streaming", "1"); msgContainer.appendChild(div); scrollBottom(); return div; } function showTyping() { const div = document.createElement("div"); div.className = "ss-typing"; div.id = "ss-typing"; div.innerHTML = ""; msgContainer.appendChild(div); scrollBottom(); } function hideTyping() { const el = document.getElementById("ss-typing"); if (el) el.remove(); } function hideStarters() { if (startersDiv) startersDiv.style.display = "none"; } /* ── Streaming chat ── */ async function sendMessage(text) { if (isStreaming || !text.trim()) return; hideStarters(); isStreaming = true; sendBtn.disabled = true; // User message messages.push({ role: "user", content: text.trim() }); addMessage("user", text.trim()); input.value = ""; autoResize(); showTyping(); try { const resp = await fetch(API_URL + "/chat", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ messages }), }); if (!resp.ok) throw new Error("API error " + resp.status); hideTyping(); const botDiv = addMessage("assistant", "", true); let fullText = ""; const reader = resp.body.getReader(); const decoder = new TextDecoder(); let buffer = ""; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split("\n\n"); buffer = lines.pop() || ""; for (const line of lines) { if (!line.startsWith("data: ")) continue; try { const evt = JSON.parse(line.slice(6)); if (evt.type === "text") { fullText += evt.text; botDiv.innerHTML = renderContent(fullText); scrollBottom(); } else if (evt.type === "error") { fullText += evt.text; botDiv.innerHTML = renderContent(fullText); } } catch {} } } messages.push({ role: "assistant", content: fullText }); botDiv.removeAttribute("data-streaming"); } catch (err) { hideTyping(); addMessage("assistant", "Oops — looks like I knocked over a bottle back here. Give me a sec and try again. 🍸"); console.error("[Bartender]", err); } finally { isStreaming = false; sendBtn.disabled = false; input.focus(); } } /* ── Toggle panel ── */ function toggle() { isOpen = !isOpen; panel.classList.toggle("ss-open", isOpen); if (isOpen) input.focus(); } /* ── Init ── */ function init() { // CSS already inlined in theme.liquid buildWidget(); panel = document.getElementById("ss-bartender-panel"); bubble = document.getElementById("ss-bartender-bubble"); msgContainer = document.getElementById("ss-messages"); input = document.getElementById("ss-input"); sendBtn = document.getElementById("ss-send"); startersDiv = document.getElementById("ss-starters"); // Welcome message addMessage("assistant", WELCOME); // Starters STARTERS.forEach(function (text) { const btn = document.createElement("button"); btn.className = "ss-starter-btn"; btn.textContent = text; btn.addEventListener("click", function () { sendMessage(text); }); startersDiv.appendChild(btn); }); // Events bubble.addEventListener("click", toggle); panel.querySelector(".ss-close-btn").addEventListener("click", toggle); sendBtn.addEventListener("click", function () { sendMessage(input.value); }); input.addEventListener("keydown", function (e) { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); sendMessage(input.value); } }); input.addEventListener("input", autoResize); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init); } else { init(); } // Expose global API for product page buttons window.BartenderWidget = { open: function() { if (!isOpen) toggle(); }, close: function() { if (isOpen) toggle(); }, toggle: toggle }; })(); */ (function () { "use strict"; const SCRIPT = document.currentScript; const API_URL = "https://ai-bartender-production-475d.up.railway.app"; const STARTERS = [ "🥃 What are you in the mood for tonight?", "🍽️ Planning a dinner party? Tell me about the menu.", "🎁 Looking for a gift? Let me help.", ]; const WELCOME = "Hey there! I'm Jack — your friendly neighborhood barkeep at SpeakSpirits. What can I pour for you tonight?"; /* CSS already inlined in theme */ function injectStyles() { /* no-op */ } /* ── Build DOM ── */ function buildWidget() { const wrapper = document.createElement("div"); wrapper.id = "ss-bartender-widget"; wrapper.innerHTML = `
🥃

Jack the Barkeep

SpeakSpirits.com

`; document.body.appendChild(wrapper); } /* ── State ── */ let messages = []; // {role, content} let isOpen = false; let isStreaming = false; /* ── DOM refs (set after build) ── */ let panel, bubble, msgContainer, input, sendBtn, startersDiv; /* ── Helpers ── */ function scrollBottom() { msgContainer.scrollTop = msgContainer.scrollHeight; } function autoResize() { input.style.height = "auto"; input.style.height = Math.min(input.scrollHeight, 80) + "px"; } /** Parse product JSON blocks from assistant text and return HTML */ function renderContent(text) { // Replace ```product {...} ``` blocks with product cards const productRe = /```product\s*(\{[\s\S]*?\})\s*```/g; let html = text; html = html.replace(productRe, function (_, jsonStr) { try { const p = JSON.parse(jsonStr); return `

${esc(p.name)}

${esc(p.tasting_note || "")}
`; } catch { return ""; } }); // Basic markdown: **bold**, *italic*, line breaks html = html.replace(/\*\*(.+?)\*\*/g, "$1"); html = html.replace(/\*(.+?)\*/g, "$1"); html = html.replace(/\n/g, "
"); return html; } function esc(s) { const d = document.createElement("div"); d.textContent = s; return d.innerHTML; } function addMessage(role, content, streaming) { const div = document.createElement("div"); div.className = "ss-msg " + (role === "user" ? "ss-msg-user" : "ss-msg-bot"); div.innerHTML = renderContent(content); if (streaming) div.setAttribute("data-streaming", "1"); msgContainer.appendChild(div); scrollBottom(); return div; } function showTyping() { const div = document.createElement("div"); div.className = "ss-typing"; div.id = "ss-typing"; div.innerHTML = ""; msgContainer.appendChild(div); scrollBottom(); } function hideTyping() { const el = document.getElementById("ss-typing"); if (el) el.remove(); } function hideStarters() { if (startersDiv) startersDiv.style.display = "none"; } /* ── Streaming chat ── */ async function sendMessage(text) { if (isStreaming || !text.trim()) return; hideStarters(); isStreaming = true; sendBtn.disabled = true; // User message messages.push({ role: "user", content: text.trim() }); addMessage("user", text.trim()); input.value = ""; autoResize(); showTyping(); try { const resp = await fetch(API_URL + "/chat", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ messages }), }); if (!resp.ok) throw new Error("API error " + resp.status); hideTyping(); const botDiv = addMessage("assistant", "", true); let fullText = ""; const reader = resp.body.getReader(); const decoder = new TextDecoder(); let buffer = ""; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split("\n\n"); buffer = lines.pop() || ""; for (const line of lines) { if (!line.startsWith("data: ")) continue; try { const evt = JSON.parse(line.slice(6)); if (evt.type === "text") { fullText += evt.text; botDiv.innerHTML = renderContent(fullText); scrollBottom(); } else if (evt.type === "error") { fullText += evt.text; botDiv.innerHTML = renderContent(fullText); } } catch {} } } messages.push({ role: "assistant", content: fullText }); botDiv.removeAttribute("data-streaming"); } catch (err) { hideTyping(); addMessage("assistant", "Oops — looks like I knocked over a bottle back here. Give me a sec and try again. 🍸"); console.error("[Bartender]", err); } finally { isStreaming = false; sendBtn.disabled = false; input.focus(); } } /* ── Toggle panel ── */ function toggle() { isOpen = !isOpen; panel.classList.toggle("ss-open", isOpen); if (isOpen) input.focus(); } /* ── Init ── */ function init() { injectStyles(); buildWidget(); panel = document.getElementById("ss-bartender-panel"); bubble = document.getElementById("ss-bartender-bubble"); msgContainer = document.getElementById("ss-messages"); input = document.getElementById("ss-input"); sendBtn = document.getElementById("ss-send"); startersDiv = document.getElementById("ss-starters"); // Welcome message addMessage("assistant", WELCOME); // Starters STARTERS.forEach(function (text) { const btn = document.createElement("button"); btn.className = "ss-starter-btn"; btn.textContent = text; btn.addEventListener("click", function () { sendMessage(text); }); startersDiv.appendChild(btn); }); // Events bubble.addEventListener("click", toggle); panel.querySelector(".ss-close-btn").addEventListener("click", toggle); sendBtn.addEventListener("click", function () { sendMessage(input.value); }); input.addEventListener("keydown", function (e) { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); sendMessage(input.value); } }); input.addEventListener("input", autoResize); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init); } else { init(); } })();