cauldron/cauldron/templates/_base.html
Sulkta 578dda32e5 v0.2 — meal picks, infinite scroll, search, mushroom vibes, mobile polish
Alice pass: more mobile friendly + infinite scroll + auto-search +
add-to-AI-plan picks list + mushroom vibes.

DB:
- New migration 005 — cauldron_meal_picks (authentik_sub, recipe_slug,
  recipe_name, added_at). Per-user wishlist for the v0.3 AI meal-plan
  generator. add/remove/list helpers in DB.

Backend:
- GET  /api/recipes.json?page=N&q=Q — paginated + searchable, returns
  {items, page, total, total_pages, next}. Each item is annotated with
  picked: bool from the user's pick set.
- POST /api/picks/<slug>            — add (slug + optional name body)
- DEL  /api/picks/<slug>            — remove
- GET  /api/picks.json              — list current picks
- GET  /picks                       — picks page (replaces empty stub)
- Mealie.list_recipes() now accepts search=...

Frontend:
- recipes.html rebuilt:
  - sticky search bar with 250ms debounce, hits /api/recipes.json?q=
  - IntersectionObserver-driven infinite scroll, loads page+1 when the
    sentinel comes into view (200px rootMargin, AbortController for
    in-flight cancel on new search)
  - per-card mushroom toggle button (top-right) — POST/DELETE to picks
    with optimistic UI flip + rollback on failure
  - picked cards get a left purple-glow stripe + tinted background
- _recipe_card.html partial — first-page server-render shares markup with
  JS-rendered subsequent cards (mushroom SVG inline, same shape)
- recipe_detail.html — '🍄 pin for ai plan' button toggles state in place
- picks.html — list of current picks with remove button + v0.3 explainer
- Topbar nav: dropped /home, added /picks

Mushroom vibes:
- Hand-rolled SVG toadstool (purple cap, bone stem, dark spots) used as
  the pick toggle icon — it's the gesture itself
- Same mushroom tiled into the body bg pattern at ~5% opacity in the
  bottom-right of the 160px sigil tile, alongside the existing pentagram
- Mushroom emoji on the detail page button + picks page nudge

Mobile pass:
- Topbar nav scrolls horizontally on narrow screens, brand-sub hidden
  under 720px, larger tap targets on cards, font-size pulled in slightly
- Recipe grid: 1 col <560, 2 col 560-900, 3 col 900+
- Page-head + button + card padding all tightened on small screens
2026-04-28 20:53:07 -07:00

