const { useState, useEffect, useCallback, useRef } = React;

// Inline icon set (subset of Lucide, MIT-licensed paths) — no external dependency.
function Icon({ size = 18, children, className, ...rest }) {
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none"
      stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"
      className={className}
      style={{ flexShrink: 0, display: "inline-block", verticalAlign: "middle" }} {...rest}>
      {children}
    </svg>
  );
}
const Calendar = (p) => <Icon {...p}><rect x="3" y="4" width="18" height="18" rx="2"/><path d="M16 2v4M8 2v4M3 10h18"/></Icon>;
const BookOpen = (p) => <Icon {...p}><path d="M12 7v14M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/></Icon>;
const Sparkles = (p) => <Icon {...p}><path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .962 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.962 0z"/><path d="M20 3v4M22 5h-4M4 17v2M5 18H3"/></Icon>;
const Clock = (p) => <Icon {...p}><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></Icon>;
const Plus = (p) => <Icon {...p}><path d="M12 5v14M5 12h14"/></Icon>;
const X = (p) => <Icon {...p}><path d="M18 6 6 18M6 6l12 12"/></Icon>;
const Trash2 = (p) => <Icon {...p}><path d="M3 6h18M8 6V4a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v2m2 0v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6M10 11v6M14 11v6"/></Icon>;
const ChevronLeft = (p) => <Icon {...p}><path d="m15 18-6-6 6-6"/></Icon>;
const ChevronRight = (p) => <Icon {...p}><path d="m9 18 6-6-6-6"/></Icon>;
const Heart = (p) => <Icon {...p}><path d="M19 14c1.5-1.5 3-3.2 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.8 0-3 .5-4.5 2-1.5-1.5-2.7-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4 3 5.5l7 7z"/></Icon>;
const Loader2 = (p) => <Icon {...p}><path d="M21 12a9 9 0 1 1-6.2-8.6"/></Icon>;
const Check = (p) => <Icon {...p}><path d="M20 6 9 17l-5-5"/></Icon>;
const Search = (p) => <Icon {...p}><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></Icon>;
const Wand2 = (p) => <Icon {...p}><path d="m21.64 3.64-1.28-1.28a1.21 1.21 0 0 0-1.72 0L2.36 18.66a1.21 1.21 0 0 0 0 1.72l1.28 1.28a1.2 1.2 0 0 0 1.72 0L21.64 5.36a1.2 1.2 0 0 0 0-1.72"/><path d="m14 7 3 3"/><path d="M5 6v4M19 14v4M10 2v2M7 8H3M21 16h-4M11 3H9"/></Icon>;
const Camera = (p) => <Icon {...p}><path d="M14.5 4h-5L7 7H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3z"/><circle cx="12" cy="13" r="3.5"/></Icon>;
const Type = (p) => <Icon {...p}><path d="M4 7V5h16v2M9 19h6M12 5v14"/></Icon>;
const ShoppingCart = (p) => <Icon {...p}><circle cx="9" cy="20" r="1.5"/><circle cx="18" cy="20" r="1.5"/><path d="M2 3h2l2.5 13h12L21 7H6"/></Icon>;
const Package = (p) => <Icon {...p}><path d="M21 8 12 3 3 8v8l9 5 9-5z"/><path d="M3 8l9 5 9-5M12 13v8"/></Icon>;
const Printer = (p) => <Icon {...p}><path d="M6 9V2h12v7M6 18H4a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-2M6 14h12v8H6z"/></Icon>;
const ThumbsUp = (p) => <Icon {...p}><path d="M7 10v11M7 10l4-7a2 2 0 0 1 3 2l-1 5h6a2 2 0 0 1 2 2.3l-1.3 7A2 2 0 0 1 17.7 21H7"/></Icon>;
const ThumbsDown = (p) => <Icon {...p}><path d="M17 14V3M17 14l-4 7a2 2 0 0 1-3-2l1-5H5a2 2 0 0 1-2-2.3l1.3-7A2 2 0 0 1 6.3 3H17"/></Icon>;
const Share2 = (p) => <Icon {...p}><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><path d="M8.6 13.5l6.8 4M15.4 6.5l-6.8 4"/></Icon>;
const Download = (p) => <Icon {...p}><path d="M12 3v12m0 0 4-4m-4 4-4-4M5 21h14"/></Icon>;
const Upload = (p) => <Icon {...p}><path d="M12 21V9m0 0 4 4m-4-4-4 4M5 3h14"/></Icon>;


// ---------------------------------------------------------------------------
// Meal Planner & Nutrition — a warm, whole-food weekly planning app
// Data persists via localStorage (per-device). Backup/restore moves it between devices.
// ---------------------------------------------------------------------------

const MEALS = ["Breakfast", "Lunch", "Dinner"];
const DAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
const DAY_SHORT = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];

// ---- ingredient model -------------------------------------------------------
// Ingredients may be either legacy plain strings ("2 cups rice") or structured
// objects { qty: number|null, unit: string, item: string }. These helpers let
// the rest of the app treat both uniformly.
function isStructured(ing) {
  return ing && typeof ing === "object" && "item" in ing;
}
// The display text for one ingredient line, scaled if a factor is given.
function ingredientText(ing, factor = 1) {
  if (!isStructured(ing)) return String(ing || "").trim(); // legacy: never scale text
  const { qty, unit, item } = ing;
  if (qty == null || qty === "") return item;
  const scaled = qty * factor;
  const rounded = Math.round(scaled * 100) / 100;
  const qtyStr = Number.isInteger(rounded) ? String(rounded) : String(rounded);
  return [qtyStr, unit, item].filter(Boolean).join(" ").trim();
}
// The bare item name (for grocery matching/categorizing).
function ingredientItem(ing) {
  return isStructured(ing) ? (ing.item || "").trim() : String(ing || "").trim();
}
// Whether this meal's ingredients support quantity scaling (all structured w/ qty).
function canScaleQuantities(meal) {
  return Array.isArray(meal?.ingredients) && meal.ingredients.length > 0 &&
    meal.ingredients.every((i) => isStructured(i) && i.qty != null && i.qty !== "");
}

// Return any avoided ingredients this meal contains (case-insensitive substring match).
function avoidedIn(meal, avoidList) {
  if (!avoidList?.length || !meal?.ingredients?.length) return [];
  const items = meal.ingredients.map((i) => ingredientItem(i).toLowerCase());
  const name = (meal.name || "").toLowerCase();
  return avoidList.filter((a) => {
    const t = a.toLowerCase().trim();
    if (!t) return false;
    return name.includes(t) || items.some((it) => it.includes(t));
  });
}

// ---- meal photo -------------------------------------------------------------
// Real uploaded photos only — no generated/illustrated art. When a meal has no
// photo, layouts simply omit the image and stand on typography.
function hasPhoto(meal) { return !!meal?.photo; }

// Meal speed/type tags
const SPEED_OPTIONS = [
  { value: "quick", label: "Quick", short: "Quick", icon: <Clock size={14} />, hint: "30 min or less" },
  { value: "premade", label: "Premade", short: "Premade", icon: <Package size={14} />, hint: "Costco, frozen, takeout" },
  { value: "standard", label: "Standard", short: "Standard", icon: null, hint: "Regular cooking" },
];
const speedInfo = (value) => SPEED_OPTIONS.find((s) => s.value === value) || SPEED_OPTIONS[2];

// Small icon badge shown on planner slots and library cards
function SpeedBadge({ speed, withLabel }) {
  const info = speedInfo(speed);
  if (!info.icon) return null; // standard = no badge
  return (
    <span className={`speed-badge speed-${speed}`} title={`${info.label} · ${info.hint}`}>
      {info.icon}{withLabel && <span>{info.short}</span>}
    </span>
  );
}

// Thumbs up/down rating control. size: "sm" | "md"
function RatingControl({ rating, onRate, size = "md" }) {
  const px = size === "sm" ? 15 : 18;
  return (
    <div className={`rating ${size}`} onClick={(e) => e.stopPropagation()}>
      <button
        className={`rate-btn up ${rating === "up" ? "on" : ""}`}
        onClick={() => onRate("up")}
        title="We liked this"
        aria-label="Thumbs up"
      >
        <ThumbsUp size={px} />
      </button>
      <button
        className={`rate-btn down ${rating === "down" ? "on" : ""}`}
        onClick={() => onRate("down")}
        title="Not a repeat"
        aria-label="Thumbs down"
      >
        <ThumbsDown size={px} />
      </button>
    </div>
  );
}

// ---- storage helpers -------------------------------------------------------
const STORAGE_PREFIX = 'familytable:';
const store = {
  async get(key, fallback) {
    try {
      const raw = localStorage.getItem(STORAGE_PREFIX + key);
      return raw != null ? JSON.parse(raw) : fallback;
    } catch { return fallback; }
  },
  async set(key, value) {
    try { localStorage.setItem(STORAGE_PREFIX + key, JSON.stringify(value)); }
    catch (e) { console.error('storage set failed', e); }
  },
};
// Export/import the whole dataset for backup & moving between devices.
async function exportAllData() {
  const keys = ['mp:plans','mp:library','mp:grocery','mp:mealsShown','mp:household','mp:avoidList'];
  const data = {};
  for (const k of keys) data[k] = await store.get(k, null);
  return JSON.stringify({ app: 'family-table', version: 1, exportedAt: new Date().toISOString(), data }, null, 2);
}
async function importAllData(json) {
  const parsed = JSON.parse(json);
  const data = parsed && parsed.data ? parsed.data : parsed;
  for (const k of Object.keys(data)) {
    if (data[k] != null) await store.set(k, data[k]);
  }
}

// Monday-anchored week key, e.g. "2026-W21"
function weekKeyFor(date) {
  const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()));
  const day = d.getUTCDay() || 7;
  d.setUTCDate(d.getUTCDate() + 4 - day);
  const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1));
  const weekNo = Math.ceil(((d - yearStart) / 86400000 + 1) / 7);
  return `${d.getUTCFullYear()}-W${String(weekNo).padStart(2, "0")}`;
}

function mondayOf(date) {
  const d = new Date(date);
  const day = d.getDay() || 7;
  if (day !== 1) d.setDate(d.getDate() - (day - 1));
  d.setHours(0, 0, 0, 0);
  return d;
}

function fmtRange(monday) {
  const end = new Date(monday);
  end.setDate(end.getDate() + 6);
  const o = { month: "short", day: "numeric" };
  return `${monday.toLocaleDateString(undefined, o)} – ${end.toLocaleDateString(undefined, o)}`;
}

// Reverse a "YYYY-Www" ISO-week key into the Monday date of that week (local).
function mondayFromWeekKey(key) {
  const m = /^(\d{4})-W(\d{2})$/.exec(key);
  if (!m) return null;
  const year = Number(m[1]);
  const week = Number(m[2]);
  // ISO: week 1 contains Jan 4th; find that week's Monday, then add weeks.
  const jan4 = new Date(year, 0, 4);
  const jan4Day = jan4.getDay() || 7; // Mon=1..Sun=7
  const week1Monday = new Date(jan4);
  week1Monday.setDate(jan4.getDate() - (jan4Day - 1));
  const monday = new Date(week1Monday);
  monday.setDate(week1Monday.getDate() + (week - 1) * 7);
  monday.setHours(0, 0, 0, 0);
  return monday;
}

const uid = () => Math.random().toString(36).slice(2, 10);

// ---- nutrition badge -------------------------------------------------------
function MacroPill({ label, value, unit, tone }) {
  const tones = {
    cal: "var(--terra)",
    protein: "var(--sage-deep)",
    carbs: "var(--honey)",
    fat: "var(--clay)",
  };
  return (
    <span className="macro-pill" style={{ "--pill": tones[tone] }}>
      <strong>{value}{unit}</strong>
      <span>{label}</span>
    </span>
  );
}

function NutritionRow({ n, compact }) {
  if (!n) return null;
  return (
    <div className={`nutri-row ${compact ? "compact" : ""}`}>
      <MacroPill label="cal" value={n.calories ?? "–"} unit="" tone="cal" />
      <MacroPill label="protein" value={n.protein ?? "–"} unit="g" tone="protein" />
      <MacroPill label="carbs" value={n.carbs ?? "–"} unit="g" tone="carbs" />
      <MacroPill label="fat" value={n.fat ?? "–"} unit="g" tone="fat" />
    </div>
  );
}

