Initial commit — Bar Pivoine cocktail recipe site

Hugo Extended site with 426 cocktail recipes from the open cocktail dataset.
Dark amber/gold editorial aesthetic, Tailwind CSS v4, Alpine.js client-side
search and filtering, HTMX page transitions, Docker + nginx production build.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-07 11:53:45 +02:00
commit b3b9fb7ac6
462 changed files with 9012 additions and 0 deletions
+239
View File
@@ -0,0 +1,239 @@
@import "tailwindcss";
@source "../../layouts/**/*.html";
@source "../../content/**/*.md";
@source "../../assets/js/**/*.js";
/* ── Design tokens ─────────────────────────────────────────────────────────── */
@theme {
/* Core palette — exact match to design */
--color-bg: #14100c;
--color-bg-deep: #0d0a07;
--color-surface: #1c1611;
--color-surface-2: #241c15;
--color-warm: #e9d2b4; /* base for line/border via opacity modifiers */
/* Text scale */
--color-ink: #efe6da;
--color-ink-soft: #c9bbab;
--color-ink-mute: #8d8073;
--color-ink-faint: #5f574d;
/* Amber gold accent */
--color-gold: #cf9648;
--color-gold-2: #e3ad5e;
--color-gold-deep: #9c6a2c;
/* Typography — overrides Tailwind defaults */
--font-serif: "Cormorant Garamond", Georgia, serif;
--font-sans: "Hanken Grotesk", system-ui, sans-serif;
--font-mono: "JetBrains Mono", ui-monospace, monospace;
}
/* ── Base ──────────────────────────────────────────────────────────────────── */
@layer base {
*, *::before, *::after { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
background-color: var(--color-bg);
color: var(--color-ink);
font-family: var(--font-sans);
font-size: 16px;
line-height: 1.6;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
overflow-x: hidden;
}
/* Warm ambient vignette */
body::before {
content: "";
position: fixed;
inset: 0;
z-index: 0;
pointer-events: none;
background:
radial-gradient(120% 80% at 50% -10%, rgba(207,150,72,0.07), transparent 60%),
radial-gradient(100% 100% at 50% 120%, rgba(0,0,0,0.5), transparent 55%);
}
#main-content { position: relative; z-index: 1; }
a { color: inherit; text-decoration: none; }
button { font-family: inherit; cursor: pointer; }
img { display: block; max-width: 100%; }
::selection { background: rgba(207,150,72,0.16); color: var(--color-gold-2); }
::-webkit-scrollbar { width: 11px; height: 11px; }
::-webkit-scrollbar-track { background: var(--color-bg-deep); }
::-webkit-scrollbar-thumb { background: #33291f; border-radius: 20px; border: 3px solid var(--color-bg-deep); }
::-webkit-scrollbar-thumb:hover { background: #473829; }
[x-cloak] { display: none !important; }
}
/* ── Components — only things that cannot be expressed with utilities alone ── */
@layer components {
/* Eyebrow label — reused across many templates */
.eyebrow {
font-family: var(--font-mono);
font-size: 11px;
letter-spacing: 0.34em;
text-transform: uppercase;
color: var(--color-gold);
font-weight: 500;
}
/* Display type — large serif headlines */
.display {
font-family: var(--font-serif);
font-weight: 500;
line-height: 0.98;
letter-spacing: -0.01em;
}
/* Chip — inline tag with hover states, used everywhere */
.chip {
display: inline-flex;
align-items: center;
gap: 7px;
font-family: var(--font-mono);
font-size: 11px;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--color-ink-soft);
background: rgba(233,210,180,0.04);
border: 1px solid rgba(233,210,180,0.10);
padding: 6px 12px;
border-radius: 30px;
transition: 0.18s;
white-space: nowrap;
cursor: pointer;
text-decoration: none;
}
.chip:hover { border-color: var(--color-gold); color: var(--color-gold-2); }
.chip.on { background: var(--color-gold); color: #1a1206; border-color: var(--color-gold); font-weight: 600; }
.chip .dot { width: 5px; height: 5px; border-radius: 50%; background: currentColor; opacity: 0.8; flex: none; }
/* Buttons */
.btn {
display: inline-flex;
align-items: center;
gap: 9px;
font-family: var(--font-sans);
font-size: 13.5px;
letter-spacing: 0.02em;
padding: 13px 24px;
border-radius: 30px;
border: 1px solid transparent;
transition: 0.22s;
text-decoration: none;
}
.btn-gold { background: var(--color-gold); color: #1a1206; font-weight: 600; }
.btn-gold:hover { background: var(--color-gold-2); transform: translateY(-1px); box-shadow: 0 10px 30px -10px var(--color-gold); }
.btn-ghost { border-color: rgba(233,210,180,0.18); color: var(--color-ink); background: transparent; }
.btn-ghost:hover { border-color: var(--color-gold); color: var(--color-gold-2); background: rgba(207,150,72,0.16); }
/* Cellar header button */
.cellar-btn {
margin-left: auto;
display: inline-flex;
align-items: center;
gap: 9px;
font-family: var(--font-sans);
font-size: 12.5px;
font-weight: 600;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--color-gold);
border: 1.5px solid rgba(207,150,72,0.55);
border-radius: 30px;
padding: 10px 20px;
transition: background .2s, color .2s, border-color .2s, box-shadow .2s;
white-space: nowrap;
text-decoration: none;
}
.cellar-btn:hover {
background: var(--color-gold);
color: #1a1206;
border-color: var(--color-gold);
box-shadow: 0 6px 24px -8px rgba(207,150,72,0.45);
}
.cellar-btn.active {
background: var(--color-gold);
color: #1a1206;
border-color: var(--color-gold);
}
/* Rise animation class */
.rise { animation: rise .55s cubic-bezier(.2,.7,.3,1); }
/* Art Deco home separator lines — gradient cannot be expressed with utilities */
/* Frame pseudo-overlay — bottom gradient on framed images */
.frame-overlay::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(to top, rgba(13,10,7,.5), transparent 42%);
pointer-events: none;
}
/* Card image hover scale — needs descendant selector */
.card-img { width: 100%; height: 100%; object-fit: cover; transition: transform .5s; }
.card-wrap:hover .card-img { transform: scale(1.05); }
/* Archive editorial search input placeholder */
.arch-input::placeholder { color: var(--color-ink-faint); font-style: italic; }
/* Filter dropdown menu — absolutely positioned, can't be expressed with utilities */
.fmenu {
position: absolute;
top: calc(100% + 8px);
left: 0;
z-index: 40;
min-width: 230px;
max-height: 340px;
overflow: auto;
background: var(--color-surface-2);
border: 1px solid rgba(233,210,180,0.18);
border-radius: 13px;
padding: 7px;
box-shadow: 0 30px 60px -28px #000;
animation: rise .2s ease both;
}
/* HTMX progress bar */
#progress-bar {
position: fixed;
top: 0; left: 0; right: 0;
z-index: 999;
height: 2px;
opacity: 0;
transition: opacity .2s;
background: linear-gradient(to right, var(--color-gold), var(--color-gold-2), var(--color-gold));
background-size: 200% 100%;
}
#progress-bar.htmx-request {
opacity: 1;
animation: progress-sweep 1.5s ease-in-out infinite;
}
}
/* ── Keyframes ─────────────────────────────────────────────────────────────── */
@keyframes rise { from { transform: translateY(14px); } to { transform: none; } }
@keyframes page-in { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
@keyframes page-out { from { opacity: 1; transform: none; } to { opacity: 0; transform: translateY(-8px); } }
@keyframes progress-sweep { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
/* ── View Transitions ──────────────────────────────────────────────────────── */
::view-transition-old(root) { animation: 200ms ease-in page-out; }
::view-transition-new(root) { animation: 300ms ease-out page-in; }
/* ── Reduced motion ────────────────────────────────────────────────────────── */
@media (prefers-reduced-motion: reduce) {
.rise { animation: none; }
::view-transition-old(root),
::view-transition-new(root) { animation: none; }
}
+182
View File
@@ -0,0 +1,182 @@
/* Bar Pivoine — main.js */
// ── Taxonomy slug helper (matches design's taxSlug) ──────────────────────────
function taxSlug(s) {
return String(s).toLowerCase().replace(/&/g, "and").replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
}
// ── Build taxonomy facets from cocktail list ─────────────────────────────────
function buildTax(list) {
const acc = { alcoholic: {}, category: {}, glass: {}, ingredient: {} };
const add = (bucket, raw) => {
if (!raw) return;
const slug = taxSlug(raw);
if (!slug) return;
const e = bucket[slug] || (bucket[slug] = { value: raw, count: 0, slug });
e.count++;
};
list.forEach(c => {
add(acc.alcoholic, c.alcoholic);
add(acc.category, c.category);
add(acc.glass, c.glass);
(c.ingredients || []).forEach(i => add(acc.ingredient, i.name));
});
const sort = o => Object.values(o).sort((a, b) => b.count - a.count || a.value.localeCompare(b.value));
return {
alcoholic: sort(acc.alcoholic),
category: sort(acc.category),
glass: sort(acc.glass),
ingredient: sort(acc.ingredient),
};
}
// ── HTMX progress bar ────────────────────────────────────────────────────────
(function () {
const bar = document.getElementById("progress-bar");
if (!bar) return;
document.addEventListener("htmx:beforeRequest", () => bar.classList.add("htmx-request"));
document.addEventListener("htmx:afterSwap", () => { bar.classList.remove("htmx-request"); });
document.addEventListener("htmx:responseError", () => bar.classList.remove("htmx-request"));
})();
// ── Alpine store ─────────────────────────────────────────────────────────────
document.addEventListener("alpine:init", () => {
Alpine.store("nav", { open: false });
});
// ── Cocktail search + filter component (Alpine x-data) ───────────────────────
function cocktailSearch() {
return {
q: "",
active: { alcoholic: "", category: "", glass: "", ingredient: "" },
page: 1,
perPage: 24,
openFilter: null,
all: [],
tax: { alcoholic: [], category: [], glass: [], ingredient: [] },
init() {
this.all = window.__COCKTAILS__ || [];
this.tax = buildTax(this.all);
const p = new URLSearchParams(window.location.search);
this.q = p.get("q") || "";
this.active.alcoholic = p.get("alcoholic") || "";
this.active.category = p.get("category") || "";
this.active.glass = p.get("glass") || "";
this.active.ingredient = p.get("ingredient") || "";
this.page = parseInt(p.get("page") || "1", 10);
// Close any open filter on outside click
document.addEventListener("click", e => {
if (!e.target.closest("[data-fgroup]")) this.openFilter = null;
});
},
get filtered() {
const needle = this.q.trim().toLowerCase();
return this.all.filter(c => {
if (this.active.alcoholic && taxSlug(c.alcoholic) !== this.active.alcoholic) return false;
if (this.active.category && taxSlug(c.category) !== this.active.category) return false;
if (this.active.glass && taxSlug(c.glass) !== this.active.glass) return false;
if (this.active.ingredient && !c.ingredients.some(i => taxSlug(i.name) === this.active.ingredient)) return false;
if (needle) {
const hay = (c.name + " " + c.category + " " + c.glass + " " +
c.ingredients.map(i => i.name).join(" ")).toLowerCase();
if (!hay.includes(needle)) return false;
}
return true;
});
},
get totalPages() {
return Math.max(1, Math.ceil(this.filtered.length / this.perPage));
},
get paged() {
const s = (this.page - 1) * this.perPage;
return this.filtered.slice(s, s + this.perPage);
},
get activeCount() {
return Object.values(this.active).filter(Boolean).length + (this.q ? 1 : 0);
},
get isFiltered() { return this.activeCount > 0; },
nobLabel(c) {
if (c.alcoholic === "Alcoholic") return "Spirited";
if (c.alcoholic === "Non alcoholic") return "Zero-proof";
return "Optional";
},
valLabel(key, slug) {
const t = this.tax[key].find(i => i.slug === slug);
return t ? t.value : slug;
},
toggleFilter(key) {
this.openFilter = this.openFilter === key ? null : key;
},
toggle(key, slug) {
this.active[key] = this.active[key] === slug ? "" : slug;
this.page = 1;
this.openFilter = null;
this.pushState();
},
setQuery(val) { this.q = val; this.page = 1; this.pushState(); },
clearAll() {
this.q = "";
this.active = { alcoholic: "", category: "", glass: "", ingredient: "" };
this.page = 1;
this.pushState();
},
changePage(n) {
this.page = Math.max(1, Math.min(this.totalPages, n));
this.pushState();
window.scrollTo({ top: 0, behavior: "smooth" });
},
pagerItems() {
const total = this.totalPages, page = this.page;
const clamp = n => Math.max(1, Math.min(total, n));
const show = new Set([1, 2, clamp(page-2), clamp(page-1), page,
clamp(page+1), clamp(page+2), total-1, total]
.filter(n => n >= 1 && n <= total));
const sorted = [...show].sort((a, b) => a - b);
const items = [];
for (let i = 0; i < sorted.length; i++) {
if (i > 0 && sorted[i] - sorted[i-1] > 1) items.push({ type: "dot" });
items.push({ type: "page", n: sorted[i] });
}
return items;
},
pushState() {
const p = new URLSearchParams();
if (this.q) p.set("q", this.q);
if (this.active.alcoholic) p.set("alcoholic", this.active.alcoholic);
if (this.active.category) p.set("category", this.active.category);
if (this.active.glass) p.set("glass", this.active.glass);
if (this.active.ingredient) p.set("ingredient", this.active.ingredient);
if (this.page > 1) p.set("page", this.page);
history.replaceState({}, "", p.toString() ? "?" + p : window.location.pathname);
},
};
}
window.cocktailSearch = cocktailSearch;
window.taxSlug = taxSlug;
// ── Page transition on HTMX swap ─────────────────────────────────────────────
document.addEventListener("htmx:afterSwap", () => {
const main = document.getElementById("main-content");
if (!main) return;
main.style.animation = "none";
void main.offsetHeight;
main.style.animation = "page-in 300ms ease-out";
});