373 lines
15 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="color-scheme" content="dark">
<meta name="theme-color" content="#0a0a0c">
<title>{% block title %}Cauldron{% endblock %}</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&family=Cinzel:wght@500;600;700&display=swap" rel="stylesheet">
<style>
:root {
--bg: #0a0a0c;
--bg-2: #0f0c14;
--surface: #14101a;
--surface-2: #1d1828;
--surface-3: #251f33;
--line: #2a223a;
--line-soft: #1f1a2a;
--green: #6ea848;
--green-bright: #88c060;
--green-dim: #4a7530;
--green-glow: rgba(110, 168, 72, .25);
--purple: #9b5fe8;
--purple-bright:#b878ff;
--purple-dim: #6b3fa0;
--purple-deep: #2d1d4a;
--purple-glow: rgba(155, 95, 232, .25);
--bone: #e8e0c8;
--bone-dim: #a89d83;
--muted: #6e6478;
--warn: #d4a854;
--crit: #e8606a;
--sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--serif: 'Cinzel', Georgia, serif;
--mono: 'JetBrains Mono', ui-monospace, Menlo, monospace;
}
* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
html { scroll-behavior: smooth; -webkit-text-size-adjust: 100%; }
html, body {
margin: 0; padding: 0;
color: var(--bone);
font-family: var(--sans);
font-size: 15px; line-height: 1.6;
min-height: 100vh; min-height: 100dvh;
-webkit-font-smoothing: antialiased;
background: var(--bg);
}
body {
background-image:
radial-gradient(ellipse 80% 60% at 15% 0%, rgba(155, 95, 232, .07) 0%, transparent 60%),
radial-gradient(ellipse 80% 60% at 85% 100%, rgba(110, 168, 72, .05) 0%, transparent 60%);
/* layered: corner candleglow, mossy underbelly, witchy sigil + mushroom tile */
background-image:
radial-gradient(ellipse 80% 60% at 15% 0%, rgba(155, 95, 232, .07) 0%, transparent 60%),
radial-gradient(ellipse 80% 60% at 85% 100%, rgba(110, 168, 72, .05) 0%, transparent 60%),
url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='160' height='160' viewBox='0 0 160 160'><g fill='none' stroke='%239b5fe8' stroke-width='.4' stroke-opacity='.05'><circle cx='40' cy='40' r='14'/><polygon points='40,28 43.5,37 53,38 45.8,44 48.1,53 40,48 31.9,53 34.2,44 27,38 36.5,37'/></g><g fill='%236ea848' fill-opacity='.045' stroke='%236ea848' stroke-width='.3' stroke-opacity='.06'><path d='M118 96 q-9 -1 -9 8 q0 4 9 4 q9 0 9 -4 q0 -9 -9 -8z'/><path d='M115 108 v8 q0 2 3 2 q3 0 3 -2 v-8'/><circle cx='113' cy='100' r='1' fill='%23000' fill-opacity='.4' stroke='none'/><circle cx='121' cy='99' r='.8' fill='%23000' fill-opacity='.4' stroke='none'/><circle cx='117' cy='104' r='.7' fill='%23000' fill-opacity='.4' stroke='none'/></g></svg>");
background-attachment: fixed;
background-size: auto, auto, 160px 160px;
}
::selection { background: rgba(155, 95, 232, .35); color: var(--bone); }
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: var(--bg-2); }
::-webkit-scrollbar-thumb { background: var(--surface-2); border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: var(--purple-dim); }
/* Top bar */
header.topbar {
position: sticky; top: 0; z-index: 50;
display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap;
gap: 14px;
padding: 16px 22px;
border-bottom: 1px solid var(--line);
background: rgba(10, 10, 12, .85);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
}
.brand { display: flex; align-items: baseline; gap: 14px; min-width: 0; }
.brand-mark {
font-family: var(--serif); font-weight: 700;
color: var(--purple-bright); font-size: 22px;
letter-spacing: .15em; text-transform: uppercase;
text-shadow: 0 0 24px var(--purple-glow);
}
.brand-sub { color: var(--muted); font-size: 11px; letter-spacing: .2em; text-transform: uppercase; font-family: var(--mono); }
nav.nav { display: flex; gap: 22px; flex-wrap: wrap; }
nav.nav a {
color: var(--bone-dim); text-decoration: none;
font-size: 12px; letter-spacing: .15em; text-transform: uppercase;
padding: 6px 0; position: relative; transition: color .2s;
}
nav.nav a:hover { color: var(--green-bright); }
nav.nav a.active { color: var(--purple-bright); }
nav.nav a.active::after {
content: ""; position: absolute; left: 0; right: 0; bottom: 0; height: 2px;
background: var(--purple-bright); box-shadow: 0 0 12px var(--purple-glow);
}
.topmeta {
color: var(--muted); font-size: 11px; letter-spacing: .15em; text-transform: uppercase;
font-family: var(--mono);
}
.topmeta .who { color: var(--green-bright); }
/* Main */
main {
max-width: 920px; margin: 0 auto; padding: 36px 22px 80px;
position: relative;
}
/* Page header */
.page-head { margin-bottom: 1.6em; animation: fadeIn .5s ease-out forwards; }
.page-head .crumb {
color: var(--purple); font-size: 11px; letter-spacing: .25em;
text-transform: uppercase; font-family: var(--mono); margin-bottom: .5em;
}
.page-head h1 {
margin: 0; color: var(--bone);
font-family: var(--serif); font-weight: 600;
font-size: 2.2em; letter-spacing: .04em; line-height: 1.2;
}
.page-head h1 .accent { color: var(--purple-bright); text-shadow: 0 0 24px var(--purple-glow); }
.page-head .lede { color: var(--bone-dim); font-size: 1em; margin-top: .5em; max-width: 60ch; }
/* Panel */
.panel {
background: var(--surface);
border: 1px solid var(--line);
border-radius: 6px;
padding: 20px 22px;
margin: 16px 0;
position: relative;
box-shadow: 0 0 24px -8px rgba(155, 95, 232, .08);
animation: fadeIn .5s ease-out forwards;
opacity: 0;
}
.panel:nth-child(2) { animation-delay: .05s; }
.panel:nth-child(3) { animation-delay: .1s; }
.panel:nth-child(4) { animation-delay: .15s; }
.panel:hover { border-color: var(--surface-3); }
.panel.green {
box-shadow: 0 0 24px -8px rgba(110, 168, 72, .1);
border-left: 2px solid var(--green-dim);
}
.panel.purple { border-left: 2px solid var(--purple-dim); }
.panel-head {
display: flex; align-items: baseline; gap: 14px; flex-wrap: wrap;
margin-bottom: 14px; padding-bottom: 10px;
border-bottom: 1px solid var(--line-soft);
}
.panel-head h2 {
margin: 0; color: var(--purple-bright);
font-family: var(--serif); font-weight: 600;
font-size: 1.05em; letter-spacing: .15em; text-transform: uppercase;
}
.panel.green .panel-head h2 { color: var(--green-bright); }
.panel-head .ctx { color: var(--muted); font-size: 11px; letter-spacing: .15em; text-transform: uppercase; margin-left: auto; font-family: var(--mono); }
/* Pill */
.pill {
display: inline-block; padding: 3px 10px; border: 1px solid;
border-radius: 4px; font-size: 10px; letter-spacing: .2em; text-transform: uppercase;
font-family: var(--mono); font-weight: 600;
}
.pill-ok { color: var(--green-bright); border-color: var(--green-dim); background: rgba(110, 168, 72, .08); }
.pill-warn { color: var(--warn); border-color: var(--warn); background: rgba(212, 168, 84, .08); }
.pill-crit { color: var(--crit); border-color: var(--crit); background: rgba(232, 96, 106, .08); }
.pill-mute { color: var(--bone-dim); border-color: var(--line); background: var(--bg-2); }
/* KV */
.kv { display: grid; grid-template-columns: max-content 1fr; gap: .5em 1.4em; margin: .6em 0; }
.kv dt { color: var(--purple); font-size: 11px; letter-spacing: .15em; text-transform: uppercase; font-family: var(--mono); padding-top: 3px; }
.kv dd { margin: 0; color: var(--bone); font-size: .95em; word-break: break-all; }
/* Buttons */
.btn {
display: inline-block; padding: .6em 1.4em;
background: var(--surface-2); color: var(--bone);
border: 1px solid var(--line);
border-radius: 5px;
font-family: var(--sans); font-weight: 500;
font-size: 13px; letter-spacing: .08em;
cursor: pointer; text-decoration: none;
transition: all .2s ease;
}
.btn:hover { background: var(--surface-3); border-color: var(--purple-dim); color: var(--bone); }
.btn-primary {
background: var(--green-dim); color: var(--bone);
border-color: var(--green); box-shadow: 0 0 20px -8px var(--green-glow);
}
.btn-primary:hover { background: var(--green); border-color: var(--green-bright); box-shadow: 0 0 24px -4px var(--green-glow); }
.btn-purple {
background: var(--purple-deep); color: var(--bone);
border-color: var(--purple-dim); box-shadow: 0 0 20px -8px var(--purple-glow);
}
.btn-purple:hover { background: var(--purple-dim); border-color: var(--purple); box-shadow: 0 0 24px -4px var(--purple-glow); }
.btn-row { display: flex; gap: 12px; flex-wrap: wrap; align-items: center; }
/* Forms */
input[type=text], input[type=password], textarea {
width: 100%; padding: .75em .9em;
background: var(--bg-2); border: 1px solid var(--line);
border-radius: 4px;
color: var(--bone); font-family: var(--mono); font-size: .92em;
transition: border-color .2s, box-shadow .2s;
}
input[type=text]:focus { outline: none; border-color: var(--purple); box-shadow: 0 0 0 3px rgba(155, 95, 232, .12); }
/* Misc */
a { color: var(--green-bright); text-decoration: none; transition: color .2s; }
a:hover { color: var(--purple-bright); }
.muted { color: var(--muted); font-size: .9em; }
.lede { color: var(--bone-dim); }
hr { border: none; border-top: 1px solid var(--line); margin: 2em 0; }
code {
background: var(--purple-deep); border: 1px solid var(--purple-dim);
color: var(--purple-bright);
padding: .12em .5em; border-radius: 3px;
font-family: var(--mono); font-size: .88em;
}
strong { color: var(--bone); font-weight: 600; }
ol, ul { padding-left: 1.4em; }
ol li, ul li { margin: .35em 0; }
/* Search bar (sticky-ish on the recipes page) */
.search-row {
display: flex; gap: 10px; align-items: center;
margin-bottom: 18px;
}
.search-row input {
flex: 1; padding: .7em .9em;
background: var(--bg-2); border: 1px solid var(--line);
border-radius: 4px;
color: var(--bone); font-family: var(--sans); font-size: 14px;
}
.search-row input:focus { outline: none; border-color: var(--purple); box-shadow: 0 0 0 3px rgba(155, 95, 232, .12); }
.search-row .count { color: var(--muted); font-size: 11px; letter-spacing: .15em; text-transform: uppercase; font-family: var(--mono); white-space: nowrap; }
/* Recipe grid + cards */
.recipe-grid {
display: grid; grid-template-columns: 1fr; gap: 12px;
}
@media (min-width: 560px) { .recipe-grid { grid-template-columns: 1fr 1fr; } }
@media (min-width: 900px) { .recipe-grid { grid-template-columns: 1fr 1fr 1fr; } }
.recipe-card {
display: block; padding: 14px 14px 14px 16px;
border: 1px solid var(--line); background: var(--surface);
border-radius: 5px;
text-decoration: none; color: inherit;
position: relative; overflow: hidden;
transition: all .2s ease;
}
.recipe-card.picked { border-color: var(--purple-dim); background: rgba(45, 29, 74, .35); }
.recipe-card.picked::before {
content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 3px;
background: var(--purple-bright); box-shadow: 0 0 12px var(--purple-glow);
}
.recipe-card:hover {
border-color: var(--purple-dim); background: var(--surface-2);
transform: translateY(-1px);
box-shadow: 0 4px 24px -8px var(--purple-glow);
}
.recipe-card .rmain { display: block; padding-right: 36px; }
.recipe-card .rname {
color: var(--bone); font-family: var(--serif); font-weight: 600;
font-size: 1.08em; letter-spacing: .02em; line-height: 1.25;
}
.recipe-card:hover .rname { color: var(--purple-bright); }
.recipe-card .rmeta {
color: var(--muted); font-size: 10px; letter-spacing: .15em;
text-transform: uppercase; font-family: var(--mono); margin-top: 8px;
}
.recipe-card .rtags { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 8px; }
.recipe-card .rtag {
color: var(--green-bright); border: 1px solid var(--green-dim);
background: rgba(110, 168, 72, .06);
padding: 1px 8px; border-radius: 3px;
font-size: 9px; letter-spacing: .15em; text-transform: uppercase;
font-family: var(--mono);
}
/* Pick toggle — small mushroom button top-right of card */
.pick-btn {
position: absolute; top: 10px; right: 10px;
width: 30px; height: 30px;
background: var(--bg-2); border: 1px solid var(--line);
border-radius: 50%;
display: flex; align-items: center; justify-content: center;
cursor: pointer; padding: 0;
transition: all .2s ease;
z-index: 2;
}
.pick-btn:hover { border-color: var(--purple); background: var(--surface-3); transform: scale(1.08); }
.pick-btn .shroom { width: 16px; height: 16px; opacity: .55; transition: opacity .2s; }
.pick-btn:hover .shroom, .pick-btn.on .shroom { opacity: 1; }
.pick-btn.on { border-color: var(--purple-bright); background: var(--purple-deep); box-shadow: 0 0 12px var(--purple-glow); }
/* Infinite-scroll sentinel */
.scroll-sentinel { height: 24px; }
.scroll-state { text-align: center; color: var(--muted); font-size: 11px; letter-spacing: .2em; text-transform: uppercase; font-family: var(--mono); padding: 14px 0; }
.scroll-state.done { color: var(--bone-dim); }
.scroll-state.error { color: var(--crit); }
/* Mobile niceties */
@media (max-width: 720px) {
body { font-size: 14.5px; }
main { padding: 22px 14px 60px; }
header.topbar { padding: 12px 14px; gap: 8px; }
.brand-mark { font-size: 19px; letter-spacing: .12em; }
.brand-sub { display: none; }
nav.nav { gap: 14px; width: 100%; order: 3; overflow-x: auto; padding-bottom: 4px; }
nav.nav a { font-size: 11px; flex-shrink: 0; padding: 4px 0; }
.topmeta .who { font-size: 10px; }
.page-head h1 { font-size: 1.7em; }
.panel { padding: 16px; }
.recipe-card { padding: 12px 12px 12px 14px; }
.recipe-card .rmain { padding-right: 32px; }
.recipe-card .rname { font-size: 1em; }
.pick-btn { width: 28px; height: 28px; top: 8px; right: 8px; }
.btn { padding: .65em 1.2em; font-size: 12.5px; }
.search-row { flex-wrap: wrap; }
.search-row .count { width: 100%; }
}
@media (max-width: 380px) {
.page-head h1 { font-size: 1.5em; }
.recipe-card .rname { font-size: .95em; }
}
/* Animations */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
form { margin: 0; }
button { font-family: inherit; }
</style>
</head>
<body>
<header class="topbar">
<div class="brand">
<span class="brand-mark">Cauldron</span>
<span class="brand-sub">family · LAN</span>
</div>
{% if session.user %}
<nav class="nav">
<a href="/recipes" class="{% if active == 'recipes' %}active{% endif %}">recipes</a>
<a href="/picks" class="{% if active == 'picks' %}active{% endif %}">picks</a>
<a href="/plan" class="{% if active == 'plan' %}active{% endif %}">plan</a>
<a href="/list" class="{% if active == 'list' %}active{% endif %}">list</a>
<a href="/me" class="{% if active == 'me' %}active{% endif %}">me</a>
</nav>
<div class="topmeta">
<span class="who">{{ session.user.email }}</span>
</div>
{% endif %}
</header>
<main>
{% block content %}{% endblock %}
</main>
</body>
</html>