// ---- AI meal idea generator (uses Claude) ----------------------------------
async function generateMeals({ craving, mealType, restrictions, avoid }) {
  const prompt = `You are a warm, practical home-cooking assistant helping plan family-friendly, whole-food meals.

Generate 4 meal ideas for: ${mealType || "any meal"}.
${craving ? `The person is in the mood for / has on hand: ${craving}.` : ""}
${restrictions ? `Preferences/targets: ${restrictions}.` : ""}
${avoid?.length ? `Generally avoid these ingredients UNLESS the person's request above explicitly asks for one of them: ${avoid.join(", ")}.` : ""}

Favor real, whole ingredients and approachable family cooking. For each meal include a realistic per-serving nutrition ESTIMATE, structured ingredients, simple step-by-step instructions, and times.

Respond ONLY with a JSON array, no preamble, no markdown fences. Each object:
{
  "name": "short meal name",
  "description": "one appetizing sentence",
  "serves": int,
  "prepTime": int (minutes),
  "cookTime": int (minutes),
  "ingredients": [{ "qty": number or null, "unit": "cup/tbsp/clove/lb/oz/etc or empty string", "item": "ingredient name" }, ...],
  "instructions": ["step 1", "step 2", ...],
  "nutrition": { "calories": int, "protein": int, "carbs": int, "fat": int }
}`;

  const res = await fetch("/api/claude", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      model: "claude-sonnet-4-6",
      max_tokens: 2000,
      messages: [{ role: "user", content: prompt }],
    }),
  });
  const data = await res.json();
  const text = data.content
    .filter((b) => b.type === "text")
    .map((b) => b.text)
    .join("")
    .replace(/```json|```/g, "")
    .trim();
  return JSON.parse(text);
}

// ---- AI single-meal generator for one planner slot (uses Claude) -----------
async function generateOneMeal({ craving, mealType, avoid }) {
  const prompt = `You are a warm, practical home-cooking assistant. Suggest ONE family-friendly, whole-food ${mealType || "meal"}.
${craving ? `The person is in the mood for / has on hand: ${craving}.` : ""}
${avoid?.length ? `Generally avoid these ingredients UNLESS the request above explicitly asks for one: ${avoid.join(", ")}.` : ""}

Include a realistic per-serving nutrition ESTIMATE, structured ingredients, step-by-step instructions, and times.

Respond ONLY with a JSON object, no preamble, no markdown fences:
{
  "name": "short meal name",
  "description": "one appetizing sentence",
  "serves": int,
  "prepTime": int (minutes),
  "cookTime": int (minutes),
  "ingredients": [{ "qty": number or null, "unit": "cup/tbsp/clove/lb/oz/etc or empty string", "item": "ingredient name" }, ...],
  "instructions": ["step 1", "step 2", ...],
  "nutrition": { "calories": int, "protein": int, "carbs": int, "fat": int }
}`;

  const res = await fetch("/api/claude", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      model: "claude-sonnet-4-6",
      max_tokens: 1500,
      messages: [{ role: "user", content: prompt }],
    }),
  });
  const data = await res.json();
  const text = data.content
    .filter((b) => b.type === "text")
    .map((b) => b.text)
    .join("")
    .replace(/```json|```/g, "")
    .trim();
  return JSON.parse(text);
}
async function generateWeek({ mealTypes, proteins, onHand, cuisines, target, avoid }) {
  // Generate the week in small chunks (run concurrently) so each call finishes
  // well under the host's function time limit. We split the 7 days into batches.
  const DAY_NAMES = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
  const chunks = [
    DAY_NAMES.slice(0, 3), // Mon–Wed
    DAY_NAMES.slice(3, 5), // Thu–Fri
    DAY_NAMES.slice(5, 7), // Sat–Sun
  ];

  const guidance = [
    proteins.length ? `- Use these proteins as a rough guide across the week (not every meal): ${proteins.join(", ")}. Other proteins welcome too.` : "- Vary the main proteins.",
    onHand ? `- Try to work in these on-hand ingredients where natural: ${onHand}.` : "",
    cuisines.length ? `- Treat these cuisines as light inspiration, not a rule; keep variety: ${cuisines.join(", ")}.` : "",
    target ? `- Aim roughly for: ${target}.` : "",
    avoid?.length ? `- Generally avoid these unless on-hand includes one: ${avoid.join(", ")}.` : "",
    "- Favor real, whole ingredients and approachable family cooking. Keep dishes varied day to day. Breakfasts can be simpler.",
  ].filter(Boolean).join("\n");

  const fetchChunk = async (days) => {
    const prompt = `You are a warm, practical home-cooking assistant planning family-friendly, whole-food meals.

Plan meals for these days: ${days.join(", ")}. For each day include these meal types: ${mealTypes.join(", ")}.

Guidance:
${guidance}

Respond ONLY with a JSON object, no preamble, no markdown fences. Keep each meal LIGHT. Shape:
{
${days.map((d) => `  "${d}": { ${mealTypes.map((m) => `"${m}": {"name": "...", "description": "one short sentence", "serves": int, "ingredients": ["item", "item"], "nutrition": {"calories": int, "protein": int, "carbs": int, "fat": int}}`).join(", ")} }`).join(",\n")}
}
Short ingredient lists (main items only, no quantities). No cooking steps or prep times.`;

    const res = await fetch("/api/claude", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        model: "claude-sonnet-4-6",
        max_tokens: 1500,
        messages: [{ role: "user", content: prompt }],
      }),
    });
    const data = await res.json();
    let text = (data.content || [])
      .filter((b) => b.type === "text")
      .map((b) => b.text)
      .join("")
      .replace(/```json|```/g, "")
      .trim();
    // Be tolerant: if the model added any stray prose, grab the outermost JSON object.
    if (text && text[0] !== "{") {
      const first = text.indexOf("{");
      const last = text.lastIndexOf("}");
      if (first !== -1 && last !== -1 && last > first) text = text.slice(first, last + 1);
    }
    try {
      return JSON.parse(text);
    } catch (e) {
      return {}; // a bad chunk shouldn't kill the whole week
    }
  };

  // Run all chunks concurrently; tolerate any single chunk failing.
  const settled = await Promise.allSettled(chunks.map((days) => fetchChunk(days)));
  const merged = Object.assign({}, ...settled.map((s) => (s.status === "fulfilled" ? s.value : {})));
  if (Object.keys(merged).length === 0) {
    throw new Error("No days could be generated");
  }
  return merged;
}

const PROTEIN_OPTIONS = ["Chicken", "Beef", "Pork", "Fish", "Eggs", "Beans / legumes", "Turkey"];
const CUISINE_OPTIONS = ["American comfort", "Italian", "Mexican", "Mediterranean", "Asian", "Indian", "BBQ / smoked", "Southern"];

// ---- recipe extraction (uses Claude) ---------------------------------------
const EXTRACT_INSTRUCTION = `Extract this recipe into a single meal entry. Estimate per-serving nutrition realistically. Capture the real ingredient quantities and the cooking steps.

Respond ONLY with a JSON object, no preamble, no markdown fences:
{
  "name": "short meal name",
  "description": "one appetizing sentence",
  "serves": int,
  "prepTime": int (minutes),
  "cookTime": int (minutes),
  "ingredients": [{ "qty": number or null, "unit": "cup/tbsp/clove/lb/oz/etc or empty string", "item": "ingredient name" }, ...],
  "instructions": ["step 1", "step 2", ...],
  "nutrition": { "calories": int, "protein": int, "carbs": int, "fat": int }
}
Use the recipe's actual quantities and steps. If something isn't present, make a sensible estimate or use null/empty.`;

function parseMealJSON(data) {
  let text = ((data && data.content) || [])
    .filter((b) => b.type === "text")
    .map((b) => b.text)
    .join("")
    .replace(/```json|```/g, "")
    .trim();
  // Tolerate stray prose: slice to the outermost JSON object.
  if (text && text[0] !== "{") {
    const first = text.indexOf("{");
    if (first !== -1) text = text.slice(first);
  }
  const lastBrace = text.lastIndexOf("}");
  if (lastBrace !== -1) text = text.slice(0, lastBrace + 1);
  return JSON.parse(text);
}

async function extractRecipeFromImage(base64, mediaType) {
  const res = await fetch("/api/claude", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      model: "claude-sonnet-4-6",
      max_tokens: 4000,
      messages: [
        {
          role: "user",
          content: [
            { type: "image", source: { type: "base64", media_type: mediaType, data: base64 } },
            { type: "text", text: EXTRACT_INSTRUCTION },
          ],
        },
      ],
    }),
  });
  return parseMealJSON(await res.json());
}

async function extractRecipeFromText(recipeText) {
  const res = await fetch("/api/claude", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      model: "claude-sonnet-4-6",
      max_tokens: 4000,
      messages: [
        { role: "user", content: `${EXTRACT_INSTRUCTION}\n\nRecipe:\n${recipeText}` },
      ],
    }),
  });
  return parseMealJSON(await res.json());
}

// ===========================================================================
function MealPlanner() {
  const [tab, setTab] = useState("plan");
  const [loaded, setLoaded] = useState(false);

  const [weekDate, setWeekDate] = useState(() => mondayOf(new Date()));
  const [plans, setPlans] = useState({}); // { weekKey: { "Monday-Dinner": mealId } }
  const [library, setLibrary] = useState([]); // saved meals
  const [grocery, setGrocery] = useState({}); // { weekKey: { checked: {item:true}, custom: [{id,name}] } }
  const [mealsShown, setMealsShown] = useState(["Dinner"]); // which meal types are shown by default
  const [household, setHousehold] = useState(4); // number of servings she cooks for
  const [avoidList, setAvoidList] = useState([]); // ingredients to generally avoid in AI suggestions

  const weekKey = weekKeyFor(weekDate);
  const currentPlan = plans[weekKey] || {};
  const currentGrocery = grocery[weekKey] || { checked: {}, custom: [] };

  // ---- load on mount ----
  useEffect(() => {
    (async () => {
      const [p, l, g, ms, hh, av] = await Promise.all([
        store.get("mp:plans", {}),
        store.get("mp:library", []),
        store.get("mp:grocery", {}),
        store.get("mp:mealsShown", ["Dinner"]),
        store.get("mp:household", 4),
        store.get("mp:avoidList", []),
      ]);
      setPlans(p);
      setLibrary(l);
      setGrocery(g);
      setMealsShown(ms);
      setHousehold(hh);
      setAvoidList(av);
      setLoaded(true);
    })();
  }, []);

  // ---- persist ----
  useEffect(() => { if (loaded) store.set("mp:plans", plans); }, [plans, loaded]);
  useEffect(() => { if (loaded) store.set("mp:library", library); }, [library, loaded]);
  useEffect(() => { if (loaded) store.set("mp:grocery", grocery); }, [grocery, loaded]);
  useEffect(() => { if (loaded) store.set("mp:mealsShown", mealsShown); }, [mealsShown, loaded]);
  useEffect(() => { if (loaded) store.set("mp:household", household); }, [household, loaded]);
  useEffect(() => { if (loaded) store.set("mp:avoidList", avoidList); }, [avoidList, loaded]);

  const getMeal = useCallback((id) => library.find((m) => m.id === id), [library]);

  // ---- grocery handlers (scoped to current week) ----
  const setWeekGrocery = (updater) => {
    setGrocery((prev) => {
      const cur = prev[weekKey] || { checked: {}, custom: [] };
      return { ...prev, [weekKey]: updater(cur) };
    });
  };
  const toggleGroceryItem = (key) =>
    setWeekGrocery((cur) => ({ ...cur, checked: { ...cur.checked, [key]: !cur.checked[key] } }));
  // Toggle several source keys together (used by consolidated aisle entries).
  // If any are currently unchecked, check them all; otherwise uncheck all.
  const toggleGroceryKeys = (keys) =>
    setWeekGrocery((cur) => {
      const anyUnchecked = keys.some((k) => !cur.checked[k]);
      const next = { ...cur.checked };
      keys.forEach((k) => { next[k] = anyUnchecked; });
      return { ...cur, checked: next };
    });
  const addCustomGrocery = (name) =>
    setWeekGrocery((cur) => ({ ...cur, custom: [...cur.custom, { id: uid(), name }] }));
  const removeCustomGrocery = (id) =>
    setWeekGrocery((cur) => ({ ...cur, custom: cur.custom.filter((c) => c.id !== id) }));
  const clearCheckedGrocery = () =>
    setWeekGrocery((cur) => ({ ...cur, checked: {} }));

  // ---- assignment dialog state ----
  const [assigning, setAssigning] = useState(null); // {day, mealType}
  const [autoPlanning, setAutoPlanning] = useState(false);
  const [viewingMealId, setViewingMealId] = useState(null);
  const [editingMeal, setEditingMeal] = useState(null); // meal object being edited
  const [publication, setPublication] = useState(null); // { title, meals:[{slot?, meal}] } or null
  const viewingMeal = viewingMealId ? getMeal(viewingMealId) : null;

  // Open the magazine/publication view for a set of meals
  const openPublication = (title, mealsWithSlots) => {
    const meals = mealsWithSlots.filter((x) => x.meal);
    if (meals.length === 0) return;
    setPublication({ title, meals });
  };
  // Build the meal list for a given week's plan (by weekKey), day/meal ordered
  const mealsForWeek = (wkPlan) => {
    const out = [];
    DAYS.forEach((day) => MEALS.forEach((mt) => {
      const id = wkPlan[`${day}-${mt}`];
      if (id && getMeal(id)) out.push({ slot: `${day} · ${mt}`, meal: getMeal(id) });
    }));
    return out;
  };

  // update an existing library meal in place (from the edit form)
  const updateMeal = (id, fields) => {
    setLibrary((prev) => prev.map((m) => m.id === id ? { ...m, ...fields } : m));
  };

  // Attach a photo to a meal, downscaled to keep storage reasonable.
  const setMealPhoto = (id, file) => {
    if (!file || !file.type.startsWith("image/")) return;
    const reader = new FileReader();
    reader.onload = () => {
      const img = new Image();
      img.onload = () => {
        const maxW = 1000;
        const scale = Math.min(1, maxW / img.width);
        const canvas = document.createElement("canvas");
        canvas.width = Math.round(img.width * scale);
        canvas.height = Math.round(img.height * scale);
        const ctx = canvas.getContext("2d");
        ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
        const dataUrl = canvas.toDataURL("image/jpeg", 0.82);
        updateMeal(id, { photo: dataUrl });
      };
      img.src = reader.result;
    };
    reader.readAsDataURL(file);
  };
  const clearMealPhoto = (id) => updateMeal(id, { photo: null });

  // Flesh out a light (week-generated) meal into a full recipe: steps + structured ingredients.
  const fleshOutRecipe = async (meal) => {
    const full = await generateOneMeal({ craving: meal.name, mealType: "meal", avoid: avoidList });
    updateMeal(meal.id, {
      ingredients: full.ingredients?.length ? full.ingredients : meal.ingredients,
      instructions: full.instructions || [],
      prepTime: full.prepTime ?? meal.prepTime ?? null,
      cookTime: full.cookTime ?? meal.cookTime ?? null,
      serves: meal.serves ?? full.serves ?? null,
      // keep existing name, description, rating, photo, nutrition unless missing
      nutrition: meal.nutrition || full.nutrition,
    });
  };

  // Fill an entire week from generated data; saves meals to library, assigns slots
  const applyGeneratedWeek = (weekData, mealTypes) => {
    const newLibraryMeals = [];
    const newSlots = {};
    DAYS.forEach((day) => {
      const dayData = weekData[day];
      if (!dayData) return;
      mealTypes.forEach((mt) => {
        const meal = dayData[mt];
        if (!meal || !meal.name) return;
        const m = {
          ...meal,
          ingredients: meal.ingredients || [],
          aiGenerated: true,
          id: uid(),
          createdAt: Date.now(),
        };
        newLibraryMeals.push(m);
        newSlots[`${day}-${mt}`] = m.id;
      });
    });
    setLibrary((prev) => [...newLibraryMeals, ...prev]);
    setPlans((prev) => ({
      ...prev,
      [weekKey]: { ...(prev[weekKey] || {}), ...newSlots },
    }));
  };

  const assignMeal = (mealId) => {
    if (!assigning) return;
    const slot = `${assigning.day}-${assigning.mealType}`;
    setPlans((prev) => ({
      ...prev,
      [weekKey]: { ...(prev[weekKey] || {}), [slot]: mealId },
    }));
    setAssigning(null);
  };

  const clearSlot = (day, mealType) => {
    const slot = `${day}-${mealType}`;
    setPlans((prev) => {
      const wk = { ...(prev[weekKey] || {}) };
      delete wk[slot];
      return { ...prev, [weekKey]: wk };
    });
  };

  // Generate one AI meal directly into a given slot. Saves to library and assigns it.
  const generateIntoSlot = async (day, mealType, craving) => {
    const meal = await generateOneMeal({ craving, mealType, avoid: avoidList });
    const m = { ...meal, ingredients: meal.ingredients || [], aiGenerated: true, id: uid(), createdAt: Date.now() };
    setLibrary((prev) => [m, ...prev]);
    const slot = `${day}-${mealType}`;
    setPlans((prev) => ({
      ...prev,
      [weekKey]: { ...(prev[weekKey] || {}), [slot]: m.id },
    }));
    return m;
  };

  const addToLibrary = (meal) => {
    const m = { ...meal, id: uid(), createdAt: Date.now() };
    setLibrary((prev) => [m, ...prev]);
    return m;
  };

  const removeFromLibrary = (id) => {
    setLibrary((prev) => prev.filter((m) => m.id !== id));
  };

  // Set a meal's rating: "up", "down", or null to clear. Toggles off if same value re-tapped.
  const rateMeal = (id, rating) => {
    setLibrary((prev) => prev.map((m) =>
      m.id === id ? { ...m, rating: m.rating === rating ? null : rating } : m
    ));
  };

  // ---- week nutrition rollup ----
  const dayTotals = (day) => {
    const t = { calories: 0, protein: 0, carbs: 0, fat: 0 };
    MEALS.forEach((mt) => {
      const meal = getMeal(currentPlan[`${day}-${mt}`]);
      if (meal?.nutrition) {
        t.calories += meal.nutrition.calories || 0;
        t.protein += meal.nutrition.protein || 0;
        t.carbs += meal.nutrition.carbs || 0;
        t.fat += meal.nutrition.fat || 0;
      }
    });
    return t;
  };

  if (!loaded) {
    return (
      <div className="mp-root loading">
        <style>{CSS}</style>
        <Loader2 className="spin" size={32} />
        <p>Setting the table…</p>
      </div>
    );
  }

  return (
    <div className="mp-root">
      <style>{CSS}</style>

      <header className="mp-header">
        <div className="header-glow" />
        <div className="brand">
          <div className="brand-mark">
            <svg viewBox="0 0 40 40" width="30" height="30" fill="none" aria-hidden="true">
              <path d="M20 33C20 33 8 27 8 16C8 16 14 14 20 19C26 14 32 16 32 16C32 27 20 33 20 33Z" fill="var(--sage)" fillOpacity="0.92"/>
              <path d="M20 33V14" stroke="#fffefa" strokeWidth="1.6" strokeLinecap="round"/>
              <path d="M20 24C20 24 16 22 14 19M20 21C20 21 24 19 26 17" stroke="#fffefa" strokeWidth="1.3" strokeLinecap="round"/>
              <circle cx="20" cy="11" r="2.4" fill="var(--honey)"/>
            </svg>
          </div>
          <div className="brand-text">
            <span className="brand-eyebrow">Kitchen & Pantry</span>
            <h1>The Family Table</h1>
            <p>Plan the week · know what's in it</p>
          </div>
        </div>
        <nav className="tabs">
          <button className={tab === "plan" ? "active" : ""} onClick={() => setTab("plan")}>
            <Calendar size={16} /> Planner
          </button>
          <button className={tab === "ideas" ? "active" : ""} onClick={() => setTab("ideas")}>
            <Sparkles size={16} /> Ideas
          </button>
          <button className={tab === "library" ? "active" : ""} onClick={() => setTab("library")}>
            <BookOpen size={16} /> Library
          </button>
          <button className={tab === "grocery" ? "active" : ""} onClick={() => setTab("grocery")}>
            <ShoppingCart size={16} /> Grocery
          </button>
          <button className={tab === "history" ? "active" : ""} onClick={() => setTab("history")}>
            <Clock size={16} /> History
          </button>
        </nav>
      </header>

      <main className="mp-main">
        {tab === "plan" && (
          <PlannerView
            weekDate={weekDate}
            setWeekDate={setWeekDate}
            currentPlan={currentPlan}
            getMeal={getMeal}
            onAssign={(day, mealType) => setAssigning({ day, mealType })}
            onClear={clearSlot}
            dayTotals={dayTotals}
            onAutoPlan={() => setAutoPlanning(true)}
            mealsShown={mealsShown}
            setMealsShown={setMealsShown}
            household={household}
            setHousehold={setHousehold}
            onRate={rateMeal}
            onOpenRecipe={setViewingMealId}
            avoidList={avoidList}
            setAvoidList={setAvoidList}
            onGenerateSlot={generateIntoSlot}
            onPublish={() => openPublication(`The Family Table · ${fmtRange(weekDate)}`, mealsForWeek(currentPlan))}
          />
        )}
        {tab === "ideas" && (
          <IdeasView
            onSave={addToLibrary}
            library={library}
            avoidList={avoidList}
          />
        )}
        {tab === "library" && (
          <LibraryView
            library={library}
            onAdd={addToLibrary}
            onRemove={removeFromLibrary}
            onRate={rateMeal}
            onOpenRecipe={setViewingMealId}
          />
        )}
        {tab === "grocery" && (
          <GroceryView
            weekRange={fmtRange(weekDate)}
            currentPlan={currentPlan}
            getMeal={getMeal}
            grocery={currentGrocery}
            onToggle={toggleGroceryItem}
            onToggleKeys={toggleGroceryKeys}
            onAddCustom={addCustomGrocery}
            onRemoveCustom={removeCustomGrocery}
            onClearChecked={clearCheckedGrocery}
            onGoPlan={() => setTab("plan")}
            household={household}
          />
        )}
        {tab === "history" && (
          <HistoryView
            plans={plans}
            getMeal={getMeal}
            onView={(monday) => { setWeekDate(monday); setTab("plan"); }}
            onPublish={(label, wkPlan) => openPublication(`The Family Table · ${label}`, mealsForWeek(wkPlan))}
          />
        )}
      </main>

      {publication && (
        <PublicationView
          title={publication.title}
          meals={publication.meals}
          household={household}
          onClose={() => setPublication(null)}
        />
      )}

      {viewingMeal && !editingMeal && (
        <RecipeView
          meal={viewingMeal}
          household={household}
          avoidList={avoidList}
          onClose={() => setViewingMealId(null)}
          onRate={rateMeal}
          onEdit={() => setEditingMeal(viewingMeal)}
          onSetPhoto={setMealPhoto}
          onClearPhoto={clearMealPhoto}
          onFleshOut={fleshOutRecipe}
          onPublish={() => openPublication(viewingMeal.name, [{ meal: viewingMeal }])}
        />
      )}
      {editingMeal && (
        <MealForm
          initial={editingMeal}
          onClose={() => setEditingMeal(null)}
          onSave={(fields) => { updateMeal(editingMeal.id, fields); setEditingMeal(null); }}
        />
      )}

      {autoPlanning && (
        <AutoPlanDialog
          weekRange={fmtRange(weekDate)}
          existingCount={Object.keys(currentPlan).length}
          avoidList={avoidList}
          onClose={() => setAutoPlanning(false)}
          onApply={(weekData, mealTypes) => {
            applyGeneratedWeek(weekData, mealTypes);
            setAutoPlanning(false);
          }}
        />
      )}

      {assigning && (
        <AssignDialog
          slot={assigning}
          library={library}
          avoidList={avoidList}
          onPick={assignMeal}
          onClose={() => setAssigning(null)}
          onCreateQuick={(meal) => {
            const m = addToLibrary(meal);
            assignMeal(m.id);
          }}
        />
      )}
    </div>
  );
}

// ---------------------------------------------------------------------------
function AvoidEditor({ avoidList, setAvoidList }) {
  const [open, setOpen] = useState(false);
  const [text, setText] = useState("");
  const add = () => {
    const v = text.trim();
    if (!v) return;
    if (!avoidList.some((a) => a.toLowerCase() === v.toLowerCase())) {
      setAvoidList([...avoidList, v]);
    }
    setText("");
  };
  const remove = (item) => setAvoidList(avoidList.filter((a) => a !== item));

  return (
    <div className="avoid-wrap">
      <button className="avoid-trigger" onClick={() => setOpen((o) => !o)} title="Ingredients to generally avoid in suggestions">
        Avoid{avoidList.length > 0 ? ` (${avoidList.length})` : ""}
      </button>
      {open && (
        <>
          <div className="avoid-backdrop" onClick={() => setOpen(false)} />
          <div className="avoid-pop">
            <div className="avoid-pop-head">
              <strong>Generally avoid</strong>
              <button className="ghost-btn sm" onClick={() => setOpen(false)}><X size={15} /></button>
            </div>
            <p className="avoid-hint">Suggestions will skip these — unless you specifically ask for one.</p>
            <div className="avoid-add">
              <input
                value={text}
                onChange={(e) => setText(e.target.value)}
                onKeyDown={(e) => e.key === "Enter" && add()}
                placeholder="e.g. shrimp, olives…"
                autoFocus
              />
              <button className="primary-btn small" onClick={add}>Add</button>
            </div>
            {avoidList.length > 0 ? (
              <div className="avoid-chips">
                {avoidList.map((a) => (
                  <span key={a} className="avoid-chip">
                    {a}
                    <button onClick={() => remove(a)}><X size={12} /></button>
                  </span>
                ))}
              </div>
            ) : (
              <p className="avoid-empty">Nothing yet. Add anything the family would rather skip.</p>
            )}
          </div>
        </>
      )}
    </div>
  );
}

// ---------------------------------------------------------------------------
function SlotEmpty({ mealType, onAdd, onGenerate }) {
  const [prompting, setPrompting] = useState(false);
  const [craving, setCraving] = useState("");
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(false);

  const run = async () => {
    setLoading(true);
    setError(false);
    try {
      await onGenerate(craving.trim());
      // slot fills; this component unmounts on success
    } catch (e) {
      setError(true);
      setLoading(false);
    }
  };

  if (loading) {
    return (
      <div className="slot-generating">
        <Loader2 size={16} className="spin" /> Cooking up a {mealType.toLowerCase()}…
      </div>
    );
  }

  if (prompting) {
    return (
      <div className="slot-prompt" onClick={(e) => e.stopPropagation()}>
        <input
          value={craving}
          onChange={(e) => setCraving(e.target.value)}
          onKeyDown={(e) => e.key === "Enter" && run()}
          placeholder="In the mood for…? (optional)"
          autoFocus
        />
        <div className="slot-prompt-row">
          <button className="primary-btn small" onClick={run}><Sparkles size={14} /> Generate</button>
          <button className="text-btn" onClick={() => setPrompting(false)}>Cancel</button>
        </div>
        {error && <span className="slot-err">Didn't work — try again.</span>}
      </div>
    );
  }

  return (
    <div className="slot-empty-actions">
      <button className="slot-add" onClick={onAdd}>
        <Plus size={15} /> Add {mealType.toLowerCase()}
      </button>
      <button className="slot-surprise-icon" onClick={() => setPrompting(true)} title="Generate a meal with AI" aria-label="Surprise me">
        <Sparkles size={16} />
      </button>
    </div>
  );
}

// ---------------------------------------------------------------------------
function PlannerView({ weekDate, setWeekDate, currentPlan, getMeal, onAssign, onClear, dayTotals, onAutoPlan, mealsShown, setMealsShown, household, setHousehold, onRate, onOpenRecipe, avoidList, setAvoidList, onGenerateSlot, onPublish }) {
  const shift = (n) => {
    const d = new Date(weekDate);
    d.setDate(d.getDate() + n * 7);
    setWeekDate(mondayOf(d));
  };
  const isThisWeek = weekKeyFor(weekDate) === weekKeyFor(mondayOf(new Date()));
  const hasMeals = Object.keys(currentPlan).length > 0;
  const todayIdx = (new Date().getDay() + 6) % 7; // Mon=0 … Sun=6
  const todayName = DAYS[todayIdx];
  // A day is "past" if it's this week and before today, or any week before this one
  const weekIsPast = weekDate < mondayOf(new Date());
  const dayIsPast = (di) => weekIsPast || (isThisWeek && di < todayIdx);

  // Presets for the meal-visibility setting
  const PRESETS = [
    { label: "Dinner only", meals: ["Dinner"] },
    { label: "Lunch + Dinner", meals: ["Lunch", "Dinner"] },
    { label: "All meals", meals: ["Breakfast", "Lunch", "Dinner"] },
  ];
  const activePreset = PRESETS.find((p) => p.meals.length === mealsShown.length && p.meals.every((m) => mealsShown.includes(m)));

  // For a given day, which meal rows to render: the globally-shown set,
  // plus any meal that already has something planned that day (one-offs).
  const rowsForDay = (day) => {
    const set = new Set(mealsShown);
    MEALS.forEach((mt) => { if (currentPlan[`${day}-${mt}`]) set.add(mt); });
    return MEALS.filter((mt) => set.has(mt)); // keep canonical B/L/D order
  };
  // meals not shown for a day → offerable as a quiet "+ add"
  const addableForDay = (day) => MEALS.filter((mt) => !rowsForDay(day).includes(mt));

  return (
    <section className="planner">
      <div className="week-bar">
        <button className="ghost-btn" onClick={() => shift(-1)}><ChevronLeft size={18} /></button>
        <div className="week-label">
          <span className="week-range">{fmtRange(weekDate)}</span>
          {isThisWeek && <span className="this-week">This week</span>}
        </div>
        <button className="ghost-btn" onClick={() => shift(1)}><ChevronRight size={18} /></button>
        <div className="week-actions">
          <button className="autoplan-btn" onClick={onAutoPlan}><Wand2 size={15} /> Plan my week</button>
          {hasMeals && (
            <button className="text-btn" onClick={onPublish}><BookOpen size={14} /> The Menu</button>
          )}
          {!isThisWeek && (
            <button className="text-btn" onClick={() => setWeekDate(mondayOf(new Date()))}>Today</button>
          )}
        </div>
      </div>

      <div className="meals-setting">
        <span className="meals-setting-label">Planning</span>
        <div className="seg">
          {PRESETS.map((p) => (
            <button
              key={p.label}
              className={activePreset?.label === p.label ? "active" : ""}
              onClick={() => setMealsShown(p.meals)}
            >
              {p.label}
            </button>
          ))}
        </div>
        <div className="household-set">
          <span>Cooking for</span>
          <input
            type="number" min="1" value={household}
            onChange={(e) => setHousehold(Math.max(1, Number(e.target.value) || 1))}
          />
          <span>servings</span>
        </div>
        <AvoidEditor avoidList={avoidList} setAvoidList={setAvoidList} />
      </div>

      {!hasMeals && (
        <button className="plan-banner" onClick={onAutoPlan}>
          <div className="plan-banner-icon"><Wand2 size={22} /></div>
          <div>
            <strong>Plan it all for me</strong>
            <span>Pick a few proteins, cuisines, or what's on hand — I'll fill the whole week. You can edit anything after.</span>
          </div>
          <ChevronRight size={20} className="plan-banner-arrow" />
        </button>
      )}

      <div className="day-stack">
        {DAYS.map((day, di) => {
          const totals = dayTotals(day);
          const rows = rowsForDay(day);
          const addable = addableForDay(day);
          const isToday = isThisWeek && day === todayName;
          return (
            <div className={`day-card ${isToday ? "today" : ""}`} key={day}>
              <div className="day-card-head">
                <div className="day-card-title">
                  <span className="day-full">{day}</span>
                  {isToday && <span className="today-tag">Today</span>}
                </div>
                {totals.calories > 0 && <span className="day-card-cal">{totals.calories} cal</span>}
              </div>

              <div className="day-slots">
                {rows.map((mt) => {
                  const meal = getMeal(currentPlan[`${day}-${mt}`]);
                  const primary = mt === "Dinner";
                  const past = dayIsPast(di);
                  return (
                    <div className={`slot ${primary ? "slot-primary" : "slot-secondary"}`} key={mt}>
                      <span className="slot-label">{mt}</span>
                      {meal ? (
                        <div className="slot-meal-wrap">
                          <div className="slot-meal">
                            <div className="slot-meal-text">
                              <span className="slot-meal-name">
                                <SpeedBadge speed={meal.speed} />
                                <button className="meal-name-link" onClick={() => onOpenRecipe(meal.id)}>{meal.name}</button>
                              </span>
                              {meal.nutrition && (
                                <span className="slot-meal-cal">{meal.nutrition.calories} cal · {meal.nutrition.protein}g protein</span>
                              )}
                            </div>
                            <button className="slot-x" onClick={() => onClear(day, mt)}><X size={14} /></button>
                          </div>
                          {(past || isToday || meal.rating) && (
                            <div className="rate-prompt">
                              {!meal.rating && <span className="rate-prompt-q">How was it?</span>}
                              <RatingControl rating={meal.rating} onRate={(r) => onRate(meal.id, r)} size="sm" />
                            </div>
                          )}
                        </div>
                      ) : (
                        <SlotEmpty
                          mealType={mt}
                          onAdd={() => onAssign(day, mt)}
                          onGenerate={(craving) => onGenerateSlot(day, mt, craving)}
                        />
                      )}
                    </div>
                  );
                })}

                {addable.length > 0 && (
                  <div className="day-extra-add">
                    {addable.map((mt) => (
                      <button key={mt} className="extra-add-btn" onClick={() => onAssign(day, mt)}>
                        <Plus size={13} /> {mt.toLowerCase()}
                      </button>
                    ))}
                  </div>
                )}
              </div>
            </div>
          );
        })}
      </div>
      <p className="estimate-note">Nutrition values are estimates for general planning — not lab-measured.</p>
    </section>
  );
}

// ---------------------------------------------------------------------------
function IdeasView({ onSave, library, avoidList }) {
  const [craving, setCraving] = useState("");
  const [mealType, setMealType] = useState("Dinner");
  const [restrictions, setRestrictions] = useState("whole-food, family-friendly");
  const [loading, setLoading] = useState(false);
  const [results, setResults] = useState([]);
  const [error, setError] = useState(null);
  const [savedNames, setSavedNames] = useState({});

  const run = async () => {
    setLoading(true);
    setError(null);
    setResults([]);
    try {
      const meals = await generateMeals({ craving, mealType, restrictions, avoid: avoidList });
      setResults(meals);
    } catch (e) {
      setError("Couldn't generate ideas just now — try again in a moment.");
    } finally {
      setLoading(false);
    }
  };

  const save = (meal, i) => {
    onSave({ ...meal, aiGenerated: true });
    setSavedNames((p) => ({ ...p, [i]: true }));
  };

  return (
    <section className="ideas">
      <div className="ideas-form">
        <h2>What sounds good?</h2>
        <p className="sub">Tell me a craving, a cuisine, or what's in the fridge — I'll suggest meals with nutrition estimates.</p>

        <div className="field">
          <label>In the mood for / on hand</label>
          <input
            value={craving}
            onChange={(e) => setCraving(e.target.value)}
            placeholder="e.g. something with chicken and squash, cozy soup, quick weeknight…"
          />
        </div>

        <div className="field-row">
          <div className="field">
            <label>Meal</label>
            <select value={mealType} onChange={(e) => setMealType(e.target.value)}>
              {["Breakfast", "Lunch", "Dinner", "Snack", "Any"].map((m) => <option key={m}>{m}</option>)}
            </select>
          </div>
          <div className="field grow">
            <label>Preferences / targets</label>
            <input value={restrictions} onChange={(e) => setRestrictions(e.target.value)} placeholder="e.g. high protein, ~500 cal, vegetarian" />
          </div>
        </div>

        <button className="primary-btn" onClick={run} disabled={loading}>
          {loading ? <><Loader2 size={16} className="spin" /> Cooking up ideas…</> : <><Sparkles size={16} /> Get meal ideas</>}
        </button>
        {error && <p className="error">{error}</p>}
      </div>

      <div className="idea-results">
        {results.map((meal, i) => (
          <div className="idea-card" key={i}>
            <div className="idea-card-head">
              <h3>{meal.name}</h3>
              <button
                className={`save-btn ${savedNames[i] ? "saved" : ""}`}
                onClick={() => save(meal, i)}
                disabled={savedNames[i]}
              >
                {savedNames[i] ? <><Check size={14} /> Saved</> : <><Heart size={14} /> Save</>}
              </button>
            </div>
            <p className="idea-desc">{meal.description}</p>
            <NutritionRow n={meal.nutrition} />
            {meal.ingredients && (
              <div className="ingredients">
                {meal.ingredients.map((ing, j) => <span key={j} className="ing-tag">{ingredientText(ing)}</span>)}
              </div>
            )}
          </div>
        ))}
      </div>
    </section>
  );
}

// ---------------------------------------------------------------------------
function LibraryView({ library, onAdd, onRemove, onRate, onOpenRecipe }) {
  const [showForm, setShowForm] = useState(false);
  const [importing, setImporting] = useState(false);
  const [prefill, setPrefill] = useState(null); // extracted meal awaiting review
  const [q, setQ] = useState("");
  const [speedFilter, setSpeedFilter] = useState("all");
  const [rateFilter, setRateFilter] = useState("all");
  const matchesQuery = (m, query) => {
    const t = query.toLowerCase().trim();
    if (!t) return true;
    if (m.name.toLowerCase().includes(t)) return true;
    if ((m.notes || "").toLowerCase().includes(t)) return true;
    return (m.ingredients || []).some((ing) => ingredientItem(ing).toLowerCase().includes(t));
  };
  const filtered = library.filter((m) =>
    matchesQuery(m, q) &&
    (speedFilter === "all" || (m.speed || "standard") === speedFilter) &&
    (rateFilter === "all" || (rateFilter === "loved" ? m.rating === "up" : rateFilter === "nope" ? m.rating === "down" : true))
  );

  const handleExtracted = (meal) => {
    setImporting(false);
    setPrefill(meal);
    setShowForm(true);
  };

  const closeForm = () => {
    setShowForm(false);
    setPrefill(null);
  };

  return (
    <section className="library">
      <div className="lib-bar">
        <div className="search">
          <Search size={16} />
          <input value={q} onChange={(e) => setQ(e.target.value)} placeholder="Search meals or ingredients…" />
        </div>
        <button className="text-btn" onClick={() => setImporting(true)}><Camera size={15} /> Import recipe</button>
        <button className="primary-btn small" onClick={() => { setPrefill(null); setShowForm(true); }}><Plus size={16} /> Add meal</button>
      </div>

      {library.length > 0 && (
        <div className="speed-filter">
          {[{ value: "all", label: "All" }, ...SPEED_OPTIONS.map((s) => ({ value: s.value, label: s.label, icon: s.icon }))].map((f) => (
            <button
              key={f.value}
              className={speedFilter === f.value ? "active" : ""}
              onClick={() => setSpeedFilter(f.value)}
            >
              {f.icon}{f.label}
            </button>
          ))}
          <span className="filter-divider" />
          <button className={rateFilter === "loved" ? "active" : ""} onClick={() => setRateFilter(rateFilter === "loved" ? "all" : "loved")}>
            <ThumbsUp size={14} />Loved
          </button>
          <button className={rateFilter === "nope" ? "active" : ""} onClick={() => setRateFilter(rateFilter === "nope" ? "all" : "nope")}>
            <ThumbsDown size={14} />Not again
          </button>
        </div>
      )}

      {library.length === 0 ? (
        <div className="empty">
          <div className="empty-mark">📖</div>
          <h3>Your meal library is empty</h3>
          <p>Import a recipe from a photo or text, save meals from the Ideas tab, or add your own family favorites here.</p>
          <button className="primary-btn" style={{ marginTop: 18 }} onClick={() => setImporting(true)}><Camera size={16} /> Import a recipe</button>
        </div>
      ) : filtered.length === 0 ? (
        <div className="empty"><p>No meals match that filter.</p></div>
      ) : (
        <div className="lib-grid">
          {filtered.map((m) => (
            <div className="lib-card" key={m.id}>
              <button className="lib-del" onClick={() => onRemove(m.id)}><Trash2 size={14} /></button>
              <div className="lib-card-head">
                <h3><button className="meal-name-link" onClick={() => onOpenRecipe(m.id)}>{m.name}</button></h3>
                <SpeedBadge speed={m.speed} withLabel />
              </div>
              {m.description && <p className="lib-desc">{m.description}</p>}
              <NutritionRow n={m.nutrition} compact />
              {m.ingredients?.length > 0 && (
                <div className="ingredients">
                  {m.ingredients.slice(0, 6).map((ing, j) => <span key={j} className="ing-tag">{ingredientText(ing)}</span>)}
                </div>
              )}
              <div className="lib-card-foot">
                <RatingControl rating={m.rating} onRate={(r) => onRate(m.id, r)} size="sm" />
              </div>
            </div>
          ))}
        </div>
      )}

      {importing && <ImportDialog onClose={() => setImporting(false)} onExtracted={handleExtracted} />}
      {showForm && (
        <MealForm
          initial={prefill}
          reviewMode={!!prefill}
          onClose={closeForm}
          onSave={(m) => { onAdd(m); closeForm(); }}
        />
      )}
    </section>
  );
}

// ---------------------------------------------------------------------------
function ImportDialog({ onClose, onExtracted }) {
  const [mode, setMode] = useState("photo"); // 'photo' | 'text'
  const [text, setText] = useState("");
  const [preview, setPreview] = useState(null);
  const [fileData, setFileData] = useState(null); // {base64, mediaType}
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const fileRef = React.useRef(null);

  const onFile = (e) => {
    const file = e.target.files?.[0];
    if (!file) return;
    if (!file.type.startsWith("image/")) { setError("Please choose an image file."); return; }
    setError(null);
    const reader = new FileReader();
    reader.onload = () => {
      // Downscale before sending — full-res phone photos make the request too large.
      const img = new Image();
      img.onload = () => {
        const maxW = 1500; // enough resolution to read recipe text clearly
        const scale = Math.min(1, maxW / img.width);
        const canvas = document.createElement("canvas");
        canvas.width = Math.round(img.width * scale);
        canvas.height = Math.round(img.height * scale);
        const ctx = canvas.getContext("2d");
        ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
        const dataUrl = canvas.toDataURL("image/jpeg", 0.85);
        setPreview(dataUrl);
        setFileData({ base64: dataUrl.split(",")[1], mediaType: "image/jpeg" });
      };
      img.onerror = () => {
        // Fallback: send original if it can't be drawn
        setPreview(reader.result);
        setFileData({ base64: reader.result.split(",")[1], mediaType: file.type });
      };
      img.src = reader.result;
    };
    reader.readAsDataURL(file);
  };

  const run = async () => {
    setLoading(true);
    setError(null);
    try {
      let meal;
      if (mode === "photo") {
        if (!fileData) { setError("Add a photo first."); setLoading(false); return; }
        meal = await extractRecipeFromImage(fileData.base64, fileData.mediaType);
      } else {
        if (text.trim().length < 10) { setError("Paste a bit more recipe text first."); setLoading(false); return; }
        meal = await extractRecipeFromText(text);
      }
      onExtracted(meal);
    } catch (e) {
      setError("Couldn't read that recipe — try a clearer photo, or paste the text instead.");
      setLoading(false);
    }
  };

  return (
    <div className="modal-backdrop" onClick={onClose}>
      <div className="modal" onClick={(e) => e.stopPropagation()}>
        <div className="modal-head">
          <h2>Import a recipe</h2>
          <button className="ghost-btn" onClick={onClose}><X size={18} /></button>
        </div>

        {loading ? (
          <div className="autoplan-loading">
            <Loader2 size={30} className="spin" />
            <strong>Reading the recipe…</strong>
            <span>Pulling out the name, ingredients, and a nutrition estimate.</span>
          </div>
        ) : (
          <>
            <div className="import-toggle">
              <button className={mode === "photo" ? "active" : ""} onClick={() => setMode("photo")}><Camera size={15} /> Photo</button>
              <button className={mode === "text" ? "active" : ""} onClick={() => setMode("text")}><Type size={15} /> Paste text</button>
            </div>

            {mode === "photo" ? (
              <div className="import-photo">
                <input ref={fileRef} type="file" accept="image/*" onChange={onFile} style={{ display: "none" }} />
                {preview ? (
                  <div className="photo-preview">
                    <img src={preview} alt="recipe" />
                    <button className="text-btn" onClick={() => fileRef.current?.click()}>Choose a different photo</button>
                  </div>
                ) : (
                  <button className="dropzone" onClick={() => fileRef.current?.click()}>
                    <Camera size={26} />
                    <strong>Take or choose a photo</strong>
                    <span>A cookbook page, recipe card, or a screenshot from Pinterest</span>
                  </button>
                )}
              </div>
            ) : (
              <div className="field">
                <label>Paste the recipe</label>
                <textarea
                  value={text}
                  onChange={(e) => setText(e.target.value)}
                  placeholder="Paste recipe text — title, ingredients, instructions. Claude will pull out what it needs."
                  rows={8}
                />
              </div>
            )}

            {error && <p className="error">{error}</p>}
            <button className="primary-btn full" onClick={run}><Sparkles size={16} /> Read recipe</button>
            <p className="estimate-note tight">You'll review and edit everything before it's saved.</p>
          </>
        )}
      </div>
    </div>
  );
}

// ---------------------------------------------------------------------------
function PublicationView({ title, meals, household, onClose }) {
  const single = meals.length === 1;
  return (
    <div className="pub-overlay">
      <div className="pub-bar">
        <button className="ghost-btn" onClick={onClose}><X size={18} /></button>
        <span className="pub-bar-title">{single ? "Recipe" : "The Menu"}</span>
        <button className="primary-btn small" onClick={() => window.print()}><Printer size={15} /> Print</button>
      </div>

      <div className="pub-page">
        <header className={`pub-cover ${single ? "pub-cover-slim" : ""}`}>
          <div className="pub-cover-eyebrow">The Family Table</div>
          {!single && <h1 className="pub-cover-title">This Week's Menu</h1>}
          <div className="pub-cover-rule"><span /><span className="pub-cover-dot" /><span /></div>
          {!single && <p className="pub-cover-sub">{title}</p>}
        </header>

        {meals.map(({ slot, meal }, i) => {
          const scale = (meal.serves && household && canScaleQuantities(meal)) ? household / meal.serves : 1;
          const showScaled = Math.abs(scale - 1) > 0.05;
          const totalTime = (meal.prepTime || 0) + (meal.cookTime || 0);
          return (
            <article className="pub-recipe" key={i}>
              {slot && <div className="pub-slot">{slot}</div>}
              {meal.photo && (
                <div className="pub-photo-banner" style={{ backgroundImage: `url(${meal.photo})` }} role="img" aria-label={meal.name} />
              )}
              <div className="pub-recipe-head">
                <h2 className="pub-recipe-title">{meal.name}</h2>
                {meal.description && <p className="pub-recipe-desc">{meal.description}</p>}
                <div className="pub-recipe-stats">
                  {meal.serves ? <span><strong>{showScaled ? household : meal.serves}</strong>serves</span> : null}
                  {meal.prepTime ? <span><strong>{meal.prepTime}m</strong>prep</span> : null}
                  {meal.cookTime ? <span><strong>{meal.cookTime}m</strong>cook</span> : null}
                  {totalTime > 0 ? <span><strong>{totalTime}m</strong>total</span> : null}
                </div>
              </div>

              <div className="pub-recipe-body">
                <div className="pub-ingredients">
                  <h3>Ingredients{showScaled && <span className="pub-scaled"> · for {household}</span>}</h3>
                  <ul>
                    {(meal.ingredients || []).map((ing, j) => (
                      <li key={j}>{ingredientText(ing, showScaled ? scale : 1)}</li>
                    ))}
                  </ul>
                  {meal.nutrition && (
                    <div className="pub-nutrition">
                      <span><strong>{meal.nutrition.calories}</strong> cal</span>
                      <span><strong>{meal.nutrition.protein}g</strong> protein</span>
                      <span><strong>{meal.nutrition.carbs}g</strong> carbs</span>
                      <span><strong>{meal.nutrition.fat}g</strong> fat</span>
                    </div>
                  )}
                </div>
                <div className="pub-method">
                  <h3>Method</h3>
                  {meal.instructions?.length > 0 ? (
                    <ol>{meal.instructions.map((s, j) => <li key={j}>{s}</li>)}</ol>
                  ) : (
                    <p className="pub-nomethod">No steps recorded for this dish.</p>
                  )}
                </div>
              </div>
            </article>
          );
        })}

        <footer className="pub-foot">Made with care, at our table.</footer>
      </div>
    </div>
  );
}

// ---------------------------------------------------------------------------
function RecipeView({ meal, household, avoidList, onClose, onRate, onEdit, onSetPhoto, onClearPhoto, onFleshOut, onPublish }) {
  const photoRef = React.useRef(null);
  const [fleshing, setFleshing] = React.useState(false);
  if (!meal) return null;
  const scale = (meal.serves && household && canScaleQuantities(meal)) ? household / meal.serves : 1;
  const showScaled = Math.abs(scale - 1) > 0.05;
  const totalTime = (meal.prepTime || 0) + (meal.cookTime || 0);
  const flagged = avoidedIn(meal, avoidList);

  return (
    <div className="modal-backdrop" onClick={onClose}>
      <div className={`modal recipe-modal ${meal.photo ? "has-hero" : ""}`} onClick={(e) => e.stopPropagation()}>
        {meal.photo ? (
          <div className="recipe-hero">
            <div className="meal-photo recipe-hero-img" style={{ backgroundImage: `url(${meal.photo})` }} role="img" aria-label={meal.name} />
            <button className="ghost-btn recipe-close" onClick={onClose}><X size={18} /></button>
            <div className="recipe-hero-photo-actions">
              <input ref={photoRef} type="file" accept="image/*" style={{ display: "none" }}
                onChange={(e) => { const f = e.target.files?.[0]; e.target.value = ""; if (f) onSetPhoto(meal.id, f); }} />
              <button className="photo-btn" onClick={() => photoRef.current?.click()}><Camera size={14} /> Change photo</button>
              <button className="photo-btn" onClick={() => onClearPhoto(meal.id)}><X size={13} /> Remove</button>
            </div>
          </div>
        ) : (
          <div className="modal-head">
            <div>
              <h2>{meal.name}</h2>
              {meal.description && <span className="modal-sub">{meal.description}</span>}
            </div>
            <button className="ghost-btn" onClick={onClose}><X size={18} /></button>
          </div>
        )}
        {meal.photo && (
          <div className="modal-head">
            <div>
              <h2>{meal.name}</h2>
              {meal.description && <span className="modal-sub">{meal.description}</span>}
            </div>
          </div>
        )}

        <div className="recipe-meta">
          <SpeedBadge speed={meal.speed} withLabel />
          {meal.serves && <span className="recipe-meta-item">Serves {meal.serves}</span>}
          {meal.prepTime ? <span className="recipe-meta-item">Prep {meal.prepTime}m</span> : null}
          {meal.cookTime ? <span className="recipe-meta-item">Cook {meal.cookTime}m</span> : null}
          {totalTime > 0 && <span className="recipe-meta-item total">Total {totalTime}m</span>}
        </div>

        {meal.nutrition && <NutritionRow n={meal.nutrition} />}

        {flagged.length > 0 && (
          <div className="avoid-flag">
            Heads up: this contains {flagged.join(", ")}, which you generally avoid.
          </div>
        )}

        {meal.notes && (
          <div className="recipe-notes">
            <span className="recipe-notes-label">Notes</span>
            {meal.notes}
          </div>
        )}

        {meal.ingredients?.length > 0 && (
          <div className="recipe-section">
            <h3>Ingredients{showScaled && <span className="scaled-note"> · scaled for {household}</span>}</h3>
            <ul className="recipe-ingredients">
              {meal.ingredients.map((ing, i) => (
                <li key={i}>{ingredientText(ing, showScaled ? scale : 1)}</li>
              ))}
            </ul>
            {showScaled && <p className="recipe-tinynote">Amounts multiplied ×{(Math.round(scale * 10) / 10)} from the recipe's {meal.serves} servings to your {household}.</p>}
          </div>
        )}

        {meal.instructions?.length > 0 ? (
          <div className="recipe-section">
            <h3>Instructions</h3>
            <ol className="recipe-steps">
              {meal.instructions.map((s, i) => <li key={i}>{s}</li>)}
            </ol>
          </div>
        ) : (
          <div className="recipe-section">
            <p className="recipe-tinynote">No steps saved yet — this was planned as a quick idea.</p>
            <button
              className="text-btn"
              style={{ marginTop: 10 }}
              disabled={fleshing}
              onClick={async () => { setFleshing(true); try { await onFleshOut(meal); } catch (e) {} setFleshing(false); }}
            >
              {fleshing ? <><Loader2 size={14} className="spin" /> Generating…</> : <><Sparkles size={14} /> Generate full recipe</>}
            </button>
          </div>
        )}

        <div className="recipe-foot">
          <RatingControl rating={meal.rating} onRate={(r) => onRate(meal.id, r)} />
          <div className="recipe-foot-actions">
            {!meal.photo && (
              <>
                <input ref={photoRef} type="file" accept="image/*" style={{ display: "none" }}
                  onChange={(e) => { const f = e.target.files?.[0]; e.target.value = ""; if (f) onSetPhoto(meal.id, f); }} />
                <button className="text-btn" onClick={() => photoRef.current?.click()}><Camera size={14} /> Add photo</button>
              </>
            )}
            <button className="text-btn" onClick={onPublish}><BookOpen size={14} /> The Menu</button>
            <button className="text-btn" onClick={onEdit}>Edit recipe</button>
          </div>
        </div>
      </div>
    </div>
  );
}

// ---------------------------------------------------------------------------
function MealForm({ onClose, onSave, initial, reviewMode }) {
  const [name, setName] = useState(initial?.name || "");
  const [description, setDescription] = useState(initial?.description || "");
  const [notes, setNotes] = useState(initial?.notes || "");
  const [speed, setSpeed] = useState(initial?.speed || "standard");
  const [serves, setServes] = useState(initial?.serves || "");
  const [prepTime, setPrepTime] = useState(initial?.prepTime || "");
  const [cookTime, setCookTime] = useState(initial?.cookTime || "");
  // ingredient rows: always edited as structured {qty, unit, item}; legacy strings convert in
  const toRows = (list) => {
    if (!list || !list.length) return [{ qty: "", unit: "", item: "" }];
    return list.map((i) =>
      isStructured(i)
        ? { qty: i.qty ?? "", unit: i.unit || "", item: i.item || "" }
        : { qty: "", unit: "", item: String(i) } // legacy string → goes in item field
    );
  };
  const [rows, setRows] = useState(toRows(initial?.ingredients));
  const [steps, setSteps] = useState(initial?.instructions?.length ? [...initial.instructions] : [""]);
  const [cal, setCal] = useState(initial?.nutrition?.calories || "");
  const [protein, setProtein] = useState(initial?.nutrition?.protein || "");
  const [carbs, setCarbs] = useState(initial?.nutrition?.carbs || "");
  const [fat, setFat] = useState(initial?.nutrition?.fat || "");

  const setRow = (i, key, val) => setRows((r) => r.map((row, j) => j === i ? { ...row, [key]: val } : row));
  const addRow = () => setRows((r) => [...r, { qty: "", unit: "", item: "" }]);
  const removeRow = (i) => setRows((r) => r.length > 1 ? r.filter((_, j) => j !== i) : r);
  const setStep = (i, val) => setSteps((s) => s.map((st, j) => j === i ? val : st));
  const addStep = () => setSteps((s) => [...s, ""]);
  const removeStep = (i) => setSteps((s) => s.length > 1 ? s.filter((_, j) => j !== i) : s);

  const submit = () => {
    if (!name.trim()) return;
    const ingredients = rows
      .filter((r) => r.item.trim())
      .map((r) => ({ qty: r.qty === "" ? null : Number(r.qty), unit: r.unit.trim(), item: r.item.trim() }));
    onSave({
      name: name.trim(),
      description: description.trim(),
      notes: notes.trim(),
      ingredients,
      instructions: steps.map((s) => s.trim()).filter(Boolean),
      speed,
      serves: Number(serves) || null,
      prepTime: Number(prepTime) || null,
      cookTime: Number(cookTime) || null,
      nutrition: {
        calories: Number(cal) || 0,
        protein: Number(protein) || 0,
        carbs: Number(carbs) || 0,
        fat: Number(fat) || 0,
      },
    });
  };

  return (
    <div className="modal-backdrop" onClick={onClose}>
      <div className="modal" onClick={(e) => e.stopPropagation()}>
        <div className="modal-head">
          <div>
            <h2>{reviewMode ? "Review recipe" : "Add a meal"}</h2>
            {reviewMode && <span className="modal-sub">Check what Claude read, edit anything, then save.</span>}
          </div>
          <button className="ghost-btn" onClick={onClose}><X size={18} /></button>
        </div>
        <div className="field">
          <label>Meal name</label>
          <input value={name} onChange={(e) => setName(e.target.value)} placeholder="e.g. Sheet-pan chicken & veggies" autoFocus />
        </div>
        <div className="field">
          <label>Description <span className="opt">(optional)</span></label>
          <input value={description} onChange={(e) => setDescription(e.target.value)} placeholder="A short appetizing line" />
        </div>
        <div className="field">
          <label>Notes <span className="opt">(optional — for you)</span></label>
          <textarea rows={2} value={notes} onChange={(e) => setNotes(e.target.value)} placeholder="e.g. kids' favorite, double the sauce, from Mom's card…" />
        </div>
        <div className="field">
          <label>Type</label>
          <div className="speed-pick">
            {SPEED_OPTIONS.map((s) => (
              <button key={s.value} type="button" className={`speed-opt ${speed === s.value ? "active" : ""}`} onClick={() => setSpeed(s.value)}>
                {s.icon}{s.label}
              </button>
            ))}
          </div>
        </div>
        <div className="field-row three">
          <div className="field">
            <label>Serves</label>
            <input type="number" min="1" value={serves} onChange={(e) => setServes(e.target.value)} placeholder="4" />
          </div>
          <div className="field">
            <label>Prep <span className="opt">min</span></label>
            <input type="number" min="0" value={prepTime} onChange={(e) => setPrepTime(e.target.value)} placeholder="10" />
          </div>
          <div className="field">
            <label>Cook <span className="opt">min</span></label>
            <input type="number" min="0" value={cookTime} onChange={(e) => setCookTime(e.target.value)} placeholder="25" />
          </div>
        </div>
        <div className="field">
          <label>Ingredients <span className="opt">(amount · unit · item)</span></label>
          <div className="ing-rows">
            {rows.map((row, i) => (
              <div className="ing-row" key={i}>
                <input className="ing-qty" type="number" value={row.qty} onChange={(e) => setRow(i, "qty", e.target.value)} placeholder="2" />
                <input className="ing-unit" value={row.unit} onChange={(e) => setRow(i, "unit", e.target.value)} placeholder="cups" />
                <input className="ing-item" value={row.item} onChange={(e) => setRow(i, "item", e.target.value)} placeholder="rice" />
                <button className="row-x" onClick={() => removeRow(i)} title="Remove"><X size={14} /></button>
              </div>
            ))}
          </div>
          <button className="add-row-btn" onClick={addRow}><Plus size={14} /> Add ingredient</button>
        </div>
        <div className="field">
          <label>Instructions <span className="opt">(one step per line)</span></label>
          <div className="step-rows">
            {steps.map((st, i) => (
              <div className="step-row" key={i}>
                <span className="step-num">{i + 1}</span>
                <textarea rows={2} value={st} onChange={(e) => setStep(i, e.target.value)} placeholder="Describe this step…" />
                <button className="row-x" onClick={() => removeStep(i)} title="Remove"><X size={14} /></button>
              </div>
            ))}
          </div>
          <button className="add-row-btn" onClick={addStep}><Plus size={14} /> Add step</button>
        </div>
        <div className="field">
          <label>Nutrition <span className="opt">(per serving, estimate)</span></label>
          <div className="macro-inputs">
            <div><input type="number" value={cal} onChange={(e) => setCal(e.target.value)} placeholder="0" /><span>cal</span></div>
            <div><input type="number" value={protein} onChange={(e) => setProtein(e.target.value)} placeholder="0" /><span>g protein</span></div>
            <div><input type="number" value={carbs} onChange={(e) => setCarbs(e.target.value)} placeholder="0" /><span>g carbs</span></div>
            <div><input type="number" value={fat} onChange={(e) => setFat(e.target.value)} placeholder="0" /><span>g fat</span></div>
          </div>
        </div>
        <button className="primary-btn" onClick={submit}>Save to library</button>
      </div>
    </div>
  );
}

// ---------------------------------------------------------------------------
function AssignDialog({ slot, library, avoidList, onPick, onClose, onCreateQuick }) {
  const [q, setQ] = useState("");
  const [creating, setCreating] = useState(false);
  const [speedFilter, setSpeedFilter] = useState("all");
  const filtered = library.filter((m) =>
    m.name.toLowerCase().includes(q.toLowerCase()) &&
    (speedFilter === "all" || (m.speed || "standard") === speedFilter)
  );

  return (
    <div className="modal-backdrop" onClick={onClose}>
      <div className="modal" onClick={(e) => e.stopPropagation()}>
        <div className="modal-head">
          <h2>{slot.day} · {slot.mealType}</h2>
          <button className="ghost-btn" onClick={onClose}><X size={18} /></button>
        </div>

        {creating ? (
          <MealForm
            onClose={() => setCreating(false)}
            onSave={(m) => onCreateQuick(m)}
          />
        ) : (
          <>
            <div className="search inline">
              <Search size={16} />
              <input value={q} onChange={(e) => setQ(e.target.value)} placeholder="Pick from your library…" autoFocus />
            </div>
            <div className="speed-filter compact">
              {[{ value: "all", label: "All" }, ...SPEED_OPTIONS.map((s) => ({ value: s.value, label: s.label, icon: s.icon }))].map((f) => (
                <button key={f.value} className={speedFilter === f.value ? "active" : ""} onClick={() => setSpeedFilter(f.value)}>
                  {f.icon}{f.label}
                </button>
              ))}
            </div>
            <div className="assign-list">
              {filtered.length === 0 && <p className="muted">No saved meals match. Add one below.</p>}
              {filtered.map((m) => {
                const flagged = avoidedIn(m, avoidList);
                return (
                  <button key={m.id} className="assign-item" onClick={() => onPick(m.id)}>
                    <div>
                      <div className="assign-name"><SpeedBadge speed={m.speed} />{m.name}</div>
                      {m.nutrition && <div className="assign-cal">{m.nutrition.calories} cal · {m.nutrition.protein}g protein</div>}
                      {flagged.length > 0 && <div className="assign-avoid">contains {flagged.join(", ")}</div>}
                    </div>
                    <Plus size={16} />
                  </button>
                );
              })}
            </div>
            <button className="text-btn center" onClick={() => setCreating(true)}><Plus size={14} /> Create a new meal instead</button>
          </>
        )}
      </div>
    </div>
  );
}

// ---------------------------------------------------------------------------
function Chip({ active, onClick, children }) {
  return (
    <button type="button" className={`chip ${active ? "active" : ""}`} onClick={onClick}>
      {children}
    </button>
  );
}

function AutoPlanDialog({ weekRange, existingCount, avoidList, onClose, onApply }) {
  const [mealTypes, setMealTypes] = useState(["Dinner"]);
  const [proteins, setProteins] = useState([]);
  const [cuisines, setCuisines] = useState([]);
  const [onHand, setOnHand] = useState("");
  const [target, setTarget] = useState("");
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  const toggle = (list, setList, val) =>
    setList(list.includes(val) ? list.filter((x) => x !== val) : [...list, val]);

  const run = async () => {
    if (mealTypes.length === 0) {
      setError("Pick at least one meal to fill.");
      return;
    }
    setLoading(true);
    setError(null);
    try {
      const week = await generateWeek({ mealTypes, proteins, onHand, cuisines, target, avoid: avoidList });
      onApply(week, mealTypes);
    } catch (e) {
      setError("Couldn't build the week just now — give it another try.");
      setLoading(false);
    }
  };

  return (
    <div className="modal-backdrop" onClick={onClose}>
      <div className="modal wide" onClick={(e) => e.stopPropagation()}>
        <div className="modal-head">
          <div>
            <h2>Plan my week</h2>
            <span className="modal-sub">{weekRange}</span>
          </div>
          <button className="ghost-btn" onClick={onClose}><X size={18} /></button>
        </div>

        {loading ? (
          <div className="autoplan-loading">
            <Loader2 size={30} className="spin" />
            <strong>Planning your week…</strong>
            <span>Choosing meals, balancing proteins, estimating nutrition.</span>
          </div>
        ) : (
          <>
            <div className="field">
              <label>Which meals should I fill?</label>
              <div className="chip-row">
                {MEALS.map((m) => (
                  <Chip key={m} active={mealTypes.includes(m)} onClick={() => toggle(mealTypes, setMealTypes, m)}>{m}</Chip>
                ))}
              </div>
            </div>

            <div className="field">
              <label>Proteins to feature <span className="opt">(optional — spread across the week)</span></label>
              <div className="chip-row">
                {PROTEIN_OPTIONS.map((p) => (
                  <Chip key={p} active={proteins.includes(p)} onClick={() => toggle(proteins, setProteins, p)}>{p}</Chip>
                ))}
              </div>
            </div>

            <div className="field">
              <label>Cuisine inspiration <span className="opt">(optional — a few meals, not the whole week)</span></label>
              <div className="chip-row">
                {CUISINE_OPTIONS.map((c) => (
                  <Chip key={c} active={cuisines.includes(c)} onClick={() => toggle(cuisines, setCuisines, c)}>{c}</Chip>
                ))}
              </div>
            </div>

            <div className="field">
              <label>Ingredients on hand to use up <span className="opt">(optional)</span></label>
              <input value={onHand} onChange={(e) => setOnHand(e.target.value)} placeholder="e.g. ground beef, sweet potatoes, spinach…" />
            </div>

            <div className="field">
              <label>Nutrition target <span className="opt">(optional)</span></label>
              <input value={target} onChange={(e) => setTarget(e.target.value)} placeholder="e.g. high protein, ~600 cal dinners" />
            </div>

            {existingCount > 0 && (
              <p className="autoplan-warn">This week already has {existingCount} meal{existingCount > 1 ? "s" : ""} planned. New meals will fill the empty slots and may replace overlapping ones.</p>
            )}
            {error && <p className="error">{error}</p>}

            <button className="primary-btn full" onClick={run}>
              <Wand2 size={16} /> Build my week
            </button>
            <p className="estimate-note tight">You can edit, swap, or remove any meal after — nothing's locked in.</p>
          </>
        )}
      </div>
    </div>
  );
}

// ---------------------------------------------------------------------------
// Grocery: categorize ingredients into store sections by keyword
const GROCERY_CATEGORIES = [
  { name: "Produce", icon: "🥬", match: ["lettuce", "spinach", "kale", "tomato", "onion", "garlic", "pepper", "carrot", "celery", "potato", "broccoli", "zucchini", "squash", "cucumber", "avocado", "lemon", "lime", "apple", "banana", "berry", "berries", "mushroom", "cilantro", "parsley", "basil", "herb", "ginger", "scallion", "cabbage", "corn", "peas", "bean sprout", "kale", "arugula", "cauliflower", "asparagus", "green bean", "sweet potato", "fruit", "vegetable", "salad", "lime", "orange", "grape", "cucumber"] },
  { name: "Meat & Seafood", icon: "🥩", match: ["chicken", "beef", "pork", "turkey", "bacon", "sausage", "fish", "salmon", "shrimp", "steak", "ground", "lamb", "tuna", "cod", "tilapia", "ham", "meat", "thigh", "breast", "ribs", "chorizo"] },
  { name: "Dairy & Eggs", icon: "🧀", match: ["milk", "cheese", "butter", "egg", "yogurt", "cream", "sour cream", "mozzarella", "parmesan", "cheddar", "feta", "ricotta", "half and half", "cottage"] },
  { name: "Bakery & Grains", icon: "🍞", match: ["bread", "rice", "pasta", "flour", "tortilla", "bun", "roll", "oat", "quinoa", "noodle", "cereal", "bagel", "couscous", "cracker", "breadcrumb", "panko", "spaghetti", "macaroni"] },
  { name: "Pantry & Canned", icon: "🥫", match: ["oil", "vinegar", "sauce", "broth", "stock", "can", "canned", "bean", "chickpea", "lentil", "tomato paste", "tomato sauce", "coconut milk", "sugar", "salt", "pepper", "spice", "soy sauce", "honey", "syrup", "mustard", "ketchup", "mayo", "mayonnaise", "peanut butter", "stock", "cumin", "paprika", "cinnamon", "vanilla", "baking", "yeast", "cornstarch", "nut", "seed", "raisin", "salsa", "dressing"] },
  { name: "Frozen", icon: "🧊", match: ["frozen", "ice cream", "frozen peas", "frozen corn"] },
  { name: "Other", icon: "🛒", match: [] },
];

function categorize(name) {
  const n = name.toLowerCase();
  for (const cat of GROCERY_CATEGORIES) {
    if (cat.match.some((kw) => n.includes(kw))) return cat.name;
  }
  return "Other";
}

// Strip leading quantities/units and trailing prep notes so the same ingredient
// from different recipes matches for consolidation. Used only for grouping —
// we never sum quantities, just collapse identical items onto one line.
const UNIT_WORDS = "cups?|cup|tbsps?|tbsp|tablespoons?|tsps?|tsp|teaspoons?|oz|ounces?|lbs?|pounds?|g|grams?|kg|ml|l|liters?|cloves?|cans?|jars?|packages?|pkgs?|pkg|bunch(?:es)?|sprigs?|stalks?|slices?|sticks?|heads?|pinch(?:es)?|dash(?:es)?|handful|to taste";
function normalizeForMatch(raw) {
  let s = (raw || "").toLowerCase().trim();
  s = s.replace(/[^a-z0-9\s/.-]/g, " ");                 // drop punctuation (incl. fractions like ½ already gone)
  s = s.replace(/^[\d\s.\/-]+/, "");                      // leading numbers/fractions
  s = s.replace(new RegExp(`^(?:${UNIT_WORDS})\\b\\.?\\s*`, "i"), ""); // a leading unit word
  s = s.replace(/\b(?:of|fresh|chopped|minced|diced|sliced|ground|grated|shredded|softened|melted|to taste|large|small|medium|ripe|boneless|skinless)\b/g, " ");
  s = s.replace(/s\b/g, "");                              // crude singularize (eggs->egg, onions->onion)
  s = s.replace(/\s+/g, " ").trim();
  return s || (raw || "").toLowerCase().trim();
}
// A tidy display name: original text with a leading quantity/unit removed, capitalized.
function cleanIngredientName(raw) {
  let s = (raw || "").trim();
  s = s.replace(/^[\d\s.\/¼½¾⅓⅔⅛-]+/, "");
  s = s.replace(new RegExp(`^(?:${UNIT_WORDS})\\b\\.?\\s*`, "i"), "");
  s = s.replace(/\s+/g, " ").trim();
  if (!s) s = (raw || "").trim();
  return s.charAt(0).toUpperCase() + s.slice(1);
}

function GroceryView({ weekRange, currentPlan, getMeal, grocery, onToggle, onToggleKeys, onAddCustom, onRemoveCustom, onClearChecked, onGoPlan, household }) {
  const [newItem, setNewItem] = useState("");
  const [groupBy, setGroupBy] = useState("aisle"); // 'aisle' | 'meal'
  const [showMeals, setShowMeals] = useState(false); // show "for which meals" sub-labels
  const [shareMsg, setShareMsg] = useState(null);

  // scale hint for a meal: only when it has a serving count that differs from household
  const scaleFor = (meal) => {
    if (!meal?.serves || meal.serves <= 0 || !household) return null;
    const factor = household / meal.serves;
    if (Math.abs(factor - 1) < 0.05) return null; // close enough to 1×, no hint
    return { factor, serves: meal.serves };
  };
  const fmtFactor = (f) => (Number.isInteger(f) ? `${f}×` : `${f.toFixed(1).replace(/\.0$/, "")}×`);

  // Build one entry per ingredient line, preserving original text & source meal.
  // Stable key = mealId + ingredient index, so identical strings stay independent.
  const lines = [];
  const mealScale = {}; // mealLabel -> {factor, serves}
  // order slots by day then meal type for a stable, sensible meal grouping
  const orderedSlots = [];
  DAYS.forEach((day) => MEALS.forEach((mt) => {
    const slot = `${day}-${mt}`;
    if (currentPlan[slot]) orderedSlots.push({ slot, day, mt });
  }));
  // include any slots not matching the standard grid (safety)
  Object.keys(currentPlan).forEach((slot) => {
    if (!orderedSlots.find((s) => s.slot === slot)) orderedSlots.push({ slot });
  });

  orderedSlots.forEach(({ slot, day, mt }) => {
    const meal = getMeal(currentPlan[slot]);
    if (!meal?.ingredients?.length) return;
    const label = day && mt ? `${day} · ${mt}` : meal.name;
    const scale = scaleFor(meal);
    if (scale) mealScale[label] = scale;
    // If the meal has structured quantities, scale them for real; otherwise leave text as-is.
    const applyQty = scale && canScaleQuantities(meal) ? scale.factor : 1;
    meal.ingredients.forEach((ing, idx) => {
      const text = ingredientText(ing, applyQty);
      if (!text) return;
      lines.push({
        key: `${meal.id}:${idx}`,
        text,
        item: ingredientItem(ing),
        mealName: meal.name,
        mealLabel: label,
        scale,
        scaledQty: applyQty !== 1, // did we actually multiply the amount?
      });
    });
  });

  const customItems = (grocery.custom || []).map((c) => ({
    key: "custom:" + c.id,
    text: c.name,
    custom: true,
    id: c.id,
  }));

  const hasPlanned = lines.length > 0;

  // ---- grouping ----
  const grouped = {};
  if (groupBy === "aisle") {
    // Consolidate same-ingredient lines into one shopping entry that lists its meals.
    // Match by normalized name (quantities/units stripped) but never sum amounts.
    const byKey = new Map(); // normKey -> consolidated entry
    lines.forEach((ln) => {
      // match & categorize on the bare item when we have it (structured), else the full text
      const basis = ln.item || ln.text;
      const norm = normalizeForMatch(basis);
      if (!byKey.has(norm)) {
        byKey.set(norm, {
          key: "agg:" + norm,
          sourceKeys: [ln.key],         // underlying per-source keys (for check state)
          display: ln.item ? (ln.item.charAt(0).toUpperCase() + ln.item.slice(1)) : cleanIngredientName(ln.text),
          meals: [ln.mealName],
          cat: categorize(basis),
          agg: true,
        });
      } else {
        const e = byKey.get(norm);
        e.sourceKeys.push(ln.key);
        if (!e.meals.includes(ln.mealName)) e.meals.push(ln.mealName);
      }
    });
    byKey.forEach((entry) => {
      (grouped[entry.cat] = grouped[entry.cat] || []).push(entry);
    });
    if (customItems.length) grouped["Extras"] = customItems;
  } else {
    // by meal: each planned meal is its own group, in day/meal order — un-grouped, full list
    const seen = [];
    lines.forEach((ln) => {
      const g = ln.mealLabel;
      if (!grouped[g]) { grouped[g] = []; seen.push(g); }
      grouped[g].push(ln);
    });
    if (customItems.length) { grouped["Extras"] = customItems; seen.push("Extras"); }
    grouped.__order = seen;
  }

  // an entry is "checked" if all its source keys are checked (aggregated) or its own key is
  const isEntryChecked = (item) =>
    item.agg ? item.sourceKeys.every((k) => grocery.checked[k]) : !!grocery.checked[item.key];
  const toggleEntry = (item) =>
    item.agg ? onToggleKeys(item.sourceKeys) : onToggle(item.key);

  // counts reflect what's actually shown in the current view
  const shownEntries = Object.entries(grouped)
    .filter(([k]) => k !== "__order")
    .flatMap(([, arr]) => arr);
  const totalItems = shownEntries.length;
  const checkedCount = shownEntries.filter((e) => isEntryChecked(e)).length;

  const orderedGroups =
    groupBy === "aisle"
      ? [...GROCERY_CATEGORIES.map((c) => c.name), "Extras"].filter((c) => grouped[c]?.length)
      : grouped.__order || [];

  const iconFor = (g) => {
    if (g === "Extras") return "➕";
    if (groupBy === "aisle") return GROCERY_CATEGORIES.find((c) => c.name === g)?.icon || "🛒";
    return "🍽️";
  };

  const addItem = () => {
    const v = newItem.trim();
    if (!v) return;
    onAddCustom(v);
    setNewItem("");
  };

  // Build a clean text version of the list — only items still to buy (unchecked).
  const buildShareText = () => {
    const lines2 = [`🛒 Grocery list — ${weekRange}`, ""];
    let any = false;
    orderedGroups.forEach((g) => {
      const items = (grouped[g] || []).filter((it) => !isEntryChecked(it));
      if (!items.length) return;
      any = true;
      lines2.push(g);
      items.forEach((it) => lines2.push(`  • ${it.agg ? it.display : it.text}`));
      lines2.push("");
    });
    if (!any) return `Grocery list — ${weekRange}\n\n(Everything's already checked off!)`;
    return lines2.join("\n").trim();
  };

  const shareList = async () => {
    const text = buildShareText();
    try {
      if (navigator.share) {
        await navigator.share({ title: "Grocery list", text });
      } else {
        await navigator.clipboard.writeText(text);
        setShareMsg("Copied to clipboard");
        setTimeout(() => setShareMsg(null), 2500);
      }
    } catch (e) {
      // user cancelled the share sheet, or clipboard blocked — try clipboard as fallback
      try {
        await navigator.clipboard.writeText(text);
        setShareMsg("Copied to clipboard");
        setTimeout(() => setShareMsg(null), 2500);
      } catch { /* nothing more we can do */ }
    }
  };

  return (
    <section className="grocery">
      <div className="grocery-head">
        <div>
          <h2>Grocery list</h2>
          <span className="grocery-range">{weekRange} · {totalItems} item{totalItems !== 1 ? "s" : ""}{checkedCount > 0 ? ` · ${checkedCount} checked` : ""}</span>
        </div>
        <div className="grocery-head-actions">
          {hasPlanned && (
            <div className="view-toggle">
              <button className={groupBy === "aisle" ? "active" : ""} onClick={() => setGroupBy("aisle")}>By aisle</button>
              <button className={groupBy === "meal" ? "active" : ""} onClick={() => setGroupBy("meal")}>By meal</button>
            </div>
          )}
          {hasPlanned && groupBy === "aisle" && (
            <button
              className={`pill-toggle ${showMeals ? "on" : ""}`}
              onClick={() => setShowMeals((v) => !v)}
              title="Show or hide which meals each item is for"
            >
              {showMeals ? "Hide meals" : "Show meals"}
            </button>
          )}
          {(hasPlanned || customItems.length > 0) && (
            <button className="text-btn" onClick={shareList} title="Send the list (items still to buy)"><Share2 size={14} /> Share{shareMsg ? ` · ${shareMsg}` : ""}</button>
          )}
          {(hasPlanned || customItems.length > 0) && (
            <button className="text-btn" onClick={() => window.print()} title="Print a clean shopping list"><Printer size={14} /> Print</button>
          )}
          {checkedCount > 0 && <button className="text-btn" onClick={onClearChecked}>Uncheck all</button>}
        </div>
      </div>
      <div className="print-only print-title">Grocery list · {weekRange}</div>

      <div className="add-item">
        <Plus size={16} />
        <input
          value={newItem}
          onChange={(e) => setNewItem(e.target.value)}
          onKeyDown={(e) => e.key === "Enter" && addItem()}
          placeholder="Add something not in a recipe — milk, paper towels…"
        />
        <button className="primary-btn small" onClick={addItem}>Add</button>
      </div>

      {!hasPlanned && customItems.length === 0 ? (
        <div className="empty">
          <div className="empty-mark">🛒</div>
          <h3>Nothing to shop for yet</h3>
          <p>Plan some meals for this week and their ingredients show up here automatically. You can also add your own items above.</p>
          <button className="primary-btn" style={{ marginTop: 18 }} onClick={onGoPlan}><Calendar size={16} /> Go to planner</button>
        </div>
      ) : (
        <div className="grocery-list">
          {orderedGroups.map((g) => (
            <div className="grocery-cat" key={g}>
              <div className="cat-head">
                <span className="cat-icon">{iconFor(g)}</span> {g}
                {groupBy === "meal" && mealScale[g] && (
                  <span className="scale-hint" title={`Recipe makes ${mealScale[g].serves}, you cook for ${household}`}>
                    ~{fmtFactor(mealScale[g].factor)} recipe
                  </span>
                )}
              </div>
              <div className="cat-items">
                {grouped[g].map((item) => {
                  const checked = isEntryChecked(item);
                  // aisle view shows the consolidated display name + the meals it's for;
                  // meal view shows the exact line as written
                  const label = item.agg ? item.display : item.text;
                  return (
                    <div className={`g-item ${checked ? "checked" : ""}`} key={item.key}>
                      <button className="g-check" onClick={() => toggleEntry(item)}>
                        {checked ? <Check size={14} /> : null}
                      </button>
                      <button className="g-label" onClick={() => toggleEntry(item)}>
                        <span className="g-name">{label}</span>
                        {showMeals && item.agg && item.meals?.length > 0 && (
                          <span className="g-from">
                            {item.meals.length === 1
                              ? item.meals[0]
                              : `${item.meals.length} meals · ${item.meals.join(", ")}`}
                          </span>
                        )}
                      </button>
                      {item.custom && (
                        <button className="g-remove" onClick={() => onRemoveCustom(item.id)}><X size={13} /></button>
                      )}
                    </div>
                  );
                })}
              </div>
            </div>
          ))}
        </div>
      )}
      {hasPlanned && <p className="estimate-note">Ingredients are listed exactly as each recipe wrote them. Where a recipe has a serving count, a "~×recipe" hint shows how much to scale for your {household} servings — quantities aren't auto-multiplied, so adjust as you shop. Tap to check off anything you have or grabbed.</p>}
    </section>
  );
}

// ---------------------------------------------------------------------------
function HistoryView({ plans, getMeal, onView, onPublish }) {
  const thisMonday = mondayOf(new Date());

  // Build past weeks live from plans: any week with meals whose Monday is before this week.
  const weeks = Object.entries(plans)
    .map(([key, slots]) => {
      const monday = mondayFromWeekKey(key);
      if (!monday) return null;
      const filledSlots = Object.entries(slots).filter(([, id]) => getMeal(id));
      if (filledSlots.length === 0) return null;
      return { key, monday, slots: filledSlots };
    })
    .filter((w) => w && w.monday < thisMonday)
    .sort((a, b) => b.monday - a.monday); // most recent first

  if (weeks.length === 0) {
    return (
      <section className="history">
        <div className="empty">
          <div className="empty-mark">🕯️</div>
          <h3>No past weeks yet</h3>
          <p>Once a week you've planned has passed, it shows up here automatically — a record of what you ate and how it went.</p>
        </div>
      </section>
    );
  }

  // order slots within a week by day then meal
  const slotOrder = (slot) => {
    const [day, mt] = slot.split("-");
    return DAYS.indexOf(day) * 10 + MEALS.indexOf(mt);
  };
  const ratingIcon = (r) =>
    r === "up" ? <ThumbsUp size={13} className="hist-up" /> :
    r === "down" ? <ThumbsDown size={13} className="hist-down" /> : null;

  return (
    <section className="history">
      <h2>Past weeks</h2>
      <p className="hist-intro">Weeks roll in here automatically once they've passed. Ratings update live as you rate meals.</p>
      {weeks.map((w) => {
        const meals = w.slots
          .map(([slot, id]) => ({ slot, meal: getMeal(id) }))
          .sort((a, b) => slotOrder(a.slot) - slotOrder(b.slot));
        const totalCal = meals.reduce((s, { meal }) => s + (meal?.nutrition?.calories || 0), 0);
        const loved = meals.filter(({ meal }) => meal?.rating === "up").length;
        return (
          <div className="hist-card" key={w.key}>
            <div className="hist-head">
              <div>
                <h3>{fmtRange(w.monday)}</h3>
                <span className="hist-meta">
                  {meals.length} meal{meals.length !== 1 ? "s" : ""} · {totalCal} cal total
                  {loved > 0 && ` · ${loved} loved`}
                </span>
              </div>
              <div className="hist-actions">
                <button className="text-btn" onClick={() => onPublish(fmtRange(w.monday), plans[w.key])}><BookOpen size={13} /> The Menu</button>
                <button className="text-btn" onClick={() => onView(w.monday)}>View week</button>
              </div>
            </div>
            <div className="hist-meals">
              {meals.map(({ slot, meal }, i) => (
                <span key={i} className="hist-meal">
                  <em>{slot.replace("-", " · ")}</em>
                  {meal.name}
                  {ratingIcon(meal.rating)}
                </span>
              ))}
            </div>
          </div>
        );
      })}
    </section>
  );
}

// ---------------------------------------------------------------------------
const CSS = `
@import url('https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,400;9..144,500;9..144,600;9..144,700&family=Karla:wght@400;500;600;700&display=swap');

.mp-root {
  --cream: #f6f1e9;
  --cream-deep: #ece4d6;
  --ink: #353129;
  --ink-soft: #76705f;
  --sage: #8a9379;
  --sage-deep: #58604a;
  --terra: #b1714e;
  --clay: #a17e58;
  --honey: #c79a52;
  --line: #ddd2bf;
  --card: #fffefa;
  --paper: #faf6ee;
  font-family: 'Karla', sans-serif;
  color: var(--ink);
  background:
    radial-gradient(circle at 12% 6%, rgba(138,147,121,0.10), transparent 40%),
    radial-gradient(circle at 88% 92%, rgba(177,113,78,0.06), transparent 44%),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180' viewBox='0 0 180 180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)' opacity='0.035'/%3E%3C/svg%3E"),
    var(--cream);
  min-height: 100vh;
  padding: 0 0 60px;
}
.mp-root * { box-sizing: border-box; }
.mp-root.loading {
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 14px; color: var(--ink-soft); min-height: 70vh;
}
.spin { animation: spin 1s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }

/* header */
.mp-header {
  position: relative; overflow: hidden;
  display: flex; align-items: center; justify-content: space-between;
  flex-wrap: wrap; gap: 18px;
  padding: 30px clamp(18px, 4vw, 48px) 28px;
  background:
    linear-gradient(180deg, var(--paper) 0%, var(--cream) 100%);
  border-bottom: 1px solid var(--line);
}
/* faint botanical line texture, very subtle on the light ground */
.mp-header::before {
  content: ""; position: absolute; inset: 0; pointer-events: none; opacity: 0.6;
  background-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240' viewBox='0 0 240 240'%3E%3Cg fill='none' stroke='%238a9379' stroke-width='1' opacity='0.10'%3E%3Cpath d='M20 220 C20 150 55 105 110 82 M110 82 C92 110 76 128 48 138 M110 82 C122 110 134 126 158 134'/%3E%3Cpath d='M210 24 C210 90 182 124 132 146 M132 146 C148 124 160 108 186 100'/%3E%3C/g%3E%3C/svg%3E");
  background-size: 280px 280px;
}
.header-glow {
  position: absolute; top: -50%; right: -8%; width: 360px; height: 360px;
  background: radial-gradient(circle, rgba(199,154,82,0.10), transparent 65%);
  pointer-events: none;
}
.brand { position: relative; display: flex; align-items: center; gap: 16px; z-index: 1; }
.brand-mark {
  flex-shrink: 0; width: 58px; height: 58px; display: grid; place-items: center;
  background: linear-gradient(145deg, #fffefa, var(--cream));
  border: 1px solid var(--line); border-radius: 50%;
  box-shadow: 0 4px 14px rgba(88,96,74,0.12), inset 0 1px 0 rgba(255,255,255,0.8);
}
.brand-text { display: flex; flex-direction: column; }
.brand-eyebrow {
  font-family: 'Karla'; font-size: 10.5px; font-weight: 700; letter-spacing: 0.28em;
  text-transform: uppercase; color: var(--sage-deep); margin-bottom: 5px;
  padding-bottom: 5px; border-bottom: 1px solid var(--line); align-self: flex-start;
}
.brand h1 {
  font-family: 'Fraunces', serif; font-weight: 500; font-size: 30px;
  margin: 0; letter-spacing: 0.01em; color: var(--ink); line-height: 1.04;
}
.brand p { margin: 5px 0 0; color: var(--ink-soft); font-size: 13.5px; font-style: italic; }

.tabs {
  position: relative; z-index: 1; display: flex; gap: 3px;
  background: rgba(236,228,214,0.6); padding: 5px; border-radius: 14px;
  border: 1px solid var(--line);
}
.tabs button {
  display: flex; align-items: center; gap: 7px;
  border: none; background: transparent; cursor: pointer;
  font-family: 'Karla'; font-size: 14px; font-weight: 600; color: var(--ink-soft);
  padding: 9px 15px; border-radius: 10px; transition: all 0.18s ease;
}
.tabs button:hover { color: var(--ink); background: rgba(255,255,255,0.5); }
.tabs button.active {
  background: var(--sage-deep); color: #fff;
  box-shadow: 0 3px 10px rgba(88,96,74,0.25);
}

.mp-main { padding: 28px clamp(16px, 4vw, 44px) 0; max-width: 1080px; margin: 0 auto; }

/* cozy warmth: richer cards with a faint paper texture + warmer depth */
.day-card, .idea-card, .lib-card, .grocery-cat, .hist-card, .ideas-form, .modal {
  background-color: var(--card);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='p'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='1.1' numOctaves='2'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23p)' opacity='0.025'/%3E%3C/svg%3E");
  box-shadow: 0 4px 18px rgba(88,80,62,0.09), 0 1px 3px rgba(88,80,62,0.05);
}
/* section headings get the refined farmhouse serif */
.ideas-form h2, .library h2, .history h2, .grocery-head h2 {
  font-weight: 500; letter-spacing: 0.01em;
}

/* planner */
.week-bar {
  display: flex; align-items: center; gap: 12px; margin-bottom: 22px; flex-wrap: wrap;
}
.week-label { display: flex; flex-direction: column; align-items: center; min-width: 150px; }
.week-range { font-family: 'Fraunces', serif; font-size: 20px; font-weight: 600; }
.this-week { font-size: 11px; color: var(--sage-deep); font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; }
.week-actions { margin-left: auto; display: flex; gap: 8px; }
.ghost-btn {
  border: 1px solid var(--line); background: var(--card); cursor: pointer;
  width: 36px; height: 36px; border-radius: 10px; display: grid; place-items: center;
  color: var(--ink-soft); transition: all 0.15s;
}
.ghost-btn:hover { border-color: var(--sage); color: var(--sage-deep); }
.text-btn {
  border: 1px solid var(--line); background: var(--card); cursor: pointer;
  font-family: 'Karla'; font-weight: 600; font-size: 13px; color: var(--sage-deep);
  padding: 8px 14px; border-radius: 10px; display: inline-flex; align-items: center; gap: 6px;
  transition: all 0.15s;
}
.text-btn:hover { background: var(--sage); color: white; border-color: var(--sage); }
.text-btn.center { margin: 14px auto 0; }

.autoplan-btn {
  display: inline-flex; align-items: center; gap: 7px; cursor: pointer;
  font-family: 'Karla'; font-weight: 700; font-size: 13.5px; color: white;
  background: var(--terra); border: none; padding: 9px 16px; border-radius: 10px;
  box-shadow: 0 4px 14px rgba(196,106,77,0.28); transition: all 0.18s;
}
.autoplan-btn:hover { background: #b15a3e; transform: translateY(-1px); }

.plan-banner {
  display: flex; align-items: center; gap: 16px; width: 100%; text-align: left;
  cursor: pointer; margin-bottom: 22px; padding: 18px 20px; border-radius: 16px;
  background: linear-gradient(120deg, rgba(196,106,77,0.10), rgba(211,154,74,0.08));
  border: 1.5px solid rgba(196,106,77,0.25); transition: all 0.18s; font-family: 'Karla';
  animation: rise 0.4s ease;
}
.plan-banner:hover { border-color: var(--terra); transform: translateY(-1px); box-shadow: 0 6px 18px rgba(196,106,77,0.14); }
.plan-banner-icon {
  flex-shrink: 0; width: 46px; height: 46px; border-radius: 13px; display: grid; place-items: center;
  background: var(--terra); color: white; box-shadow: 0 4px 12px rgba(196,106,77,0.3);
}
.plan-banner strong { display: block; font-family: 'Fraunces', serif; font-size: 17px; color: var(--ink); }
.plan-banner span { display: block; font-size: 13.5px; color: var(--ink-soft); margin-top: 2px; line-height: 1.4; }
.plan-banner-arrow { color: var(--terra); flex-shrink: 0; margin-left: auto; }

.chip-row { display: flex; flex-wrap: wrap; gap: 8px; }
.chip {
  font-family: 'Karla'; font-weight: 600; font-size: 13.5px; cursor: pointer;
  padding: 8px 14px; border-radius: 22px; border: 1.5px solid var(--line);
  background: var(--card); color: var(--ink-soft); transition: all 0.15s;
}
.chip:hover { border-color: var(--sage); color: var(--sage-deep); }
.chip.active { background: var(--sage-deep); color: white; border-color: var(--sage-deep); }

.modal.wide { max-width: 540px; }
.modal-sub { font-size: 13px; color: var(--ink-soft); }
.primary-btn.full { width: 100%; margin-top: 6px; }
.autoplan-warn {
  font-size: 12.5px; color: var(--clay); background: rgba(176,137,104,0.10);
  border-radius: 10px; padding: 10px 12px; margin: 4px 0 0; line-height: 1.4;
}
.autoplan-loading {
  display: flex; flex-direction: column; align-items: center; gap: 10px;
  padding: 40px 20px; text-align: center; color: var(--ink-soft);
}
.autoplan-loading strong { font-family: 'Fraunces', serif; font-size: 18px; color: var(--ink); }
.autoplan-loading span { font-size: 13.5px; max-width: 280px; }
.estimate-note.tight { margin-top: 10px; }

.import-toggle {
  display: flex; gap: 6px; background: var(--cream-deep); padding: 5px; border-radius: 12px; margin-bottom: 16px;
}
.import-toggle button {
  flex: 1; display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  border: none; background: transparent; cursor: pointer; font-family: 'Karla';
  font-weight: 600; font-size: 14px; color: var(--ink-soft); padding: 9px; border-radius: 9px;
  transition: all 0.15s;
}
.import-toggle button.active { background: var(--card); color: var(--sage-deep); box-shadow: 0 2px 6px rgba(0,0,0,0.06); }

.dropzone {
  width: 100%; display: flex; flex-direction: column; align-items: center; gap: 6px;
  padding: 36px 20px; cursor: pointer; border-radius: 14px; text-align: center;
  border: 2px dashed var(--line); background: var(--card); color: var(--clay);
  transition: all 0.16s; font-family: 'Karla';
}
.dropzone:hover { border-color: var(--sage); color: var(--sage-deep); background: white; }
.dropzone strong { font-size: 15px; color: var(--ink); margin-top: 4px; }
.dropzone span { font-size: 12.5px; color: var(--ink-soft); max-width: 240px; }

.photo-preview { display: flex; flex-direction: column; align-items: center; gap: 12px; }
.photo-preview img {
  max-width: 100%; max-height: 280px; border-radius: 12px; border: 1px solid var(--line);
  object-fit: contain; background: var(--cream-deep);
}
.field textarea {
  font-family: 'Karla'; font-size: 14.5px; padding: 12px 13px; border-radius: 11px;
  border: 1px solid var(--line); background: var(--cream); color: var(--ink); outline: none;
  resize: vertical; line-height: 1.5; transition: border 0.15s;
}
.field textarea:focus { border-color: var(--sage); background: white; }

/* grocery */
.grocery-head { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 16px; gap: 12px; flex-wrap: wrap; }
.grocery-head h2 { font-family: 'Fraunces', serif; font-weight: 600; font-size: 22px; margin: 0 0 2px; }
.grocery-range { font-size: 13px; color: var(--ink-soft); }
.grocery-head-actions { display: flex; align-items: center; gap: 10px; }
.view-toggle { display: flex; gap: 3px; background: var(--cream-deep); padding: 4px; border-radius: 10px; }
.view-toggle button {
  border: none; background: transparent; cursor: pointer; font-family: 'Karla';
  font-weight: 600; font-size: 13px; color: var(--ink-soft); padding: 7px 13px; border-radius: 8px;
  transition: all 0.15s;
}
.view-toggle button.active { background: var(--card); color: var(--sage-deep); box-shadow: 0 2px 6px rgba(0,0,0,0.06); }
.add-item {
  display: flex; align-items: center; gap: 10px; background: var(--card);
  border: 1px solid var(--line); border-radius: 13px; padding: 6px 6px 6px 14px;
  margin-bottom: 22px; color: var(--ink-soft);
}
.add-item input {
  flex: 1; border: none; background: transparent; outline: none; font-family: 'Karla';
  font-size: 14.5px; color: var(--ink); padding: 7px 0;
}
.grocery-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 18px; align-items: start; }
.grocery-cat {
  background: var(--card); border: 1px solid var(--line); border-radius: 16px;
  padding: 16px 16px 8px; box-shadow: 0 3px 12px rgba(94,110,80,0.06);
  animation: rise 0.35s ease backwards;
}
.cat-head {
  font-family: 'Fraunces', serif; font-weight: 600; font-size: 15px; color: var(--sage-deep);
  display: flex; align-items: center; gap: 8px; padding-bottom: 10px; margin-bottom: 4px;
  border-bottom: 1px solid var(--line);
}
.cat-icon { font-size: 17px; }
.cat-items { display: flex; flex-direction: column; }
.g-item {
  display: flex; align-items: center; gap: 11px; padding: 9px 2px;
  border-bottom: 1px solid rgba(224,213,192,0.5); transition: opacity 0.15s;
}
.g-item:last-child { border-bottom: none; }
.g-check {
  flex-shrink: 0; width: 22px; height: 22px; border-radius: 7px; cursor: pointer;
  border: 1.6px solid var(--line); background: var(--cream); display: grid; place-items: center;
  color: white; transition: all 0.15s;
}
.g-item.checked .g-check { background: var(--sage-deep); border-color: var(--sage-deep); }
.g-label { flex: 1; text-align: left; border: none; background: transparent; cursor: pointer; font-family: 'Karla'; padding: 0; display: flex; flex-direction: column; gap: 1px; }
.g-name { font-size: 14.5px; font-weight: 600; color: var(--ink); transition: all 0.15s; }
.g-from { font-size: 11.5px; color: var(--ink-soft); }
.g-item.checked .g-name { text-decoration: line-through; color: var(--ink-soft); font-weight: 500; }
.g-item.checked { opacity: 0.6; }
.g-remove {
  flex-shrink: 0; border: none; background: transparent; cursor: pointer; color: var(--ink-soft);
  padding: 4px; border-radius: 6px; opacity: 0.5;
}
.g-remove:hover { opacity: 1; background: var(--cream-deep); color: var(--terra); }

/* meal-visibility setting */
.meals-setting { display: flex; align-items: center; gap: 12px; margin-bottom: 20px; flex-wrap: wrap; }
.meals-setting-label { font-size: 13px; font-weight: 700; color: var(--ink-soft); text-transform: uppercase; letter-spacing: 0.05em; }
.seg { display: flex; gap: 3px; background: var(--cream-deep); padding: 4px; border-radius: 11px; }
.seg button {
  border: none; background: transparent; cursor: pointer; font-family: 'Karla';
  font-weight: 600; font-size: 13px; color: var(--ink-soft); padding: 8px 14px; border-radius: 8px;
  transition: all 0.15s;
}
.seg button.active { background: var(--card); color: var(--sage-deep); box-shadow: 0 2px 6px rgba(0,0,0,0.06); }

.household-set {
  display: inline-flex; align-items: center; gap: 7px; font-size: 13px; color: var(--ink-soft);
  background: var(--card); border: 1px solid var(--line); border-radius: 11px; padding: 6px 12px;
}
.household-set input {
  width: 46px; text-align: center; font-family: 'Karla'; font-weight: 700; font-size: 14px;
  color: var(--ink); border: 1px solid var(--line); border-radius: 7px; padding: 4px; outline: none;
  background: var(--cream);
}
.household-set input:focus { border-color: var(--sage); background: #fff; }

/* avoid-list editor */
.avoid-wrap { position: relative; display: inline-block; }
.avoid-trigger {
  cursor: pointer; font-family: 'Karla'; font-weight: 600; font-size: 13px; color: var(--ink-soft);
  background: var(--card); border: 1px solid var(--line); border-radius: 11px; padding: 7px 13px;
  transition: all 0.15s;
}
.avoid-trigger:hover { border-color: var(--terra); color: var(--terra); }
.avoid-backdrop { position: fixed; inset: 0; z-index: 40; }
.avoid-pop {
  position: absolute; top: calc(100% + 8px); left: 0; z-index: 41; width: 300px;
  background: var(--card); border: 1px solid var(--line); border-radius: 14px; padding: 16px;
  box-shadow: 0 12px 36px rgba(53,49,41,0.2);
}
.avoid-pop-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }
.avoid-pop-head strong { font-family: 'Fraunces', serif; font-size: 16px; }
.ghost-btn.sm { width: 28px; height: 28px; }
.avoid-hint { font-size: 12.5px; color: var(--ink-soft); margin: 0 0 12px; line-height: 1.4; }
.avoid-add { display: flex; gap: 7px; margin-bottom: 12px; }
.avoid-add input {
  flex: 1; min-width: 0; font-family: 'Karla'; font-size: 14px; padding: 9px 11px; border-radius: 9px;
  border: 1px solid var(--line); background: var(--cream); color: var(--ink); outline: none;
}
.avoid-add input:focus { border-color: var(--sage); background: #fff; }
.avoid-chips { display: flex; flex-wrap: wrap; gap: 7px; }
.avoid-chip {
  display: inline-flex; align-items: center; gap: 5px; font-size: 13px; font-weight: 600;
  color: var(--terra); background: rgba(177,113,78,0.12); border: 1px solid rgba(177,113,78,0.3);
  padding: 4px 6px 4px 11px; border-radius: 20px;
}
.avoid-chip button { border: none; background: none; cursor: pointer; color: var(--terra); display: flex; padding: 2px; border-radius: 50%; }
.avoid-chip button:hover { background: rgba(177,113,78,0.2); }
.avoid-empty { font-size: 12.5px; color: var(--ink-soft); font-style: italic; margin: 0; }

.avoid-flag {
  font-size: 13px; color: #8a4a2c; background: rgba(177,113,78,0.12);
  border: 1px solid rgba(177,113,78,0.3); border-radius: 10px; padding: 9px 12px; margin-top: 14px;
}
.assign-avoid { font-size: 11.5px; color: var(--terra); margin-top: 3px; font-weight: 600; }


.serves-input { display: flex; align-items: center; gap: 9px; }
.serves-input input {
  width: 80px; font-family: 'Karla'; font-size: 14.5px; padding: 10px 12px; border-radius: 11px;
  border: 1px solid var(--line); background: var(--cream); color: var(--ink); outline: none;
}
.serves-input input:focus { border-color: var(--sage); background: #fff; }
.serves-input span { font-size: 13px; color: var(--ink-soft); }

.scale-hint {
  margin-left: auto; font-family: 'Karla'; font-size: 11.5px; font-weight: 700;
  color: #9a7322; background: rgba(199,154,82,0.18); padding: 3px 9px; border-radius: 20px;
  text-transform: none; letter-spacing: 0;
}
.scale-inline { color: #9a7322; font-weight: 700; }

/* day stack */
.day-stack { display: grid; grid-template-columns: 1fr; gap: 14px; }
.day-card {
  background: var(--card); border: 1px solid var(--line); border-radius: 16px;
  padding: 16px 18px; box-shadow: 0 3px 12px rgba(79,96,67,0.07);
  animation: rise 0.35s ease backwards;
}
.day-card.today { border-color: var(--sage); box-shadow: 0 4px 16px rgba(79,96,67,0.14); }
.day-card-head {
  display: flex; justify-content: space-between; align-items: center;
  padding-bottom: 12px; margin-bottom: 12px; border-bottom: 1px solid var(--line);
}
.day-card-title { display: flex; align-items: center; gap: 10px; }
.day-full { font-family: 'Fraunces', serif; font-weight: 600; font-size: 18px; color: var(--ink); }
.today-tag {
  font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em;
  color: white; background: var(--sage-deep); padding: 3px 8px; border-radius: 20px;
}
.day-card-cal { font-size: 12.5px; color: var(--ink-soft); font-weight: 600; }
.day-slots { display: flex; flex-direction: column; gap: 9px; }

.slot { display: flex; flex-direction: column; gap: 5px; }
.slot-label {
  font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em;
  color: var(--clay);
}
.slot-primary .slot-label { color: var(--terra); }
.slot-meal {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  background: var(--cream); border: 1px solid var(--line); border-radius: 12px;
  padding: 12px 14px; transition: all 0.15s;
}
.slot-primary .slot-meal {
  background: linear-gradient(135deg, rgba(126,144,104,0.12), rgba(204,149,64,0.07));
  border-color: rgba(126,144,104,0.35); border-left: 4px solid var(--sage);
  padding: 15px 16px;
}
.slot-meal-text { display: flex; flex-direction: column; gap: 3px; }
.slot-meal-name { font-weight: 600; font-size: 15px; line-height: 1.25; color: var(--ink); }
.slot-primary .slot-meal-name { font-size: 16.5px; }
.slot-meal-cal { font-size: 12px; color: var(--ink-soft); }
.slot-x {
  flex-shrink: 0; border: none; background: transparent; color: var(--ink-soft);
  cursor: pointer; padding: 5px; border-radius: 8px; opacity: 0.55; transition: all 0.15s;
}
.slot-x:hover { opacity: 1; background: var(--cream-deep); color: var(--terra); }
.slot-add {
  display: flex; align-items: center; gap: 8px; width: 100%; cursor: pointer;
  border: 1.5px dashed var(--line); background: rgba(253,249,239,0.5); border-radius: 12px;
  padding: 13px 14px; color: var(--clay); font-family: 'Karla'; font-weight: 600; font-size: 14px;
  transition: all 0.15s;
}
.slot-add:hover { border-color: var(--sage); color: var(--sage-deep); background: var(--card); }
.slot-primary .slot-add { padding: 16px; font-size: 15px; }

.slot-empty-actions { display: flex; gap: 8px; align-items: stretch; }
.slot-empty-actions .slot-add { flex: 1; }
.slot-surprise-icon {
  display: grid; place-items: center; cursor: pointer; flex-shrink: 0; width: 46px;
  border: 1.5px dashed rgba(199,154,82,0.5); background: rgba(199,154,82,0.08); border-radius: 12px;
  color: var(--clay); transition: all 0.15s;
}
.slot-surprise-icon:hover { border-color: var(--honey); color: #9a7322; background: rgba(199,154,82,0.18); }

.slot-prompt {
  display: flex; flex-direction: column; gap: 8px; padding: 12px;
  background: var(--card); border: 1px solid var(--line); border-radius: 12px;
}
.slot-prompt input {
  font-family: 'Karla'; font-size: 14px; padding: 9px 11px; border-radius: 9px;
  border: 1px solid var(--line); background: var(--cream); color: var(--ink); outline: none;
}
.slot-prompt input:focus { border-color: var(--sage); background: #fff; }
.slot-prompt-row { display: flex; align-items: center; gap: 8px; }
.slot-err { font-size: 12px; color: var(--terra); }
.slot-generating {
  display: flex; align-items: center; gap: 9px; padding: 15px;
  color: var(--sage-deep); font-family: 'Karla'; font-weight: 600; font-size: 14px;
  background: rgba(138,147,121,0.10); border: 1px dashed rgba(138,147,121,0.4); border-radius: 12px;
}

.day-extra-add { display: flex; gap: 7px; flex-wrap: wrap; margin-top: 2px; }
.extra-add-btn {
  display: inline-flex; align-items: center; gap: 5px; cursor: pointer;
  border: 1px solid var(--line); background: transparent; color: var(--ink-soft);
  font-family: 'Karla'; font-weight: 600; font-size: 12.5px; padding: 6px 11px; border-radius: 20px;
  text-transform: capitalize; transition: all 0.15s;
}
.extra-add-btn:hover { border-color: var(--sage); color: var(--sage-deep); background: var(--card); }

.estimate-note { font-size: 12px; color: var(--ink-soft); font-style: italic; margin-top: 18px; text-align: center; }

/* macro pills */
.nutri-row { display: flex; gap: 7px; flex-wrap: wrap; margin: 12px 0; }
.nutri-row.compact { gap: 5px; margin: 9px 0; }
.macro-pill {
  display: inline-flex; flex-direction: column; align-items: center;
  padding: 6px 11px; border-radius: 10px; background: var(--cream);
  border: 1px solid var(--line); line-height: 1.1; min-width: 52px;
}
.macro-pill strong { font-size: 14px; color: var(--pill); font-weight: 700; }
.macro-pill span { font-size: 10px; color: var(--ink-soft); text-transform: uppercase; letter-spacing: 0.04em; margin-top: 2px; }

/* ideas */
.ideas-form {
  background: var(--card); border: 1px solid var(--line); border-radius: 18px;
  padding: 26px; box-shadow: 0 6px 22px rgba(94,110,80,0.08); margin-bottom: 26px;
}
.ideas-form h2, .library h2, .history h2 { font-family: 'Fraunces', serif; font-weight: 600; font-size: 22px; margin: 0 0 4px; }
.ideas-form .sub { color: var(--ink-soft); margin: 0 0 18px; font-size: 14px; }
.field { display: flex; flex-direction: column; gap: 6px; margin-bottom: 14px; }
.field label { font-size: 13px; font-weight: 700; color: var(--ink-soft); }
.field label .opt { font-weight: 400; font-style: italic; }
.field input, .field select {
  font-family: 'Karla'; font-size: 14.5px; padding: 11px 13px; border-radius: 11px;
  border: 1px solid var(--line); background: var(--cream); color: var(--ink); outline: none;
  transition: border 0.15s;
}
.field input:focus, .field select:focus { border-color: var(--sage); background: white; }
.field-row { display: flex; gap: 12px; }
.field-row .field { flex: 0 0 auto; }
.field-row .grow { flex: 1; }

.primary-btn {
  display: inline-flex; align-items: center; justify-content: center; gap: 8px;
  background: var(--sage-deep); color: white; border: none; cursor: pointer;
  font-family: 'Karla'; font-weight: 700; font-size: 14.5px; padding: 12px 20px;
  border-radius: 12px; transition: all 0.18s; box-shadow: 0 4px 14px rgba(94,110,80,0.25);
}
.primary-btn:hover:not(:disabled) { background: #4e5d42; transform: translateY(-1px); }
.primary-btn:disabled { opacity: 0.7; cursor: default; }
.primary-btn.small { padding: 9px 15px; font-size: 13.5px; box-shadow: none; }
.error { color: var(--terra); font-size: 13.5px; margin: 10px 0 0; }

.idea-results { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 16px; }
.idea-card, .lib-card {
  background: var(--card); border: 1px solid var(--line); border-radius: 16px;
  padding: 18px; box-shadow: 0 3px 12px rgba(94,110,80,0.06); position: relative;
  animation: rise 0.4s ease backwards;
}
@keyframes rise { from { opacity: 0; transform: translateY(8px); } }
.idea-card-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 10px; }
.idea-card h3, .lib-card h3 { font-family: 'Fraunces', serif; font-weight: 600; font-size: 17px; margin: 0; line-height: 1.2; }
.idea-desc, .lib-desc { color: var(--ink-soft); font-size: 13.5px; margin: 7px 0 0; line-height: 1.45; }
.save-btn {
  display: inline-flex; align-items: center; gap: 5px; flex-shrink: 0;
  border: 1px solid var(--line); background: var(--cream); cursor: pointer;
  font-family: 'Karla'; font-weight: 600; font-size: 12.5px; color: var(--terra);
  padding: 6px 11px; border-radius: 9px; transition: all 0.15s;
}
.save-btn:hover:not(:disabled) { background: var(--terra); color: white; border-color: var(--terra); }
.save-btn.saved { color: var(--sage-deep); border-color: var(--sage); cursor: default; }
.ingredients { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 4px; }
.ing-tag {
  font-size: 11.5px; background: var(--cream-deep); color: var(--ink-soft);
  padding: 3px 9px; border-radius: 20px;
}

/* speed / meal-type tags */
.speed-badge {
  display: inline-flex; align-items: center; gap: 4px; vertical-align: middle;
  margin-right: 6px; padding: 2px 7px 2px 5px; border-radius: 20px;
  font-size: 10.5px; font-weight: 700; letter-spacing: 0.02em; line-height: 1;
}
.speed-badge svg { display: block; }
.speed-quick { background: rgba(138,147,121,0.20); color: var(--sage-deep); }
.speed-premade { background: rgba(199,154,82,0.20); color: #9a7322; }

.speed-pick { display: flex; gap: 8px; flex-wrap: wrap; }
.speed-opt {
  display: inline-flex; align-items: center; gap: 6px; cursor: pointer;
  font-family: 'Karla'; font-weight: 600; font-size: 13.5px; color: var(--ink-soft);
  padding: 9px 14px; border-radius: 11px; border: 1.5px solid var(--line); background: var(--card);
  transition: all 0.15s;
}
.speed-opt:hover { border-color: var(--sage); color: var(--sage-deep); }
.speed-opt.active { background: var(--sage-deep); color: #fff; border-color: var(--sage-deep); }

.speed-filter { display: flex; gap: 7px; flex-wrap: wrap; margin-bottom: 18px; }
.speed-filter.compact { margin: 0 0 14px; }
.speed-filter button {
  display: inline-flex; align-items: center; gap: 5px; cursor: pointer;
  font-family: 'Karla'; font-weight: 600; font-size: 13px; color: var(--ink-soft);
  padding: 7px 13px; border-radius: 20px; border: 1px solid var(--line); background: var(--card);
  transition: all 0.15s;
}
.speed-filter button:hover { border-color: var(--sage); color: var(--sage-deep); }
.speed-filter button.active { background: var(--sage-deep); color: #fff; border-color: var(--sage-deep); }
.filter-divider { width: 1px; align-self: stretch; background: var(--line); margin: 2px 4px; }

/* thumbs rating */
.rating { display: inline-flex; gap: 6px; align-items: center; }
.rate-btn {
  display: inline-flex; align-items: center; justify-content: center; cursor: pointer;
  border: 1px solid var(--line); background: var(--card); color: var(--ink-soft);
  border-radius: 9px; padding: 6px; transition: all 0.15s;
}
.rating.sm .rate-btn { padding: 5px; border-radius: 8px; }
.rate-btn:hover { border-color: var(--sage); }
.rate-btn.up:hover { color: var(--sage-deep); }
.rate-btn.down:hover { color: var(--terra); }
.rate-btn.up.on { background: var(--sage-deep); border-color: var(--sage-deep); color: #fff; }
.rate-btn.down.on { background: var(--terra); border-color: var(--terra); color: #fff; }

.lib-card-foot { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--line); }

.rate-prompt {
  display: flex; align-items: center; gap: 9px; margin-top: 8px; padding: 8px 10px;
  background: rgba(199,154,82,0.10); border: 1px dashed rgba(199,154,82,0.4); border-radius: 10px;
}
.rate-prompt-q { font-size: 12.5px; font-weight: 600; color: var(--clay); }
.slot-meal-wrap { display: flex; flex-direction: column; }

.lib-card-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; }
.lib-card-head h3 { flex: 1; }
.lib-card-head .speed-badge { margin-right: 0; flex-shrink: 0; }

/* library */
.lib-bar { display: flex; gap: 12px; margin-bottom: 20px; align-items: center; }
.search {
  display: flex; align-items: center; gap: 8px; flex: 1; background: var(--card);
  border: 1px solid var(--line); border-radius: 12px; padding: 0 13px; color: var(--ink-soft);
}
.search.inline { margin-bottom: 14px; }
.search input { border: none; background: transparent; outline: none; font-family: 'Karla'; font-size: 14.5px; padding: 11px 0; flex: 1; color: var(--ink); }
.lib-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 16px; }
.lib-card { padding-right: 40px; }
.lib-del {
  position: absolute; top: 14px; right: 14px; border: none; background: transparent;
  color: var(--ink-soft); cursor: pointer; padding: 4px; border-radius: 7px; opacity: 0.55;
}
.lib-del:hover { opacity: 1; background: var(--cream-deep); color: var(--terra); }

.empty { text-align: center; padding: 70px 20px; color: var(--ink-soft); }
.empty-mark { font-size: 44px; margin-bottom: 12px; }
.empty h3 { font-family: 'Fraunces', serif; font-weight: 600; font-size: 20px; color: var(--ink); margin: 0 0 6px; }
.empty p { font-size: 14px; max-width: 380px; margin: 0 auto; line-height: 1.5; }

/* modal */
.modal-backdrop {
  position: fixed; inset: 0; background: rgba(46,42,35,0.4); backdrop-filter: blur(3px);
  display: grid; place-items: center; padding: 20px; z-index: 50; animation: fade 0.2s ease;
}
@keyframes fade { from { opacity: 0; } }
.modal {
  background: var(--cream); border-radius: 20px; padding: 24px; width: 100%; max-width: 460px;
  max-height: 88vh; overflow-y: auto; box-shadow: 0 20px 60px rgba(0,0,0,0.25);
  animation: pop 0.25s cubic-bezier(0.2,0.9,0.3,1.2);
}
@keyframes pop { from { opacity: 0; transform: scale(0.96); } }
.modal-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 18px; }
.modal-head h2 { font-family: 'Fraunces', serif; font-weight: 600; font-size: 20px; margin: 0; }
.macro-inputs { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }

/* clickable meal names */
.meal-name-link {
  border: none; background: none; padding: 0; margin: 0; cursor: pointer;
  font: inherit; color: inherit; text-align: left; transition: color 0.15s;
  border-bottom: 1px dotted transparent;
}
.meal-name-link:hover { color: var(--sage-deep); border-bottom-color: var(--sage); }

/* recipe view modal */
.recipe-modal { max-width: 560px; }
.recipe-hero { position: relative; margin: 0 -24px 18px; height: 200px; overflow: hidden; border-radius: 20px 20px 0 0; }

.meal-photo { background-size: cover; background-position: center; }

/* recipe hero (only shown when a real photo exists) */
.recipe-modal.has-hero { padding-top: 0; }
.recipe-modal.has-hero .recipe-hero { margin-top: 0; }
.recipe-hero-img { width: 100%; height: 100%; }
.recipe-close {
  position: absolute; top: 12px; right: 12px; background: rgba(255,255,255,0.9); backdrop-filter: blur(4px);
}
.recipe-hero-photo-actions { position: absolute; bottom: 10px; right: 12px; display: flex; gap: 6px; }
.photo-btn {
  display: inline-flex; align-items: center; gap: 5px; cursor: pointer; font-family: 'Karla';
  font-weight: 600; font-size: 12px; color: var(--ink); background: rgba(255,255,255,0.92);
  border: 1px solid var(--line); padding: 6px 10px; border-radius: 8px; backdrop-filter: blur(4px);
}
.photo-btn:hover { background: #fff; }
.recipe-foot-actions { display: flex; gap: 10px; }

/* ---- Publication / magazine view ---- */
.pub-overlay {
  position: fixed; inset: 0; z-index: 60; background: var(--cream); overflow-y: auto;
}
.pub-bar {
  position: sticky; top: 0; z-index: 2; display: flex; align-items: center; justify-content: space-between;
  padding: 12px clamp(16px, 4vw, 40px); background: rgba(246,241,233,0.92); backdrop-filter: blur(8px);
  border-bottom: 1px solid var(--line);
}
.pub-bar-title { font-family: 'Fraunces', serif; font-size: 16px; font-weight: 600; color: var(--ink-soft); }
.pub-page { max-width: 760px; margin: 0 auto; padding: 40px clamp(20px, 5vw, 56px) 80px; }

.pub-cover { text-align: center; padding: 30px 0 44px; }
.pub-cover-slim { padding: 16px 0 28px; }
.pub-cover-eyebrow {
  font-family: 'Karla'; font-size: 12px; font-weight: 700; letter-spacing: 0.32em; text-transform: uppercase;
  color: var(--sage-deep); margin-bottom: 16px;
}
.pub-cover-title {
  font-family: 'Fraunces', serif; font-weight: 500; font-size: clamp(34px, 7vw, 52px); line-height: 1.05;
  margin: 0; color: var(--ink); letter-spacing: 0.01em;
}
.pub-cover-rule { display: flex; align-items: center; justify-content: center; gap: 14px; margin: 26px 0 0; }
.pub-cover-rule span { height: 1px; width: 64px; background: var(--clay); }
.pub-cover-dot { width: 6px !important; height: 6px; border-radius: 50%; background: var(--honey) !important; }
.pub-cover-sub { font-family: 'Fraunces', serif; font-style: italic; font-size: 17px; color: var(--ink-soft); margin: 18px 0 0; }

.pub-recipe { padding: 40px 0; border-top: 1px solid var(--line); break-inside: avoid; }
.pub-recipe:first-of-type { border-top: none; }
.pub-slot {
  font-family: 'Karla'; font-size: 11.5px; font-weight: 700; letter-spacing: 0.18em; text-transform: uppercase;
  color: var(--honey); margin-bottom: 14px;
}
.pub-photo-banner {
  width: 100%; height: 280px; border-radius: 14px; background-size: cover; background-position: center;
  margin-bottom: 26px; border: 1px solid var(--line);
}
.pub-recipe-head { margin-bottom: 28px; }
.pub-recipe-title { font-family: 'Fraunces', serif; font-weight: 500; font-size: clamp(28px, 5vw, 38px); line-height: 1.08; margin: 0; color: var(--ink); }
.pub-recipe-desc { font-family: 'Fraunces', serif; font-style: italic; font-size: 17px; color: var(--ink-soft); margin: 12px 0 0; line-height: 1.5; max-width: 90%; }
.pub-recipe-stats { display: flex; flex-wrap: wrap; gap: 28px; margin-top: 22px; padding-top: 18px; border-top: 1px solid var(--line); }
.pub-recipe-stats span { display: flex; flex-direction: column; font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--ink-soft); }
.pub-recipe-stats strong { font-family: 'Fraunces', serif; font-size: 21px; font-weight: 600; color: var(--sage-deep); text-transform: none; letter-spacing: 0; }

.pub-recipe-body { display: grid; grid-template-columns: 1fr 1.4fr; gap: 36px; }
.pub-ingredients h3, .pub-method h3 {
  font-family: 'Fraunces', serif; font-weight: 600; font-size: 15px; color: var(--ink);
  text-transform: uppercase; letter-spacing: 0.08em; margin: 0 0 14px; padding-bottom: 8px;
  border-bottom: 2px solid var(--sage); display: inline-block;
}
.pub-scaled { font-family: 'Karla'; font-size: 12px; font-weight: 600; color: var(--clay); text-transform: none; letter-spacing: 0; }
.pub-ingredients ul { list-style: none; margin: 0; padding: 0; }
.pub-ingredients li { font-size: 14.5px; line-height: 1.8; color: var(--ink); padding-left: 16px; position: relative; }
.pub-ingredients li::before { content: ""; position: absolute; left: 0; top: 11px; width: 5px; height: 5px; border-radius: 50%; background: var(--honey); }
.pub-nutrition { display: flex; flex-wrap: wrap; gap: 14px; margin-top: 18px; padding-top: 14px; border-top: 1px dashed var(--line); }
.pub-nutrition span { font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; color: var(--ink-soft); }
.pub-nutrition strong { color: var(--terra); font-size: 13px; }
.pub-method ol { margin: 0; padding: 0; list-style: none; counter-reset: step; }
.pub-method li {
  font-size: 15px; line-height: 1.6; color: var(--ink); margin-bottom: 16px; padding-left: 42px; position: relative; counter-increment: step;
}
.pub-method li::before {
  content: counter(step); position: absolute; left: 0; top: -2px; width: 28px; height: 28px;
  display: grid; place-items: center; background: var(--sage-deep); color: #fff; border-radius: 50%;
  font-family: 'Fraunces', serif; font-size: 14px; font-weight: 600;
}
.pub-nomethod { font-size: 14px; color: var(--ink-soft); font-style: italic; }
.pub-foot {
  text-align: center; font-family: 'Fraunces', serif; font-style: italic; font-size: 16px;
  color: var(--clay); margin-top: 50px; padding-top: 30px; border-top: 1px solid var(--line);
}
.hist-actions { display: flex; gap: 8px; flex-shrink: 0; }

@media (max-width: 600px) {
  .pub-photo-banner { height: 200px; }
  .pub-recipe-stats { gap: 20px; }
  .pub-recipe-body { grid-template-columns: 1fr; gap: 26px; }

  /* Week bar: centered date row on top, full-width actions below */
  .week-bar { justify-content: center; gap: 8px 10px; }
  .week-label { min-width: 0; flex: 1; }
  .week-actions {
    margin-left: 0; order: 3; width: 100%; flex-basis: 100%;
    justify-content: center; flex-wrap: wrap;
  }
  .week-actions .autoplan-btn { flex: 1 1 100%; justify-content: center; }
  .week-actions .text-btn { flex: 1 1 auto; justify-content: center; }
}

@media print {
  body * { visibility: hidden; }
  .pub-overlay, .pub-overlay * { visibility: visible; }
  .pub-overlay { position: absolute; overflow: visible; background: #fff; }
  .pub-bar { display: none !important; }
  .pub-page { max-width: none; padding: 0; }
  .pub-recipe { break-inside: avoid; page-break-inside: avoid; }
  .pub-cover { page-break-after: always; }
  .pub-method li::before, .pub-ingredients li::before { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
  .pub-nutrition strong, .pub-recipe-stats strong { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
}
.recipe-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-bottom: 14px; }
.recipe-meta-item {
  font-size: 12.5px; font-weight: 600; color: var(--ink-soft);
  background: var(--cream); border: 1px solid var(--line); padding: 4px 10px; border-radius: 20px;
}
.recipe-meta-item.total { color: var(--sage-deep); }
.recipe-section { margin-top: 18px; }
.recipe-section h3 {
  font-family: 'Fraunces', serif; font-weight: 600; font-size: 16px; margin: 0 0 10px;
  color: var(--ink); padding-bottom: 6px; border-bottom: 1px solid var(--line);
}
.scaled-note { font-family: 'Karla'; font-size: 12px; font-weight: 600; color: var(--clay); }
.recipe-ingredients { margin: 0; padding-left: 20px; }
.recipe-ingredients li { font-size: 14.5px; line-height: 1.7; color: var(--ink); }
.recipe-steps { margin: 0; padding-left: 22px; }
.recipe-steps li { font-size: 14.5px; line-height: 1.55; color: var(--ink); margin-bottom: 10px; padding-left: 4px; }
.recipe-tinynote { font-size: 12px; color: var(--ink-soft); font-style: italic; margin: 8px 0 0; }
.recipe-notes {
  margin-top: 14px; padding: 12px 14px; background: var(--cream); border: 1px solid var(--line);
  border-radius: 11px; font-size: 14px; color: var(--ink); line-height: 1.5; white-space: pre-wrap;
}
.recipe-notes-label {
  display: block; font-family: 'Karla'; font-size: 11px; font-weight: 700; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--clay); margin-bottom: 5px;
}
.ai-note {
  display: flex; align-items: flex-start; gap: 7px; margin-top: 14px; padding: 10px 12px;
  font-size: 12.5px; line-height: 1.45; color: var(--ink-soft);
  background: rgba(138,147,121,0.10); border: 1px solid rgba(138,147,121,0.28); border-radius: 10px;
}
.ai-note svg { flex-shrink: 0; margin-top: 1px; color: var(--sage-deep); }
.field textarea {
  font-family: 'Karla'; font-size: 16px; padding: 11px 13px; border-radius: 11px; width: 100%;
  border: 1px solid var(--line); background: var(--cream); color: var(--ink); outline: none;
  resize: vertical; line-height: 1.4; box-sizing: border-box;
}
.field textarea:focus { border-color: var(--sage); background: #fff; }
.recipe-foot {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  margin-top: 22px; padding-top: 16px; border-top: 1px solid var(--line);
}

/* structured ingredient & step rows in the form */
.field-row.three { display: flex; gap: 10px; }
.field-row.three .field { flex: 1; margin-bottom: 14px; }
.ing-rows { display: flex; flex-direction: column; gap: 7px; }
.ing-row { display: flex; gap: 7px; align-items: center; }
.ing-row input {
  font-family: 'Karla'; font-size: 14px; padding: 9px 10px; border-radius: 9px;
  border: 1px solid var(--line); background: var(--cream); color: var(--ink); outline: none;
}
.ing-row input:focus { border-color: var(--sage); background: #fff; }
.ing-qty { width: 56px; }
.ing-unit { width: 78px; }
.ing-item { flex: 1; min-width: 0; }
.step-rows { display: flex; flex-direction: column; gap: 8px; }
.step-row { display: flex; gap: 8px; align-items: flex-start; }
.step-num {
  flex-shrink: 0; width: 24px; height: 24px; margin-top: 6px; display: grid; place-items: center;
  background: var(--sage-deep); color: #fff; border-radius: 50%; font-size: 12px; font-weight: 700;
}
.step-row textarea {
  flex: 1; font-family: 'Karla'; font-size: 14px; padding: 9px 11px; border-radius: 9px;
  border: 1px solid var(--line); background: var(--cream); color: var(--ink); outline: none;
  resize: vertical; line-height: 1.4;
}
.step-row textarea:focus { border-color: var(--sage); background: #fff; }
.row-x {
  flex-shrink: 0; border: none; background: transparent; color: var(--ink-soft); cursor: pointer;
  padding: 6px; border-radius: 7px; opacity: 0.55; margin-top: 2px;
}
.row-x:hover { opacity: 1; background: var(--cream-deep); color: var(--terra); }
.add-row-btn {
  display: inline-flex; align-items: center; gap: 6px; margin-top: 9px; cursor: pointer;
  font-family: 'Karla'; font-weight: 600; font-size: 13px; color: var(--sage-deep);
  background: none; border: 1px dashed var(--line); border-radius: 9px; padding: 8px 13px;
  transition: all 0.15s;
}
.add-row-btn:hover { border-color: var(--sage); background: var(--card); }
.macro-inputs > div { display: flex; align-items: center; gap: 7px; }
.macro-inputs input { width: 100%; font-family: 'Karla'; font-size: 14px; padding: 9px 11px; border-radius: 10px; border: 1px solid var(--line); background: white; outline: none; }
.macro-inputs span { font-size: 12px; color: var(--ink-soft); white-space: nowrap; }

.assign-list { display: flex; flex-direction: column; gap: 7px; max-height: 320px; overflow-y: auto; }
.assign-item {
  display: flex; justify-content: space-between; align-items: center; text-align: left;
  border: 1px solid var(--line); background: var(--card); cursor: pointer;
  padding: 12px 14px; border-radius: 12px; transition: all 0.14s; color: var(--ink);
}
.assign-item:hover { border-color: var(--sage); background: white; transform: translateX(2px); }
.assign-name { font-weight: 600; font-size: 14.5px; }
.assign-cal { font-size: 12px; color: var(--ink-soft); margin-top: 2px; }
.muted { color: var(--ink-soft); font-size: 13.5px; text-align: center; padding: 16px; }

/* history */
.hist-card {
  background: var(--card); border: 1px solid var(--line); border-radius: 16px;
  padding: 18px 20px; margin-bottom: 14px; box-shadow: 0 3px 12px rgba(94,110,80,0.06);
}
.hist-head { display: flex; justify-content: space-between; align-items: flex-start; }
.hist-head h3 { font-family: 'Fraunces', serif; font-weight: 600; font-size: 18px; margin: 0; }
.hist-meta { font-size: 12.5px; color: var(--ink-soft); }
.hist-intro { font-size: 13px; color: var(--ink-soft); font-style: italic; margin: -2px 0 18px; }
.hist-meals { display: flex; flex-direction: column; gap: 5px; margin-top: 12px; }
.hist-meal { font-size: 13.5px; display: flex; align-items: center; gap: 5px; }
.hist-meal em { color: var(--sage-deep); font-style: normal; font-weight: 600; text-transform: capitalize; margin-right: 2px; }
.hist-up { color: var(--sage-deep); }
.hist-down { color: var(--terra); }

@media (min-width: 720px) {
  .day-stack { grid-template-columns: 1fr 1fr; gap: 16px; }
}

.pill-toggle {
  cursor: pointer; font-family: 'Karla'; font-weight: 600; font-size: 13px;
  color: var(--ink-soft); background: var(--card); border: 1px solid var(--line);
  padding: 8px 14px; border-radius: 20px; transition: all 0.15s;
}
.pill-toggle:hover { border-color: var(--sage); color: var(--sage-deep); }
.pill-toggle.on { background: var(--sage-deep); color: #fff; border-color: var(--sage-deep); }

.print-only { display: none; }

@media print {
  /* hide everything by default, then reveal just the grocery list */
  body * { visibility: hidden; }
  .grocery, .grocery * { visibility: visible; }
  .grocery { position: absolute; left: 0; top: 0; width: 100%; }

  /* strip app chrome and interactive bits from the printout */
  .grocery-head-actions, .add-item, .estimate-note, .view-toggle,
  .pill-toggle, .text-btn, .g-remove { display: none !important; }

  /* clean, ink-friendly list */
  .mp-root { background: #fff !important; padding: 0 !important; }
  .grocery-head h2 { font-size: 20px; }
  .grocery-range { color: #444 !important; }
  .print-only { display: block !important; visibility: visible !important; }
  .print-title { font-family: 'Fraunces', serif; font-size: 18px; margin: 0 0 12px; color: #000; }

  .grocery-list { display: block !important; }
  .grocery-cat {
    background: #fff !important; box-shadow: none !important; border: none !important;
    border-bottom: 1px solid #ccc !important; border-radius: 0 !important;
    padding: 8px 0 4px !important; break-inside: avoid; margin: 0 !important;
  }
  .cat-head { color: #000 !important; border-bottom: 1px solid #999 !important; }
  .scale-hint { color: #444 !important; background: none !important; border: 1px solid #999; }
  .g-item { border-bottom: 1px dotted #ddd !important; padding: 6px 2px !important; }
  .g-check {
    border: 1.5px solid #333 !important; background: #fff !important;
    color: #000 !important; width: 18px; height: 18px;
  }
  .g-item.checked { opacity: 1 !important; }
  .g-item.checked .g-name { text-decoration: line-through; color: #888 !important; }
  .g-name { color: #000 !important; }
  .g-from { color: #555 !important; }
}

@media (max-width: 560px) {
  .mp-header { flex-direction: column; align-items: stretch; gap: 16px; padding-bottom: 20px; }
  .tabs { justify-content: space-between; flex-wrap: wrap; }
  .tabs button { flex: 1; justify-content: center; padding: 10px 8px; font-size: 15px; }
  .field-row { flex-direction: column; gap: 0; }

  /* Slightly larger, more readable type on phones */
  .brand h1 { font-size: 30px; }
  .brand p { font-size: 14.5px; }
  .brand-eyebrow { font-size: 11.5px; }
  .field label { font-size: 14.5px; }
  .field input, .field select, .field textarea { font-size: 16px; }
  .ideas-form .sub, .empty p { font-size: 15.5px; }
  .idea-card h3, .lib-card h3 { font-size: 18.5px; }
  .idea-desc, .lib-desc { font-size: 15px; }
  .macro-pill strong { font-size: 15px; }
  .macro-pill span { font-size: 11px; }
  .cell-meal-name { font-size: 15px; }
  .slot-meal-name { font-size: 16px; }
  .slot-primary .slot-meal-name { font-size: 17.5px; }
  .slot-meal-cal { font-size: 13px; }
  .slot-label { font-size: 12px; }
  .slot-add { font-size: 15px; }
  .day-full { font-size: 19px; }
  .day-card-cal { font-size: 13.5px; }
  .seg button { font-size: 14px; }
  .extra-add-btn { font-size: 13.5px; }
  .g-name { font-size: 16px; }
  .g-from { font-size: 13px; }
  .cat-head { font-size: 16px; }
  .assign-name { font-size: 16px; }
  .assign-cal { font-size: 13px; }
  .grocery-range, .hist-meta { font-size: 14px; }
  .hist-meal { font-size: 15px; }
  .ing-tag { font-size: 12.5px; }
  .add-item input { font-size: 16px; }
  .estimate-note { font-size: 13.5px; }
  .primary-btn { font-size: 16px; }
  .text-btn, .backup-btn { font-size: 14.5px; }
  .chip { font-size: 15px; }
}
`;


// ---- mount + backup/restore -------------------------------------------------
function Root() {
  const fileRef = useRef(null);
  const doExport = async () => {
    const json = await exportAllData();
    const blob = new Blob([json], { type: "application/json" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url;
    a.download = "family-table-backup-" + new Date().toISOString().slice(0, 10) + ".json";
    a.click();
    URL.revokeObjectURL(url);
  };
  const doImport = async (file) => {
    if (!file) return;
    try {
      const text = await file.text();
      await importAllData(text);
      alert("Backup restored. The page will reload.");
      location.reload();
    } catch (e) {
      alert("Could not read that backup file.");
    }
  };
  return (
    <>
      <MealPlanner />
      <div className="backup-bar">
        <button onClick={doExport} title="Download a backup of all your data"><Download size={13} /> Back up</button>
        <input ref={fileRef} type="file" accept="application/json" style={{ display: "none" }}
          onChange={(e) => { const f = e.target.files?.[0]; e.target.value = ""; doImport(f); }} />
        <button onClick={() => fileRef.current?.click()} title="Restore from a backup file"><Upload size={13} /> Restore</button>
      </div>
    </>
  );
}

const _root = ReactDOM.createRoot(document.getElementById("root"));
_root.render(<Root />);
