diff --git a/app.css b/app.css
index f312edf..2425b2c 100644
--- a/app.css
+++ b/app.css
@@ -396,7 +396,7 @@ textarea.inline-edit {
letter-spacing: 0;
}
-/* ── Per-type bespoke: Recipe ── */
+/* ── Shared stat chips (Recipe, Organization, etc.) ── */
.stats-strip {
display: flex;
align-items: center;
@@ -430,89 +430,271 @@ textarea.inline-edit {
height: 24px;
background: var(--line);
}
+/* Shared step-list (Recipe legacy, HowTo) */
+.step-list {
+ counter-reset: step;
+ list-style: none;
+ padding: 0;
+ margin: 0;
+}
+.step-list li {
+ position: relative;
+ padding: .5rem 0 .5rem 2.2rem;
+ line-height: 1.6;
+ color: var(--ink-2);
+ border-left: 2px solid color-mix(in oklab, var(--accent) 20%, var(--line));
+ margin-left: .6rem;
+}
+.step-list li::before {
+ counter-increment: step;
+ content: counter(step);
+ position: absolute;
+ left: -13px;
+ top: .45rem;
+ width: 24px; height: 24px;
+ border-radius: 50%;
+ background: var(--accent);
+ color: #fff;
+ font-size: .7rem;
+ font-weight: 700;
+ display: grid;
+ place-items: center;
+ box-shadow: 0 1px 4px rgba(0,0,0,.12);
+}
+
+/* ── Per-type bespoke: Recipe (stunning recipe-card) ── */
+.card.recipe-card { overflow: hidden; }
+.card.recipe-card .hd.hidden { display: none; }
-.section-block {
- margin: .75rem 0;
+/* Hero */
+.recipe-hero {
+ position: relative;
+ width: 100%;
+ height: 340px;
+ overflow: hidden;
+ cursor: pointer;
}
-.section-label {
- font-size: .68rem;
+.recipe-hero-img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ display: block;
+ transition: transform .6s cubic-bezier(.16,1,.3,1);
+}
+.recipe-hero:hover .recipe-hero-img {
+ transform: scale(1.05);
+}
+.recipe-scrim {
+ position: absolute;
+ inset: 0;
+ background:
+ linear-gradient(180deg,
+ rgba(0,0,0,.05) 0%,
+ rgba(0,0,0,.15) 35%,
+ rgba(0,0,0,.55) 70%,
+ rgba(20,10,5,.85) 100%);
+}
+.recipe-overlay {
+ position: absolute;
+ bottom: 0; left: 0; right: 0;
+ padding: 1.5rem 2rem 1.75rem;
+ color: #fff;
+}
+.recipe-badge {
+ display: inline-block;
+ padding: .3rem .7rem;
+ border-radius: 100px;
+ background: rgba(255,255,255,.18);
+ backdrop-filter: blur(10px);
+ -webkit-backdrop-filter: blur(10px);
+ border: 1px solid rgba(255,255,255,.2);
+ font-size: .62rem;
font-weight: 700;
+ letter-spacing: .1em;
+ text-transform: uppercase;
+ margin-bottom: .6rem;
+}
+.recipe-title {
+ margin: 0;
+ font-size: 2rem;
+ font-weight: 800;
+ letter-spacing: -.025em;
+ line-height: 1.1;
+ text-shadow: 0 2px 12px rgba(0,0,0,.4);
+}
+.recipe-desc {
+ margin: .5rem 0 0;
+ font-size: .88rem;
+ line-height: 1.5;
+ opacity: .88;
+ max-width: 540px;
+}
+
+/* Body */
+.card.recipe-card .bd {
+ padding: 1.5rem 2rem 2rem;
+}
+.card.recipe-card .bd::before { display: none; }
+
+/* Stats strip */
+.recipe-stats {
+ display: flex;
+ flex-wrap: wrap;
+ gap: .65rem;
+ margin-bottom: 1.5rem;
+}
+.recipe-stat {
+ display: flex;
+ align-items: center;
+ gap: .5rem;
+ padding: .55rem .9rem;
+ border-radius: 12px;
+ background: color-mix(in oklab, var(--accent) 6%, var(--surface));
+ border: 1px solid color-mix(in oklab, var(--accent) 14%, var(--line));
+ transition: transform .2s, box-shadow .2s;
+}
+.recipe-stat:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 4px 12px color-mix(in oklab, var(--accent) 15%, transparent);
+}
+.recipe-stat-icon {
+ font-size: 1.15rem;
+ line-height: 1;
+}
+.recipe-stat-text {
+ display: flex;
+ flex-direction: column;
+ line-height: 1.25;
+ font-weight: 600;
+ font-size: .82rem;
+ color: var(--ink-2);
+}
+.recipe-stat-text small {
+ font-size: .58rem;
+ font-weight: 700;
+ text-transform: uppercase;
letter-spacing: .08em;
+ color: var(--accent);
+ opacity: .85;
+}
+
+/* Section blocks */
+.recipe-section {
+ margin: 0 0 1.5rem;
+}
+.recipe-section:last-child {
+ margin-bottom: 0;
+}
+.recipe-section-title {
+ font-size: .72rem;
+ font-weight: 800;
+ letter-spacing: .1em;
text-transform: uppercase;
- color: var(--muted);
- margin-bottom: .4rem;
+ color: var(--accent);
+ margin-bottom: .75rem;
+ padding-bottom: .4rem;
+ border-bottom: 2px solid color-mix(in oklab, var(--accent) 15%, var(--line));
}
-.checklist {
+
+/* Ingredients checklist */
+.recipe-checklist {
list-style: none;
padding: 0;
margin: 0;
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
+ gap: .15rem .8rem;
}
-.checklist li {
+.recipe-check-item {
display: flex;
align-items: flex-start;
gap: .55rem;
- padding: .35rem 0;
+ padding: .35rem .35rem;
+ border-radius: 8px;
cursor: pointer;
- transition: opacity .15s;
+ transition: background .15s;
+}
+.recipe-check-item:hover {
+ background: color-mix(in oklab, var(--accent) 4%, transparent);
}
-.check-box {
+.recipe-check-box {
flex-shrink: 0;
- width: 18px; height: 18px;
- border: 2px solid color-mix(in oklab, var(--accent) 40%, var(--line));
- border-radius: 4px;
+ width: 20px; height: 20px;
+ border: 2px solid color-mix(in oklab, var(--accent) 35%, var(--line));
+ border-radius: 6px;
margin-top: 1px;
display: grid;
place-items: center;
- transition: background .15s, border-color .15s;
+ transition: background .15s, border-color .15s, transform .15s;
}
-.checklist li.checked .check-box {
+.recipe-check-item:hover .recipe-check-box {
+ border-color: var(--accent);
+}
+.recipe-check-item.checked .recipe-check-box {
background: var(--accent);
border-color: var(--accent);
+ transform: scale(1.08);
}
-.checklist li.checked .check-box::after {
- content: "\2713";
+.recipe-check-item.checked .recipe-check-box::after {
+ content: "\2713 ";
color: #fff;
- font-size: .65rem;
+ font-size: .7rem;
font-weight: 700;
}
-.check-text {
- line-height: 1.55;
+.recipe-check-text {
+ line-height: 1.5;
color: var(--ink-2);
- transition: opacity .15s;
+ transition: opacity .15s, text-decoration .15s;
}
-.checklist li.checked .check-text {
- opacity: .45;
+.recipe-check-item.checked .recipe-check-text {
+ opacity: .4;
text-decoration: line-through;
}
-.step-list {
- counter-reset: step;
+
+/* Method steps */
+.recipe-steps {
list-style: none;
padding: 0;
margin: 0;
+ counter-reset: recipe-step;
}
-.step-list li {
+.recipe-step {
position: relative;
- padding: .5rem 0 .5rem 2.2rem;
- line-height: 1.6;
- color: var(--ink-2);
- border-left: 2px solid color-mix(in oklab, var(--accent) 20%, var(--line));
- margin-left: .6rem;
+ display: flex;
+ align-items: flex-start;
+ gap: .85rem;
+ padding: .6rem 0;
}
-.step-list li::before {
- counter-increment: step;
- content: counter(step);
- position: absolute;
- left: -13px;
- top: .45rem;
- width: 24px; height: 24px;
+.recipe-step-num {
+ flex-shrink: 0;
+ counter-increment: recipe-step;
+ width: 32px; height: 32px;
border-radius: 50%;
- background: var(--accent);
+ background: linear-gradient(135deg, var(--accent), color-mix(in oklab, var(--accent) 75%, #d97706));
color: #fff;
- font-size: .7rem;
- font-weight: 700;
+ font-size: .85rem;
+ font-weight: 800;
display: grid;
place-items: center;
- box-shadow: 0 1px 4px rgba(0,0,0,.12);
+ box-shadow: 0 2px 8px color-mix(in oklab, var(--accent) 30%, transparent);
+}
+.recipe-step-num::before {
+ content: counter(recipe-step);
+}
+.recipe-step-text {
+ line-height: 1.65;
+ color: var(--ink-2);
+ padding-top: .2rem;
+}
+
+/* Dark mode */
+@media (max-width: 600px) {
+ .recipe-hero { height: 260px; }
+ .recipe-overlay { padding: 1rem 1.25rem 1.25rem; }
+ .recipe-title { font-size: 1.5rem; }
+ .recipe-desc { font-size: .82rem; }
+ .recipe-checklist { grid-template-columns: 1fr; }
+ .card.recipe-card .bd { padding: 1.25rem; }
}
/* ── Per-type bespoke: Review ── */
diff --git a/app.js b/app.js
index cb62982..654ff0f 100644
--- a/app.js
+++ b/app.js
@@ -298,23 +298,83 @@
function renderTypeSpecific(view, heroRendered) {
const handled = new Set();
- // ── RECIPE ──
+ // ── RECIPE (recipe-card layout) ──
if (cfg.type === "Recipe") {
- // Stats strip: prep, cook, yield
+ const card = document.querySelector(".card");
+ if (card) card.classList.add("recipe-card");
+
+ const heroMount = document.getElementById("hero-mount");
+ const typeLabel = document.getElementById("type-label");
+
+ // Remove generic hero so we can build our own
+ heroMount.querySelector(".hero")?.remove();
+
+ // ── Hero: food photo with overlay ──
+ const imageData = data["image"];
+ if (imageData) {
+ const hero = document.createElement("div"); hero.className = "recipe-hero";
+ hero.setAttribute("data-prop", "image");
+ hero.setAttribute("title", "Click to change image URL");
+ hero.setAttribute("tabindex", "0");
+
+ const img = document.createElement("img");
+ img.className = "recipe-hero-img";
+ img.src = imageData;
+ img.alt = data["name"] || "Recipe photo";
+ img.loading = "lazy";
+ hero.appendChild(img);
+
+ // Warm gradient scrim
+ const scrim = document.createElement("div"); scrim.className = "recipe-scrim";
+ hero.appendChild(scrim);
+
+ // Overlay content
+ const overlay = document.createElement("div"); overlay.className = "recipe-overlay";
+
+ const badge = document.createElement("span"); badge.className = "recipe-badge";
+ badge.textContent = "\u{1F374}\u{00A0}" + (typeLabel ? typeLabel.textContent : "Recipe");
+ overlay.appendChild(badge);
+
+ const h1 = document.createElement("h1"); h1.className = "recipe-title";
+ h1.textContent = data["name"] || "";
+ overlay.appendChild(h1);
+
+ const desc = data["description"];
+ if (desc) {
+ const descEl = document.createElement("p"); descEl.className = "recipe-desc";
+ descEl.textContent = desc;
+ overlay.appendChild(descEl);
+ handled.add("description");
+ }
+
+ hero.appendChild(overlay);
+ heroMount.appendChild(hero);
+ }
+
+ // Hide normal header since title is in the hero
+ const hd = document.querySelector(".hd");
+ if (hd) hd.classList.add("hidden");
+
+ handled.add("name");
+ handled.add("image");
+
+ // ── Stats strip: prep, cook, yield ──
const stats = [];
const prep = data["prepTime"];
- if (prep) { const d = formatDuration(prep); if (d) stats.push({ label: "Prep", val: d }); }
+ if (prep) { const d = formatDuration(prep); if (d) stats.push({ icon: "\u{23F1}\u{FE0F}", label: "Prep", val: d }); }
const cook = data["cookTime"];
- if (cook) { const d = formatDuration(cook); if (d) stats.push({ label: "Cook", val: d }); }
+ if (cook) { const d = formatDuration(cook); if (d) stats.push({ icon: "\u{1F525}", label: "Cook", val: d }); }
const yield_ = data["recipeYield"];
- if (yield_) stats.push({ label: "Yield", val: yield_ });
+ if (yield_) stats.push({ icon: "\u{1F37D}\u{FE0F}", label: "Yields", val: yield_ });
if (stats.length) {
- const strip = document.createElement("div"); strip.className = "stats-strip";
- stats.forEach((s, i) => {
- if (i > 0) { const sep = document.createElement("span"); sep.className = "stats-sep"; strip.appendChild(sep); }
- const chip = document.createElement("span"); chip.className = "stat-chip";
- chip.innerHTML = "" + s.label + "" + s.val;
+ const strip = document.createElement("div"); strip.className = "recipe-stats";
+ stats.forEach((s) => {
+ const chip = document.createElement("div"); chip.className = "recipe-stat";
+ const iconSpan = document.createElement("span"); iconSpan.className = "recipe-stat-icon"; iconSpan.textContent = s.icon;
+ const textSpan = document.createElement("span"); textSpan.className = "recipe-stat-text";
+ textSpan.innerHTML = "" + s.label + "" + s.val;
+ chip.append(iconSpan, textSpan);
strip.appendChild(chip);
});
view.appendChild(strip);
@@ -323,18 +383,21 @@
if (cook) handled.add("cookTime");
if (yield_) handled.add("recipeYield");
- // Ingredients checklist
+ // ── Ingredients checklist ──
const ingredients = data["recipeIngredient"];
if (ingredients) {
- const items = typeof ingredients === "string" ? ingredients.split(/,(?=[\s])/) : (Array.isArray(ingredients) ? ingredients : [ingredients]);
- const section = document.createElement("div"); section.className = "section-block";
- const h = document.createElement("div"); h.className = "section-label"; h.textContent = "Ingredients";
+ const items = typeof ingredients === "string"
+ ? ingredients.split(/,(?=\s)/)
+ : (Array.isArray(ingredients) ? ingredients : [ingredients]);
+ const section = document.createElement("div"); section.className = "recipe-section";
+ const h = document.createElement("div"); h.className = "recipe-section-title";
+ h.textContent = "\u{1F4E6} Ingredients";
section.appendChild(h);
- const ul = document.createElement("ul"); ul.className = "checklist";
+ const ul = document.createElement("ul"); ul.className = "recipe-checklist";
items.forEach((item) => {
- const li = document.createElement("li");
- const cb = document.createElement("span"); cb.className = "check-box";
- const txt = document.createElement("span"); txt.className = "check-text"; txt.textContent = item.trim();
+ const li = document.createElement("li"); li.className = "recipe-check-item";
+ const cb = document.createElement("span"); cb.className = "recipe-check-box";
+ const txt = document.createElement("span"); txt.className = "recipe-check-text"; txt.textContent = item.trim();
li.append(cb, txt);
li.addEventListener("click", () => li.classList.toggle("checked"));
ul.appendChild(li);
@@ -344,16 +407,23 @@
handled.add("recipeIngredient");
}
- // Instructions numbered list
+ // ── Method: numbered steps ──
const instr = data["recipeInstructions"];
if (instr) {
- const steps = typeof instr === "string" ? instr.split(/\n|\.\s+(?=[A-Z])/).filter(s => s.trim()) : (Array.isArray(instr) ? instr : [instr]);
- const section = document.createElement("div"); section.className = "section-block";
- const h = document.createElement("div"); h.className = "section-label"; h.textContent = "Instructions";
+ const steps = typeof instr === "string"
+ ? instr.split(/\n/).filter((s) => s.trim())
+ : (Array.isArray(instr) ? instr : [instr]);
+ const section = document.createElement("div"); section.className = "recipe-section";
+ const h = document.createElement("div"); h.className = "recipe-section-title";
+ h.textContent = "\u{1F4DD} Method";
section.appendChild(h);
- const ol = document.createElement("ol"); ol.className = "step-list";
+ const ol = document.createElement("ol"); ol.className = "recipe-steps";
steps.forEach((step) => {
- const li = document.createElement("li"); li.textContent = step.trim().replace(/^\d+\.\s*/, "");
+ const li = document.createElement("li"); li.className = "recipe-step";
+ const num = document.createElement("span"); num.className = "recipe-step-num";
+ const txt = document.createElement("span"); txt.className = "recipe-step-text";
+ txt.textContent = step.trim().replace(/^\d+\.\s*/, "");
+ li.append(num, txt);
ol.appendChild(li);
});
section.appendChild(ol);