/* eslint-disable */
// ===========================================================================
// Omnivore UI — React (no build step; transpiled in-browser by Babel).
//
// Minimalist "warm paper" redesign, built to run full-screen on a phone with a
// REAL camera preview. Everything renders from the same functional core as
// before (core/*.js + engine.js); only the presentation changed.
//
//   Today    one big health-score number + a sentence + flat, tappable metric
//            rows (worst-first). Settings reachable from the header.
//   History  per-day list of meals; tap a meal to reveal nutrition / edit / delete.
//   Settings two tabs: "Coach" (chat that configures the plan) and "Goals" (a
//            read-only view of the plan the coach has set).
//   Scan     the "+" blob FAB opens a bottom sheet with a live camera preview
//            and a text field; "Scan" runs analysis → clarify → result → log.
//
// Today / History tabs in the bar + a Settings header button + a morphing blob FAB.
// Palette and type come from the layout study; all colours are inline so there's
// a single source of truth (the C object below).
// ===========================================================================
const { useState, useRef, useCallback, useEffect } = React;

const Scoring = window.OmniScoring;
const Metrics = window.OmniMetrics;
const Store = window.OmniStore;
const Engine = window.OmniEngine;
const Auth = window.OmniAuth;
const Data = window.OmniData;

// ── palette / type ─────────────────────────────────────────────────────────
const C = {
  bg: "#f7f5f1",      // warm paper
  card: "#f1eee8",    // barely-there raised tone
  line: "#e2ddd3",    // hairline
  text: "#1c1a17",    // warm ink
  dim: "#7c766c",     // secondary
  faint: "#a8a298",   // tertiary / placeholder
  good: "#5b8f6f",    // sage — also the accent
  watch: "#bd8a4a",   // clay
  over: "#b5604f",    // faded terracotta
  accent: "#5b8f6f",
};
const SANS = "'Hanken Grotesk', ui-sans-serif, system-ui, sans-serif";
const SERIF = "'Fraunces', Georgia, serif";

const FontLink = () => (
  <style>{`
    @import url('https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,300;9..144,400&family=Hanken+Grotesk:wght@400;500;600&display=swap');
    * { -webkit-tap-highlight-color: transparent; box-sizing: border-box; }
    /* Lock the whole document: no page scroll, no rubber-band. Only the inner
       body of the app scrolls (it sets overscroll-behavior: contain). */
    html, body, #root { margin: 0; height: 100%; }
    html, body { overflow: hidden; overscroll-behavior: none; position: fixed; width: 100%; }
    textarea, input, button { font-family: inherit; }
    ::placeholder { color: ${C.faint}; }
    *::-webkit-scrollbar { width: 0; height: 0; }

    /* Responsive shell. On a phone the frame fills the screen edge-to-edge; on a
       tablet / desktop it becomes a centered phone-shaped card (≈9:19.5). */
    #omni-frame {
      width: 100vw;
      height: 100vh;       /* fallback */
      height: 100dvh;
      border-radius: 0;
      box-shadow: none;
    }
    @media (min-width: 480px) {
      #omni-frame {
        width: 412px;
        height: min(calc(100dvh - 40px), 900px);
        border-radius: 30px;
        box-shadow: 0 14px 50px rgba(60, 50, 35, 0.22);
      }
    }
  `}</style>
);

// ── platform detection (for install instructions) ───────────────────────────
const UA = (typeof navigator !== "undefined" && navigator.userAgent) || "";
const IS_IOS =
  /iphone|ipad|ipod/i.test(UA) ||
  (typeof navigator !== "undefined" && navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1);
const IS_ANDROID = /android/i.test(UA);

// ── small helpers ───────────────────────────────────────────────────────────
function fmt(v) {
  if (v == null || Number.isNaN(v)) return "—";
  return Math.abs(v) >= 100 ? Math.round(v) : Math.round(v * 10) / 10;
}
const p2 = (n) => String(n).padStart(2, "0");
function nowForInput() {
  const d = new Date();
  return `${d.getFullYear()}-${p2(d.getMonth() + 1)}-${p2(d.getDate())}T${p2(d.getHours())}:${p2(d.getMinutes())}`;
}
function localDay(d = new Date()) {
  return `${d.getFullYear()}-${p2(d.getMonth() + 1)}-${p2(d.getDate())}`;
}
const dayOf = (rec) => (rec.eaten_at || rec.logged_at || "").slice(0, 10);
const clampPct = (x) => Math.max(0, Math.min(100, x));
const dot = (s) => (s === "good" ? C.good : s === "watch" ? C.watch : C.over);

function dayLabel(day) {
  if (day === localDay()) return "Today";
  const y = new Date(); y.setDate(y.getDate() - 1);
  if (day === localDay(y)) return "Yesterday";
  return day;
}

// score → status tier for the big number + day sentence
function scoreColor(score) {
  if (score == null) return C.faint;
  if (score >= 75) return C.good;
  if (score >= 50) return C.watch;
  return C.over;
}
function dayLine(score) {
  if (score == null) return "Log a meal to start your day.";
  if (score >= 85) return "On a good day.";
  if (score >= 75) return "Looking solid.";
  if (score >= 60) return "A couple things to watch.";
  if (score >= 45) return "A few things to fix.";
  return "Let’s turn it around.";
}

// metric subscore → three-tier dot
function metricTier(m) {
  if (m.subscore >= 75) return "good";
  if (m.subscore >= 50) return "watch";
  return "over";
}
// "88 / 130g" style value/target string
const ushort = (u) => (u && u.startsWith("%") ? "%" : u === "g" ? "g" : "");
function metricValue(m) {
  const us = ushort(m.unit);
  const v = fmt(m.value);
  if (m.goalType === "range") {
    const max = m.target && typeof m.target === "object" ? fmt(m.target.max) : fmt(m.target);
    if (m.unit === "kcal") return `${v}`;
    return `${v} / ${max}${us}`;
  }
  const t = fmt(m.target);
  return us === "%" ? `${v} / ${t}%` : `${v} / ${t}${us}`;
}
// one-line explanation shown in the info sheet
const METRIC_INFO = {
  protein: "Total grams of protein today vs. your target. Supports muscle and keeps you full.",
  fiber: "Dietary fiber from whole plants. Helps digestion and satiety.",
  plant: "Grams of distinct plant foods — vegetables, fruit, legumes, nuts, seeds, whole grains.",
  healthy_fat: "Unsaturated fats from fish, nuts, seeds and oils. Aim to stay within the range.",
  upf: "Share of your calories from ultra-processed foods. Lower is better.",
  sugar: "Total sugars today vs. your daily ceiling.",
  added_sugar: "Sugars added during processing, not those natural to whole foods.",
  sat_fat: "Saturated fat today vs. your daily ceiling.",
  calories: "Total energy today vs. your target range.",
};
const metricInfo = (m) => METRIC_INFO[m.id] || (m.phrasing && m.phrasing.good) || m.label;

// shared input look
const inputStyle = {
  background: C.card, border: `1px solid ${C.line}`, borderRadius: 12, color: C.text,
  padding: "11px 13px", fontSize: 15, boxSizing: "border-box", outline: "none", width: "100%",
};
const pillBtn = (active) => ({
  background: active ? C.accent : C.card, border: `1px solid ${active ? C.accent : C.line}`,
  color: active ? "#fff" : C.text, borderRadius: 16, padding: "7px 13px", fontSize: 13, cursor: "pointer",
});

// Swipe-down-to-dismiss for bottom sheets. Spread `handlers` onto the grab
// area(s); apply `style` to the sliding panel. Releasing past ~90px closes.
function useDragToClose(onClose) {
  const [dy, setDy] = useState(0);
  const [dragging, setDragging] = useState(false);
  const startY = useRef(null);
  const onTouchStart = (e) => { startY.current = e.touches[0].clientY; setDragging(true); };
  const onTouchMove = (e) => {
    if (startY.current == null) return;
    const d = e.touches[0].clientY - startY.current;
    setDy(d > 0 ? d : 0);
  };
  const onTouchEnd = () => {
    const d = dy; startY.current = null; setDragging(false);
    if (d > 90) onClose(); else setDy(0);
  };
  const handlers = { onTouchStart, onTouchMove, onTouchEnd };
  const style = { transform: dy ? `translateY(${dy}px)` : "none", transition: dragging ? "none" : "transform .25s ease" };
  return { handlers, style, dragging };
}

// ── nutrition breakdown (shared by Result + History detail) ─────────────────
const NUTRIENTS = [
  ["calories", "Calories", 0], ["fat", "Total fat", 0], ["saturated_fat", "Saturated fat", 1],
  ["carbs", "Total carbohydrate", 0], ["fiber", "Dietary fiber", 1],
  ["sugar", "Total sugars", 1], ["added_sugar", "Added sugars", 2], ["protein", "Protein", 0],
];

function NutrientValue({ n }) {
  if (!n || !n.known || n.amount == null) return <span style={{ color: C.faint }}>unknown</span>;
  return (
    <span>
      {fmt(n.amount)} {n.unit || ""}
      {n.estimated && <span style={{ color: C.faint }}> ~</span>}
      {n.partial && <span style={{ color: C.faint }}> *</span>}
    </span>
  );
}
function NutrientTable({ nutrients }) {
  return (
    <table style={{ width: "100%", fontSize: 13, borderCollapse: "collapse" }}>
      <tbody>
        {NUTRIENTS.map(([k, label, indent]) => (
          <tr key={k} style={{ borderBottom: `1px solid ${C.line}` }}>
            <td style={{ padding: "6px 0", color: C.dim, paddingLeft: indent === 1 ? 16 : indent === 2 ? 32 : 0 }}>{label}</td>
            <td style={{ padding: "6px 0", textAlign: "right", color: C.text }}><NutrientValue n={nutrients[k]} /></td>
          </tr>
        ))}
      </tbody>
    </table>
  );
}
function ClassTags({ cls }) {
  if (!cls) return null;
  const tags = [];
  if (cls.plant_category && cls.plant_category !== "none") tags.push(cls.plant_category.replace("_", " "));
  if (cls.nova_group === 4) tags.push("ultra-processed");
  if (!tags.length) return null;
  return (
    <span style={{ marginLeft: 6 }}>
      {tags.map((t) => (
        <span key={t} style={{ display: "inline-block", padding: "1px 6px", borderRadius: 6, background: C.bg,
          border: `1px solid ${C.line}`, fontSize: 10, color: C.dim, marginRight: 4 }}>{t}</span>
      ))}
    </span>
  );
}
function MealDetail({ meal }) {
  const comps = meal.components || [];
  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 14 }}>
      {comps.length > 0 && (
        <div>
          {comps.map((c, i) => {
            const q = c.quantity || {};
            const amt = q.grams ? `${fmt(q.grams)} g` : q.serving || `×${q.count || 1}`;
            return (
              <div key={i} style={{ display: "flex", justifyContent: "space-between", alignItems: "center",
                borderBottom: `1px solid ${C.line}`, padding: "8px 0", fontSize: 14 }}>
                <span style={{ color: C.text }}>{c.name}<ClassTags cls={c.classification} /></span>
                <span style={{ color: C.faint, whiteSpace: "nowrap" }}>{amt}</span>
              </div>
            );
          })}
        </div>
      )}
      <div>
        <div style={{ fontSize: 12, color: C.faint, marginBottom: 4 }}>Nutrition facts</div>
        <NutrientTable nutrients={meal.nutrients || {}} />
      </div>
    </div>
  );
}

// ── info bottom sheet (metric explanation) ──────────────────────────────────
function InfoSheet({ item, onClose }) {
  const drag = useDragToClose(onClose);
  if (!item) return null;
  return (
    <div onClick={onClose} style={{ position: "absolute", inset: 0, background: "rgba(0,0,0,0.5)",
      display: "flex", alignItems: "flex-end", zIndex: 60 }}>
      <div onClick={(e) => e.stopPropagation()} {...drag.handlers} style={{ width: "100%", background: C.card,
        borderRadius: "18px 18px 0 0", padding: "18px 22px 32px", touchAction: "none", ...drag.style }}>
        <div style={{ width: 36, height: 4, background: C.line, borderRadius: 2, margin: "0 auto 16px" }} />
        <div style={{ fontSize: 16, fontWeight: 600, color: C.text, marginBottom: 8 }}>{item.label}</div>
        <div style={{ fontSize: 14, color: C.dim, lineHeight: 1.55 }}>{metricInfo(item)}</div>
      </div>
    </div>
  );
}

// ── install instructions bottom sheet (manual / iOS / non-secure fallback) ──
function InstallSheet({ open, onClose }) {
  const drag = useDragToClose(onClose);
  if (!open) return null;
  let steps;
  if (IS_IOS)
    steps = [
      "Make sure you opened this page in Safari.",
      "Tap the Share button (the square with an up-arrow).",
      "Scroll down and choose “Add to Home Screen”, then “Add”.",
    ];
  else if (IS_ANDROID)
    steps = [
      "Open the browser menu (⋮ in the top-right).",
      "Tap “Install app” or “Add to Home screen”.",
      "Confirm to add Omnivore to your home screen.",
    ];
  else
    steps = [
      "Open your browser’s menu, or the install icon in the address bar.",
      "Choose “Install Omnivore”.",
    ];
  const insecure = typeof window !== "undefined" && !window.isSecureContext;

  return (
    <div onClick={onClose} style={{ position: "absolute", inset: 0, background: "rgba(0,0,0,0.5)",
      display: "flex", alignItems: "flex-end", zIndex: 60 }}>
      <div onClick={(e) => e.stopPropagation()} {...drag.handlers} style={{ width: "100%", background: C.card,
        borderRadius: "18px 18px 0 0", padding: "18px 22px 32px", touchAction: "none", ...drag.style }}>
        <div style={{ width: 36, height: 4, background: C.line, borderRadius: 2, margin: "0 auto 16px" }} />
        <div style={{ fontSize: 16, fontWeight: 600, color: C.text, marginBottom: 12 }}>Install Omnivore</div>
        <ol style={{ margin: 0, paddingLeft: 20, color: C.dim, fontSize: 14, lineHeight: 1.7 }}>
          {steps.map((s, i) => <li key={i}>{s}</li>)}
        </ol>
        {insecure && (
          <div style={{ marginTop: 14, fontSize: 13, color: C.over, lineHeight: 1.5 }}>
            {IS_IOS
              ? "Heads up: the camera needs an https:// address to work inside the installed app."
              : "On Android the one-tap install only appears over an https:// address — open the app over HTTPS, not http://."}
          </div>
        )}
      </div>
    </div>
  );
}

// ── a single tappable metric row (tap = reveal detail) ──────────────────────
function MetricRow({ m, onInfo }) {
  const [open, setOpen] = useState(false);
  const tier = metricTier(m);
  return (
    <div style={{ borderBottom: `1px solid ${C.line}` }}>
      <div onClick={() => setOpen(!open)}
        style={{ display: "flex", alignItems: "center", gap: 12, padding: "15px 4px", cursor: "pointer" }}>
        <span style={{ width: 8, height: 8, borderRadius: 4, background: dot(tier), flexShrink: 0 }} />
        <span style={{ flex: 1, fontSize: 15, color: C.text }}>{m.label}</span>
        <span style={{ fontSize: 13, color: C.dim }}>{metricValue(m)}</span>
      </div>
      {open && (
        <div style={{ padding: "0 4px 14px 32px", display: "flex", alignItems: "center", gap: 10 }}>
          <span style={{ fontSize: 13, color: C.faint, flex: 1 }}>{metricInfo(m).split(".")[0]}.</span>
          <button onClick={(e) => { e.stopPropagation(); onInfo(m); }}
            style={{ background: "none", border: `1px solid ${C.line}`, color: C.dim, borderRadius: 16,
              width: 26, height: 26, fontSize: 13, cursor: "pointer", flexShrink: 0 }}>ⓘ</button>
        </div>
      )}
    </div>
  );
}

// ── TODAY ───────────────────────────────────────────────────────────────────
function TodayView({ profile, toast, onCoach, onInfo, onInstall, showInstall, onSignOut }) {
  const log = Store.readLog();
  const today = localDay();
  const entries = log.filter((r) => dayOf(r) === today).map((r) => r.meal);
  const scored = Scoring.scoreDay(entries, profile, new Date());
  const rows = [...scored.metrics].sort((a, b) => a.subscore - b.subscore);

  const headerBtn = {
    background: "none", border: `1px solid ${C.line}`, color: C.dim, borderRadius: 16,
    padding: "5px 12px", fontSize: 12, cursor: "pointer",
  };
  return (
    <div style={{ display: "flex", flexDirection: "column", minHeight: "100%" }}>
      {/* header: day + install / coach access */}
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 8 }}>
        <span style={{ fontSize: 13, color: C.faint }}>Today</span>
        <div style={{ display: "flex", gap: 8 }}>
          {showInstall && <button onClick={onInstall} style={headerBtn}>↓ Install</button>}
          <button onClick={onCoach} style={headerBtn}>Settings</button>
          {onSignOut && <button onClick={onSignOut} style={headerBtn} title={Auth.email()}>Sign out</button>}
        </div>
      </div>

      {/* hero: just a number and a sentence */}
      <div style={{ textAlign: "center", padding: "44px 0 38px" }}>
        <div style={{ fontSize: 96, fontWeight: 300, fontFamily: SERIF, color: scoreColor(scored.score), lineHeight: 1 }}>
          {scored.score == null ? "—" : scored.score}
        </div>
        <div style={{ fontSize: 13, color: C.faint, marginTop: 2 }}>health score</div>
        <div style={{ fontSize: 15, color: C.text, marginTop: 18 }}>{dayLine(scored.score)}</div>
      </div>

      {/* metric rows — flat, tappable, worst-first */}
      {scored.score != null && (
        <div>{rows.map((m) => <MetricRow key={m.id} m={m} onInfo={onInfo} />)}</div>
      )}
    </div>
  );
}

// ── HISTORY ──────────────────────────────────────────────────────────────────
function MealRow({ rec, onChange, toast }) {
  const [open, setOpen] = useState(false);
  const [editing, setEditing] = useState(false);
  const m = rec.meal || {};
  const time = (rec.eaten_at || "").slice(11, 16);

  function del() { Store.deleteMeal(rec.id); toast("Deleted"); onChange(); }

  return (
    <div style={{ borderBottom: `1px solid ${C.line}` }}>
      <div onClick={() => setOpen(!open)}
        style={{ padding: "14px 4px", display: "flex", justifyContent: "space-between", alignItems: "center", cursor: "pointer", gap: 12 }}>
        <span style={{ fontSize: 15, color: C.text }}>{m.description || "(meal)"}</span>
        <span style={{ fontSize: 13, color: C.faint, whiteSpace: "nowrap" }}>{time}</span>
      </div>
      {open && (
        <div style={{ padding: "2px 4px 16px" }}>
          {editing ? (
            <MealEditor rec={rec} toast={toast} onCancel={() => setEditing(false)}
              onSaved={() => { setEditing(false); onChange(); }} />
          ) : (
            <div style={{ display: "flex", flexDirection: "column", gap: 14 }}>
              <MealDetail meal={m} />
              <div style={{ display: "flex", gap: 8 }}>
                <button onClick={() => setEditing(true)} style={pillBtn(false)}>Edit</button>
                <button onClick={del} style={pillBtn(false)}>Delete</button>
              </div>
            </div>
          )}
        </div>
      )}
    </div>
  );
}

function HistoryView({ profile, toast, onChange }) {
  const log = Store.readLog();
  if (!log.length)
    return <div style={{ color: C.faint, textAlign: "center", padding: "60px 0", fontSize: 15 }}>No meals logged yet.</div>;

  const days = {};
  for (const rec of log) (days[dayOf(rec)] = days[dayOf(rec)] || []).push(rec);
  const dayKeys = Object.keys(days).sort().reverse();

  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 22 }}>
      {dayKeys.map((day) => {
        const recs = days[day].slice().sort((a, b) => (a.eaten_at < b.eaten_at ? 1 : -1));
        const endOfDay = new Date(day + "T23:59:00");
        const scored = Scoring.scoreDay(recs.map((r) => r.meal), profile, endOfDay);
        return (
          <div key={day}>
            <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", padding: "0 4px 6px" }}>
              <span style={{ fontSize: 15, color: C.text, fontWeight: 600 }}>{dayLabel(day)}</span>
              <span style={{ fontSize: 13, color: scoreColor(scored.score) }}>{scored.score == null ? "—" : scored.score}</span>
            </div>
            {recs.map((rec) => <MealRow key={rec.id} rec={rec} onChange={onChange} toast={toast} />)}
          </div>
        );
      })}
    </div>
  );
}

// ── meal editor (paper theme; AI quick-edit + manual grams) ──────────────────
const mkRows = (meal) =>
  (meal.components || []).map((c) => ({ grams: String((c.quantity && c.quantity.grams) || ""), removed: false }));

function MealEditor({ rec, onSaved, onCancel, toast }) {
  const [meal, setMeal] = useState(rec.meal || {});
  const [desc, setDesc] = useState((rec.meal || {}).description || "");
  const [when, setWhen] = useState((rec.eaten_at || "").slice(0, 16));
  const [rows, setRows] = useState(() => mkRows(rec.meal || {}));
  const [ai, setAi] = useState("");
  const [busy, setBusy] = useState(false);
  const comps = meal.components || [];

  const setRow = (i, patch) => setRows((rs) => rs.map((r, j) => (j === i ? { ...r, ...patch } : r)));
  const rowItems = () => rows.map((r) => (r.removed ? { removed: true } : { grams: Number(r.grams) }));
  const applyManual = () =>
    Engine.reviseMeal(meal, { description: desc.trim() || meal.description, items: rowItems() });

  async function askAi() {
    const text = ai.trim();
    if (!text || busy) return;
    setBusy(true);
    const r = await Engine.aiEditMeal(applyManual(), text);
    setBusy(false);
    if (!r.ok) { toast(r.error || "Edit failed"); return; }
    setMeal(r.entry); setDesc(r.entry.description || ""); setRows(mkRows(r.entry)); setAi("");
    toast("AI updated it — review & save");
  }
  function save() {
    if (rowItems().every((it) => it.removed)) { toast("Keep at least one ingredient"); return; }
    Store.updateMeal(rec.id, { meal: applyManual(), eaten_at: when ? when + ":00" : rec.eaten_at });
    toast("Saved ✓"); onSaved();
  }

  const lbl = { fontSize: 12, color: C.faint, marginBottom: 5, display: "block" };
  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 14 }}>
      <div>
        <label style={lbl}>Edit with AI</label>
        <div style={{ display: "flex", gap: 8 }}>
          <input type="text" value={ai} onChange={(e) => setAi(e.target.value)}
            onKeyDown={(e) => { if (e.key === "Enter") askAi(); }}
            placeholder="e.g. double the rice, drop the bun" style={{ ...inputStyle, flex: 1 }} />
          <button onClick={askAi} disabled={busy}
            style={{ ...pillBtn(true), opacity: busy ? 0.5 : 1, padding: "0 16px" }}>{busy ? "…" : "Apply"}</button>
        </div>
        {busy && <div style={{ fontSize: 12, color: C.faint, marginTop: 6 }}>AI is rebuilding the meal…</div>}
      </div>
      <div>
        <label style={lbl}>Description</label>
        <input type="text" value={desc} onChange={(e) => setDesc(e.target.value)} style={inputStyle} />
      </div>
      <div>
        <label style={lbl}>When</label>
        <input type="datetime-local" value={when} onChange={(e) => setWhen(e.target.value)} style={{ ...inputStyle, width: "auto" }} />
      </div>
      <div>
        <label style={lbl}>Ingredients (grams)</label>
        <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
          {comps.map((c, i) => (
            <div key={i} style={{ display: "flex", alignItems: "center", gap: 8, fontSize: 14 }}>
              <span style={{ flex: 1, color: rows[i].removed ? C.faint : C.text, textDecoration: rows[i].removed ? "line-through" : "none" }}>{c.name}</span>
              <input type="number" value={rows[i].grams} disabled={rows[i].removed}
                onChange={(e) => setRow(i, { grams: e.target.value })}
                style={{ ...inputStyle, width: 76, textAlign: "right", padding: "7px 9px", opacity: rows[i].removed ? 0.4 : 1 }} />
              <span style={{ color: C.faint }}>g</span>
              <button onClick={() => setRow(i, { removed: !rows[i].removed })} style={{ ...pillBtn(false), padding: "5px 10px", fontSize: 12 }}>
                {rows[i].removed ? "undo" : "remove"}
              </button>
            </div>
          ))}
        </div>
      </div>
      <div style={{ display: "flex", gap: 8 }}>
        <button onClick={save} style={{ ...pillBtn(true), flex: 1, padding: "10px 0", borderRadius: 12 }}>Save changes</button>
        <button onClick={onCancel} style={{ ...pillBtn(false), flex: 1, padding: "10px 0", borderRadius: 12 }}>Cancel</button>
      </div>
    </div>
  );
}

// ── SETTINGS (Coach + Goals tabs) ────────────────────────────────────────────
// The old "Coach" screen is now one tab of a Settings panel. "Goals" shows the
// plan the coach has set (body stats, active goals, per-metric targets and
// priorities); "Coach" is the chat that actually changes them.
function SettingsView({ profile, onApply, toast }) {
  const [sub, setSub] = useState("coach");
  const segBtn = (id, label) => (
    <button key={id} onClick={() => setSub(id)}
      style={{ flex: 1, padding: "8px 0", fontSize: 14, cursor: "pointer", borderRadius: 9,
        border: "none", background: sub === id ? C.bg : "transparent",
        color: sub === id ? C.text : C.dim, fontWeight: sub === id ? 600 : 400,
        boxShadow: sub === id ? "0 1px 3px rgba(60,50,35,0.12)" : "none", transition: "all .15s" }}>
      {label}
    </button>
  );
  return (
    <div style={{ display: "flex", flexDirection: "column", height: "100%", gap: 14 }}>
      <div style={{ display: "flex", gap: 4, background: C.card, border: `1px solid ${C.line}`,
        borderRadius: 12, padding: 4, flexShrink: 0 }}>
        {segBtn("coach", "Coach")}
        {segBtn("goals", "Goals")}
      </div>
      {sub === "coach"
        ? <div style={{ flex: 1, minHeight: 0, display: "flex", flexDirection: "column" }}>
            <CoachView profile={profile} onApply={onApply} toast={toast} />
          </div>
        : <div style={{ flex: 1, minHeight: 0, overflowY: "auto", overscrollBehavior: "contain" }}>
            <GoalsView profile={profile} onAdjust={() => setSub("coach")} />
          </div>}
    </div>
  );
}

// Read-only view of the current plan. Editing happens through the coach.
function GoalsView({ profile, onAdjust }) {
  const p = (window.OmniProfile ? window.OmniProfile.normalize : (x) => x)(profile);
  const targets = Metrics.resolveTargets(p);
  const stars = Metrics.resolveStars(p);

  const GOAL_LABELS = { nutrition: "General nutrition", weight_loss: "Weight loss", muscle_gain: "Muscle gain" };
  const SEX_LABELS = { male: "Male", female: "Female", unspecified: "Unspecified" };
  const fmtTarget = (m) => {
    const t = targets[m.id];
    const body = t && typeof t === "object" ? `${fmt(t.min)}–${fmt(t.max)}` : fmt(t);
    const dir = m.goalType === "below" ? "≤ " : m.goalType === "range" ? "" : "≥ ";
    return `${dir}${body} ${m.unit}`;
  };
  const starStr = (n) => "★".repeat(n) + "☆".repeat(Metrics.MAX_STARS - n);

  const stat = (label, value) => (
    <div style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline",
      padding: "9px 4px", borderBottom: `1px solid ${C.line}` }}>
      <span style={{ fontSize: 14, color: C.dim }}>{label}</span>
      <span style={{ fontSize: 14, color: C.text }}>{value}</span>
    </div>
  );
  const heading = (t) => (
    <div style={{ fontSize: 12, color: C.faint, textTransform: "uppercase", letterSpacing: ".06em",
      margin: "18px 4px 4px" }}>{t}</div>
  );

  return (
    <div style={{ display: "flex", flexDirection: "column", paddingBottom: 8 }}>
      <p style={{ fontSize: 13, color: C.dim, margin: "2px 4px 0", lineHeight: 1.5 }}>
        Your current plan. To change anything, ask the coach.
      </p>

      {heading("Goals")}
      <div style={{ display: "flex", flexWrap: "wrap", gap: 8, padding: "4px" }}>
        {p.goals.map((g) => (
          <span key={g} style={{ ...pillBtn(true), cursor: "default" }}>{GOAL_LABELS[g] || g}</span>
        ))}
      </div>

      {heading("About you")}
      {stat("Age", `${p.age}`)}
      {stat("Height", `${p.heightCm} cm`)}
      {stat("Weight", `${p.weightKg} kg`)}
      {stat("Sex", SEX_LABELS[p.sex] || p.sex)}
      {p.dietaryPrefs && p.dietaryPrefs.length ? stat("Dietary", p.dietaryPrefs.join(", ")) : null}

      {heading("Daily targets & priority")}
      {Metrics.METRICS.map((m) => (
        <div key={m.id} style={{ display: "flex", alignItems: "baseline", gap: 10,
          padding: "10px 4px", borderBottom: `1px solid ${C.line}` }}>
          <span style={{ flex: 1, fontSize: 14, color: C.text }}>{m.label}</span>
          <span style={{ fontSize: 13, color: C.dim, whiteSpace: "nowrap" }}>{fmtTarget(m)}</span>
          <span title={`Priority ${stars[m.id]}/${Metrics.MAX_STARS}`}
            style={{ fontSize: 12, color: stars[m.id] ? C.watch : C.faint, whiteSpace: "nowrap", width: 58, textAlign: "right" }}>
            {starStr(stars[m.id])}
          </span>
        </div>
      ))}

      <button onClick={onAdjust}
        style={{ ...pillBtn(true), padding: "12px 0", borderRadius: 12, marginTop: 18 }}>
        Adjust with coach
      </button>
    </div>
  );
}

// ── COACH ────────────────────────────────────────────────────────────────────
function CoachView({ profile, onApply, toast }) {
  const [msgs, setMsgs] = useState(() => Store.getChat());
  const [input, setInput] = useState("");
  const [busy, setBusy] = useState(false);
  const endRef = useRef(null);

  useEffect(() => {
    if (!msgs.length) {
      const greet = {
        role: "assistant",
        content:
          "Hi! I’m your nutrition coach — I set up everything here through chat. Tell me your goal (general nutrition, weight loss, muscle gain, or a mix) and your age, height, weight and sex, and I’ll set your targets and priorities. Ask me to tweak anything anytime.",
      };
      setMsgs([greet]); Store.setChat([greet]);
    }
  }, []);
  useEffect(() => { if (endRef.current) endRef.current.scrollIntoView({ block: "end" }); }, [msgs, busy]);

  async function send() {
    const text = input.trim();
    if (!text || busy) return;
    const next = [...msgs, { role: "user", content: text }];
    setMsgs(next); setInput(""); setBusy(true);
    const r = await Engine.coachChat({ profile, history: msgs, message: text });
    setBusy(false);
    const reply = { role: "assistant", content: r.ok ? r.reply : r.error || "Something went wrong." };
    const after = [...next, reply];
    setMsgs(after); Store.setChat(after);
    if (r.ok && r.updates && Object.keys(r.updates).length) { onApply(r.updates); toast("Plan updated ✓"); }
  }

  const bubble = (role) => ({
    alignSelf: role === "user" ? "flex-end" : "flex-start",
    background: role === "user" ? C.accent : C.card,
    color: role === "user" ? "#fff" : C.text,
    padding: "10px 13px", fontSize: 14, lineHeight: 1.5, maxWidth: "82%",
    borderRadius: role === "user" ? "14px 14px 4px 14px" : "14px 14px 14px 4px",
  });

  return (
    <div style={{ display: "flex", flexDirection: "column", height: "100%", gap: 12 }}>
      <div style={{ flex: 1, display: "flex", flexDirection: "column", gap: 10, overflowY: "auto", minHeight: 0 }}>
        {msgs.map((m, i) => <div key={i} style={bubble(m.role)}>{m.content}</div>)}
        {busy && <div style={{ alignSelf: "flex-start", color: C.faint, fontSize: 13 }}>Coach is thinking…</div>}
        <div ref={endRef} />
      </div>
      <div style={{ display: "flex", gap: 8 }}>
        <input value={input} onChange={(e) => setInput(e.target.value)}
          onKeyDown={(e) => { if (e.key === "Enter") send(); }} placeholder="Message your coach…"
          style={{ ...inputStyle, borderRadius: 22, padding: "12px 16px" }} />
        <button onClick={send} disabled={busy}
          style={{ background: C.accent, border: "none", color: "#fff", borderRadius: 22, width: 44,
            fontSize: 16, cursor: "pointer", flexShrink: 0, opacity: busy ? 0.5 : 1 }}>↑</button>
      </div>
    </div>
  );
}

// ── clarify questions (paper theme) ──────────────────────────────────────────
function Clarify({ questions, onSubmit }) {
  const [chosen, setChosen] = useState(() => questions.map(() => null));
  const [typed, setTyped] = useState(() => questions.map(() => ""));
  const ready = chosen.every((c, i) => c != null || typed[i].trim());
  function pick(qi, opt) { setChosen((c) => c.map((v, i) => (i === qi ? opt : v))); setTyped((t) => t.map((v, i) => (i === qi ? "" : v))); }
  function type(qi, val) { setTyped((t) => t.map((v, i) => (i === qi ? val : v))); if (val.trim()) setChosen((c) => c.map((v, i) => (i === qi ? null : v))); }
  function submit() {
    onSubmit(questions.map((q, i) => ({ question: q.question, answer: typed[i].trim() || chosen[i] || (q.options && q.options[0]) || "" })));
  }
  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 18 }}>
      <p style={{ fontSize: 14, color: C.text, fontWeight: 600, margin: 0 }}>A couple of quick questions to get this right:</p>
      {questions.map((q, qi) => (
        <div key={qi} style={{ display: "flex", flexDirection: "column", gap: 8 }}>
          <p style={{ fontSize: 14, color: C.text, margin: 0 }}>{q.question}</p>
          <div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
            {(q.options || []).map((opt) => (
              <button key={opt} onClick={() => pick(qi, opt)} style={pillBtn(chosen[qi] === opt)}>{opt}</button>
            ))}
          </div>
          <input type="text" value={typed[qi]} onChange={(e) => type(qi, e.target.value)} placeholder="Or type your own…"
            style={{ ...inputStyle, padding: "9px 12px", fontSize: 14 }} />
        </div>
      ))}
      <button onClick={submit} disabled={!ready}
        style={{ ...pillBtn(true), padding: "11px 0", borderRadius: 12, opacity: ready ? 1 : 0.4 }}>Continue</button>
    </div>
  );
}

// ── result (paper theme) ─────────────────────────────────────────────────────
function Result({ entry, onLog, onDiscard }) {
  const [eatenAt, setEatenAt] = useState(nowForInput);
  const N = entry.nutrients || {};
  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
      <div>
        <h3 style={{ fontSize: 18, fontWeight: 600, color: C.text, margin: 0, fontFamily: SERIF }}>{entry.description || "Food entry"}</h3>
        <span style={{ fontSize: 12, color: C.dim }}>{entry.confidence} confidence</span>
      </div>
      <div style={{ display: "grid", gridTemplateColumns: "repeat(4,1fr)", gap: 8, textAlign: "center" }}>
        {[["calories", "kcal"], ["protein", "protein"], ["carbs", "carbs"], ["fat", "fat"]].map(([k, label]) => {
          const n = N[k] || {};
          return (
            <div key={k} style={{ background: C.card, borderRadius: 12, padding: "10px 0" }}>
              <div style={{ fontSize: 18, fontWeight: 600, color: C.text }}>{n.known ? fmt(n.amount) : "—"}</div>
              <div style={{ fontSize: 11, color: C.faint }}>{label}</div>
            </div>
          );
        })}
      </div>
      <MealDetail meal={entry} />
      <div>
        <label style={{ fontSize: 12, color: C.faint, marginBottom: 5, display: "block" }}>When did you eat this?</label>
        <input type="datetime-local" value={eatenAt} onChange={(e) => setEatenAt(e.target.value)} style={{ ...inputStyle, width: "auto" }} />
      </div>
      <div style={{ display: "flex", gap: 8 }}>
        <button onClick={() => onLog(entry, eatenAt ? eatenAt + ":00" : null)}
          style={{ ...pillBtn(true), flex: 1, padding: "12px 0", borderRadius: 12 }}>Log it</button>
        <button onClick={onDiscard} style={{ ...pillBtn(false), flex: 1, padding: "12px 0", borderRadius: 12 }}>Discard</button>
      </div>
    </div>
  );
}

// ── ADD / SCAN sheet (live camera + text; analyze → clarify → result) ────────
function AddSheet({ focused, setFocused, captured, setCaptured, text, setText,
                    status, questions, result, error, busy, onAnswer, onLog, onDiscard, onClose }) {
  const videoRef = useRef(null);
  const streamRef = useRef(null);
  const [camErr, setCamErr] = useState(null);
  const liveCamera = status === "capture" && !captured;

  // start/stop the camera with the live-preview state
  useEffect(() => {
    let cancelled = false;
    async function start() {
      if (!liveCamera) return;
      if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { setCamErr("Camera not available on this device."); return; }
      try {
        const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: { ideal: "environment" } }, audio: false });
        if (cancelled) { stream.getTracks().forEach((t) => t.stop()); return; }
        streamRef.current = stream;
        if (videoRef.current) { videoRef.current.srcObject = stream; videoRef.current.play().catch(() => {}); }
        setCamErr(null);
      } catch (e) {
        setCamErr("Camera blocked — describe your meal below instead.");
      }
    }
    start();
    return () => {
      cancelled = true;
      if (streamRef.current) { streamRef.current.getTracks().forEach((t) => t.stop()); streamRef.current = null; }
    };
  }, [liveCamera]);

  function capture() {
    const v = videoRef.current;
    if (!v || !v.videoWidth) return;
    const cn = document.createElement("canvas");
    cn.width = v.videoWidth; cn.height = v.videoHeight;
    cn.getContext("2d").drawImage(v, 0, 0, cn.width, cn.height);
    setCaptured(cn.toDataURL("image/jpeg", 0.9));
  }

  const showCapture = status === "capture";
  const analyzing = status === "analyzing";
  const drag = useDragToClose(onClose);

  return (
    <div style={{ position: "absolute", inset: 0, zIndex: 35, display: "flex", flexDirection: "column", background: "rgba(15,16,18,0.55)" }}>
      <div onClick={onClose} style={{ flex: focused ? 0 : "0 0 12px", minHeight: 12 }} />
      <div style={{ flex: 1, background: C.bg, borderRadius: "20px 20px 0 0", display: "flex", flexDirection: "column",
        padding: showCapture ? "14px 18px 96px" : "14px 18px 28px", gap: 14, overflowY: showCapture ? "hidden" : "auto",
        ...drag.style }}>
        {/* grab handle — drag down to dismiss */}
        <div {...drag.handlers} style={{ flexShrink: 0, padding: "2px 0 6px", margin: "-2px 0 -4px", touchAction: "none", cursor: "grab" }}>
          <div style={{ width: 36, height: 4, background: C.line, borderRadius: 2, margin: "0 auto" }} />
        </div>

        {showCapture && (
          <>
            {/* camera / captured preview — shrinks when keyboard is up; drag to dismiss */}
            <div {...drag.handlers} style={{ flex: focused ? "0 0 84px" : 1, minHeight: 84, transition: "flex-basis .25s",
              background: "#000", borderRadius: 14, overflow: "hidden", position: "relative", touchAction: "none",
              display: "flex", alignItems: "center", justifyContent: "center" }}>
              {captured ? (
                <img src={captured} alt="Meal" style={{ width: "100%", height: "100%", objectFit: "cover" }} />
              ) : (
                <>
                  <video ref={videoRef} muted playsInline autoPlay
                    style={{ width: "100%", height: "100%", objectFit: "cover" }} />
                  {camErr && (
                    <div style={{ position: "absolute", inset: 0, display: "flex", alignItems: "center", justifyContent: "center",
                      textAlign: "center", color: C.faint, fontSize: 13, padding: 20 }}>{camErr}</div>
                  )}
                  {!camErr && (
                    <div style={{ position: "absolute", width: 54, height: 54, border: "2px solid rgba(255,255,255,0.5)", borderRadius: 8 }} />
                  )}
                </>
              )}
              {/* shutter / retake — hidden when keyboard up to save room */}
              {!focused && (
                captured ? (
                  <button onClick={() => setCaptured(null)}
                    style={{ position: "absolute", bottom: 12, left: "50%", transform: "translateX(-50%)",
                      background: "rgba(0,0,0,0.55)", color: "#fff", border: "1px solid rgba(255,255,255,0.5)",
                      borderRadius: 18, padding: "7px 16px", fontSize: 13, cursor: "pointer" }}>↻ Retake</button>
                ) : !camErr ? (
                  <button onClick={capture}
                    style={{ position: "absolute", bottom: 14, left: "50%", transform: "translateX(-50%)",
                      width: 56, height: 56, borderRadius: "50%", background: "rgba(255,255,255,0.15)",
                      border: "3px solid #fff", cursor: "pointer", padding: 0,
                      display: "flex", alignItems: "center", justifyContent: "center" }}>
                    <span style={{ width: 42, height: 42, borderRadius: "50%", background: "#fff", display: "block" }} />
                  </button>
                ) : null
              )}
            </div>

            {/* description field */}
            <textarea value={text} onChange={(e) => setText(e.target.value)}
              onFocus={() => setFocused(true)} onBlur={() => setFocused(false)}
              placeholder="…or describe the meal"
              style={{ flex: "0 0 auto", height: 56, background: C.card, border: `1px solid ${C.line}`,
                borderRadius: 12, color: C.text, padding: 12, fontSize: 15, resize: "none", boxSizing: "border-box" }} />

            {error && <div style={{ fontSize: 13, color: C.over, flexShrink: 0 }}>{error}</div>}
          </>
        )}

        {analyzing && (
          <div style={{ display: "flex", alignItems: "center", gap: 12, padding: "30px 4px", color: C.dim, fontSize: 14 }}>
            <span style={{ width: 16, height: 16, border: `2px solid ${C.line}`, borderTopColor: C.accent,
              borderRadius: "50%", display: "inline-block", animation: "omspin 0.8s linear infinite" }} />
            <span>Analyzing… this can take a few seconds.</span>
            <style>{`@keyframes omspin{to{transform:rotate(360deg)}}`}</style>
          </div>
        )}

        {status === "clarify" && questions && <Clarify questions={questions} onSubmit={onAnswer} />}
        {status === "result" && result && <Result entry={result} onLog={onLog} onDiscard={onDiscard} />}
      </div>
    </div>
  );
}

// ── tab button ────────────────────────────────────────────────────────────────
function TabBtn({ label, active, onClick }) {
  return (
    <button onClick={onClick} style={{ flex: 2, background: "none", border: "none", cursor: "pointer",
      color: active ? C.text : C.faint, fontSize: 13, fontWeight: active ? 600 : 400, paddingBottom: 6 }}>
      {label}
    </button>
  );
}

// ── APP SHELL ────────────────────────────────────────────────────────────────
function App() {
  const [tab, setTab] = useState("today");
  const [profile, setProfile] = useState(() => Store.getProfile());
  const [rev, setRev] = useState(0);
  const [info, setInfo] = useState(null);
  const [toastMsg, setToastMsg] = useState(null);
  const toastTimer = useRef(null);

  // PWA install: capture Android's beforeinstallprompt; detect already-installed.
  const deferredPrompt = useRef(null);
  const [installOpen, setInstallOpen] = useState(false);
  const [standalone, setStandalone] = useState(() =>
    (typeof window !== "undefined" &&
      (window.matchMedia("(display-mode: standalone)").matches ||
        window.matchMedia("(display-mode: fullscreen)").matches ||
        window.navigator.standalone === true)) || false
  );
  useEffect(() => {
    const onBIP = (e) => { e.preventDefault(); deferredPrompt.current = e; };
    const onInstalled = () => { deferredPrompt.current = null; setStandalone(true); };
    const mq = window.matchMedia("(display-mode: standalone)");
    const onMode = (e) => setStandalone(e.matches);
    window.addEventListener("beforeinstallprompt", onBIP);
    window.addEventListener("appinstalled", onInstalled);
    if (mq.addEventListener) mq.addEventListener("change", onMode);
    return () => {
      window.removeEventListener("beforeinstallprompt", onBIP);
      window.removeEventListener("appinstalled", onInstalled);
      if (mq.removeEventListener) mq.removeEventListener("change", onMode);
    };
  }, []);
  async function onInstall() {
    const dp = deferredPrompt.current;
    if (dp) {
      dp.prompt();
      try { await dp.userChoice; } catch (e) {}
      deferredPrompt.current = null;
    } else {
      setInstallOpen(true); // iOS, or Android without a native prompt available
    }
  }

  // add / scan flow state
  const [adding, setAdding] = useState(false);
  const [focused, setFocused] = useState(false);
  const [captured, setCaptured] = useState(null);
  const [addText, setAddText] = useState("");
  const [addStatus, setAddStatus] = useState("capture"); // capture | analyzing | clarify | result
  const [questions, setQuestions] = useState(null);
  const [result, setResult] = useState(null);
  const [addError, setAddError] = useState(null);
  const askResolver = useRef(null);

  // frame measurement (so the morphing FAB tweens responsively) + safe-area
  // insets (so nothing hides under a phone's notch / home indicator).
  const frameRef = useRef(null);
  const [dims, setDims] = useState({ w: 380, h: 760 });
  const [safe, setSafe] = useState({ top: 0, bottom: 0 });
  useEffect(() => {
    const el = frameRef.current; if (!el) return;
    const probe = document.createElement("div");
    probe.style.cssText = "position:fixed;top:0;left:0;visibility:hidden;pointer-events:none;" +
      "padding-top:env(safe-area-inset-top);padding-bottom:env(safe-area-inset-bottom);";
    document.body.appendChild(probe);
    const update = () => {
      setDims({ w: el.clientWidth, h: el.clientHeight });
      const cs = getComputedStyle(probe);
      setSafe({ top: parseFloat(cs.paddingTop) || 0, bottom: parseFloat(cs.paddingBottom) || 0 });
    };
    update();
    let ro;
    if (window.ResizeObserver) { ro = new ResizeObserver(update); ro.observe(el); }
    window.addEventListener("resize", update);
    return () => {
      if (ro) ro.disconnect();
      window.removeEventListener("resize", update);
      probe.remove();
    };
  }, []);

  const bump = useCallback(() => setRev((r) => r + 1), []);
  const toast = useCallback((msg) => {
    setToastMsg(msg);
    clearTimeout(toastTimer.current);
    toastTimer.current = setTimeout(() => setToastMsg(null), 2200);
  }, []);
  const applyProfile = useCallback((patch) => { const r = Store.patchProfile(patch); setProfile(r.profile); bump(); }, [bump]);
  const signOut = useCallback(async () => {
    try { await Data.flush(); } catch (e) {} // push any pending writes before we drop the local copy
    await Auth.logout();
    location.reload();                       // re-runs Root -> back to the sign-in screen
  }, []);

  function resetAdd() {
    setCaptured(null); setAddText(""); setQuestions(null); setResult(null);
    setAddError(null); setAddStatus("capture"); setFocused(false);
    askResolver.current = null;
  }
  function closeAdd() { resetAdd(); setAdding(false); }

  async function runScan() {
    if (addStatus !== "capture") return;
    if (!captured && !addText.trim()) { setAddError("Snap a photo or describe the meal first."); return; }
    setAddError(null); setAddStatus("analyzing");
    const ask = (qs) => new Promise((res) => { askResolver.current = res; setQuestions(qs); setAddStatus("clarify"); });
    let r;
    try { r = await Engine.analyzeMeal({ image: captured, text: addText.trim() }, ask); }
    catch (e) { r = { ok: false, error: `Network error: ${e.message || e}` }; }
    setQuestions(null);
    if (!r.ok) { setAddError(r.error || "Analysis failed."); setAddStatus("capture"); return; }
    setResult(r.entry); setAddStatus("result");
  }
  function answerQuestions(ans) {
    const res = askResolver.current; askResolver.current = null;
    setQuestions(null); setAddStatus("analyzing");
    if (res) res(ans);
  }
  function logResult(entry, eatenAt) {
    Store.logMeal(entry, eatenAt);
    closeAdd(); toast("Logged ✓"); bump(); setTab("today");
  }
  function discardResult() { resetAdd(); toast("Discarded"); }

  // FAB geometry — anchored top-left in every state so it can tween
  const fabVisible = !adding || addStatus === "capture";
  const fabAdding = adding && addStatus === "capture";
  const fabFocused = fabAdding && focused;
  const fabW = !adding ? 58 : fabFocused ? 84 : 150;
  const fabH = fabFocused ? 40 : 58;
  const fabTop = fabFocused ? 22 + safe.top : dims.h - 58 - 30 - safe.bottom;
  const fabLeft = fabFocused ? dims.w - 84 - 18 : (dims.w - fabW) / 2;

  return (
    <div style={{ height: "100dvh", overflow: "hidden", background: "#e8e4dc", display: "flex",
      justifyContent: "center", alignItems: "center", fontFamily: SANS, color: C.text }}>
      <FontLink />
      <div ref={frameRef} id="omni-frame" style={{ background: C.bg, position: "relative",
        display: "flex", flexDirection: "column", overflow: "hidden" }}>

        {/* body — the only scrollable region; contains its own overscroll */}
        <div style={{ flex: 1, minHeight: 0, overflowY: tab === "settings" ? "hidden" : "auto",
          overscrollBehavior: "contain", WebkitOverflowScrolling: "touch",
          paddingTop: 20 + safe.top, paddingLeft: 18, paddingRight: 18,
          paddingBottom: (tab === "settings" ? 90 : 96) + safe.bottom }}>
          {tab === "today" && <TodayView key={rev} profile={profile} toast={toast} onCoach={() => setTab("settings")}
            onInfo={setInfo} onInstall={onInstall} showInstall={!standalone} onSignOut={signOut} />}
          {tab === "history" && <HistoryView key={rev} profile={profile} toast={toast} onChange={bump} />}
          {tab === "settings" && <SettingsView key={rev} profile={profile} onApply={applyProfile} toast={toast} />}
        </div>

        {adding && (
          <AddSheet focused={focused} setFocused={setFocused} captured={captured} setCaptured={setCaptured}
            text={addText} setText={setAddText} status={addStatus} questions={questions} result={result}
            error={addError} onAnswer={answerQuestions} onLog={logResult} onDiscard={discardResult} onClose={closeAdd} />
        )}
        <InfoSheet item={info} onClose={() => setInfo(null)} />
        <InstallSheet open={installOpen} onClose={() => setInstallOpen(false)} />

        {/* tab bar with scooped notch for the blob; extends through the bottom
            safe area so the home indicator sits on paper, not a hard edge. */}
        <div style={{ position: "absolute", bottom: 0, left: 0, right: 0, height: 76 + safe.bottom, zIndex: 20 }}>
          <div style={{ position: "absolute", inset: 0, background: C.bg }} />
          <svg viewBox="0 0 380 76" width="100%" height="76" preserveAspectRatio="none" style={{ position: "absolute", top: 0, left: 0, right: 0 }}>
            <defs>
              <linearGradient id="barfade" x1="0" y1="0" x2="0" y2="1">
                <stop offset="0%" stopColor={C.card} />
                <stop offset="100%" stopColor={C.bg} />
              </linearGradient>
            </defs>
            <path d="M0,14 L150,14 C168,14 172,40 190,40 C208,40 212,14 230,14 L380,14 L380,76 L0,76 Z"
              fill="url(#barfade)" stroke={C.line} strokeWidth="1" />
          </svg>
          <div style={{ position: "absolute", top: 0, left: 0, right: 0, height: 76, display: "flex", alignItems: "center" }}>
            <TabBtn label="Today" active={tab === "today"} onClick={() => setTab("today")} />
            <div style={{ flex: 1.4 }} />
            <TabBtn label="History" active={tab === "history"} onClick={() => setTab("history")} />
          </div>
        </div>

        {/* blob FAB — '+' when closed, 'Scan' when adding, slides top-right when typing */}
        {fabVisible && (
          <button onClick={() => (adding ? runScan() : setAdding(true))}
            style={{ position: "absolute", zIndex: 50, color: "#fff", border: "none", background: C.accent,
              width: fabW, height: fabH, top: fabTop, left: fabLeft,
              fontSize: adding ? (fabFocused ? 14 : 16) : 28, fontWeight: adding ? 600 : 400,
              borderRadius: adding ? (fabFocused ? 20 : 29) : "50%",
              transition: "top .3s cubic-bezier(.4,0,.2,1), left .3s cubic-bezier(.4,0,.2,1), width .28s, height .28s, border-radius .28s, font-size .2s",
              cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center",
              boxShadow: "0 3px 10px rgba(60,50,35,0.2)" }}>
            {adding ? "Scan" : "+"}
          </button>
        )}

        {toastMsg && (
          <div style={{ position: "absolute", bottom: 92 + safe.bottom, left: "50%", transform: "translateX(-50%)", zIndex: 70,
            background: C.text, color: C.bg, padding: "9px 18px", borderRadius: 20, fontSize: 13,
            boxShadow: "0 4px 14px rgba(60,50,35,0.25)" }}>{toastMsg}</div>
        )}
      </div>
    </div>
  );
}

// ── AUTH / INSTALL GATE ──────────────────────────────────────────────────────
// Shared styles + a centered "warm paper" shell for the splash / install / sign-in screens.
const GATE = {
  input: { width: "100%", padding: "14px 16px", fontSize: 16, border: `1px solid ${C.line}`,
    borderRadius: 12, background: C.card, color: C.text, outline: "none" },
  primary: { width: "100%", marginTop: 14, padding: 14, fontSize: 15, fontWeight: 600, color: "#fff",
    background: C.accent, border: "none", borderRadius: 12, cursor: "pointer" },
  link: { background: "none", border: "none", color: C.dim, fontSize: 13, textDecoration: "underline", cursor: "pointer" },
};

// Is the app running as an installed PWA (standalone), not a browser tab?
// `omni_dev=1` in localStorage bypasses the gate so the app is testable in a browser.
function isInstalled() {
  try { if (localStorage.getItem("omni_dev") === "1") return true; } catch (_) {}
  return (typeof window !== "undefined" && (
    window.matchMedia("(display-mode: standalone)").matches ||
    window.matchMedia("(display-mode: fullscreen)").matches ||
    window.matchMedia("(display-mode: minimal-ui)").matches ||
    window.navigator.standalone === true
  )) || false;
}

function Shell({ children, center }) {
  return (
    <div style={{ height: "100dvh", background: "#e8e4dc", display: "flex",
      justifyContent: "center", alignItems: "center", fontFamily: SANS, color: C.text }}>
      <FontLink />
      <div id="omni-frame" style={{ background: C.bg, position: "relative", display: "flex",
        flexDirection: "column", justifyContent: center ? "center" : "flex-start", padding: "0 30px" }}>
        {children}
      </div>
    </div>
  );
}

function Wordmark({ tagline }) {
  return (
    <div style={{ textAlign: "center", marginBottom: 30 }}>
      <div style={{ fontFamily: SERIF, fontSize: 40, color: C.text }}>Omnivore</div>
      {tagline && <div style={{ fontSize: 14, color: C.faint, marginTop: 8 }}>{tagline}</div>}
    </div>
  );
}

function Splash() {
  return <Shell center><div style={{ textAlign: "center", fontFamily: SERIF, fontSize: 30, color: C.faint }}>Omnivore</div></Shell>;
}

// Shown in a plain browser tab: Omnivore only runs as an installed app, so this
// explains how to add it to the home screen (and offers Android's one-tap install).
function InstallGate() {
  const [dp, setDp] = useState(null); // Android beforeinstallprompt event
  useEffect(() => {
    const onBIP = (e) => { e.preventDefault(); setDp(e); };
    window.addEventListener("beforeinstallprompt", onBIP);
    return () => window.removeEventListener("beforeinstallprompt", onBIP);
  }, []);
  async function install() {
    if (!dp) return;
    dp.prompt();
    try { await dp.userChoice; } catch (_) {}
    setDp(null);
  }

  let steps;
  if (IS_IOS)
    steps = ["Open this page in Safari.", "Tap the Share button (the square with an up-arrow).",
      "Choose “Add to Home Screen”, then “Add”."];
  else if (IS_ANDROID)
    steps = ["Open the browser menu (⋮, top-right).", "Tap “Install app” or “Add to Home screen”.",
      "Confirm to add Omnivore."];
  else
    steps = ["Open your browser’s menu, or the install icon in the address bar.", "Choose “Install Omnivore”."];
  const insecure = typeof window !== "undefined" && !window.isSecureContext;

  return (
    <Shell center>
      <Wordmark tagline="Install the app to continue." />
      <div style={{ fontSize: 14, color: C.text, marginBottom: 10, fontWeight: 600 }}>Add Omnivore to your home screen</div>
      <ol style={{ margin: 0, paddingLeft: 20, color: C.dim, fontSize: 14, lineHeight: 1.85 }}>
        {steps.map((s, i) => <li key={i}>{s}</li>)}
      </ol>
      {dp && <button onClick={install} style={GATE.primary}>Install Omnivore</button>}
      {insecure && (
        <div style={{ marginTop: 14, fontSize: 13, color: C.over, lineHeight: 1.5 }}>
          {IS_IOS
            ? "The camera needs an https:// address inside the installed app."
            : "The install option only appears over an https:// address — open this over HTTPS."}
        </div>
      )}
      <div style={{ fontSize: 12, color: C.faint, marginTop: 18, textAlign: "center", lineHeight: 1.5 }}>
        Omnivore runs as an installed app so it can use your camera and keep you signed in.
      </div>
    </Shell>
  );
}

// Sign-in inside the installed app: enter email -> type the 6-digit code we email.
function AuthScreen({ onAuthed }) {
  const [step, setStep] = useState("email"); // email | code
  const [email, setEmail] = useState("");
  const [code, setCode] = useState("");
  const [busy, setBusy] = useState(false);
  const [err, setErr] = useState(null);

  async function sendCode(e) {
    if (e) e.preventDefault();
    const addr = email.trim().toLowerCase();
    if (!addr || !addr.includes("@")) { setErr("Enter a valid email address."); return; }
    setErr(null); setBusy(true);
    try {
      const r = await Auth.requestCode(addr);
      if (r && r.ok) { setEmail(addr); setCode(""); setStep("code"); }
      else setErr((r && r.error) || "Couldn't send the code. Try again.");
    } catch (_) { setErr("Network error. Try again."); }
    setBusy(false);
  }

  async function verify(e) {
    if (e) e.preventDefault();
    const c = code.trim();
    if (!/^\d{6}$/.test(c)) { setErr("Enter the 6-digit code from your email."); return; }
    setErr(null); setBusy(true);
    try {
      const r = await Auth.verifyCode(email, c);
      if (r.ok) { await onAuthed(); return; }
      setErr(r.error || "That code didn't work.");
    } catch (_) { setErr("Network error. Try again."); }
    setBusy(false);
  }

  return (
    <Shell center>
      <Wordmark tagline="Food logging from a photo." />
      {step === "email" ? (
        <form onSubmit={sendCode}>
          <input type="email" value={email} onChange={(e) => setEmail(e.target.value)}
            placeholder="you@example.com" autoComplete="email" inputMode="email" style={GATE.input} />
          {err && <div style={{ color: C.over, fontSize: 13, marginTop: 10 }}>{err}</div>}
          <button type="submit" disabled={busy} style={{ ...GATE.primary, opacity: busy ? 0.6 : 1 }}>
            {busy ? "Sending…" : "Email me a sign-in code"}
          </button>
          <div style={{ fontSize: 12, color: C.faint, marginTop: 14, textAlign: "center", lineHeight: 1.5 }}>
            No password. We email you a 6-digit code to sign in.
          </div>
        </form>
      ) : (
        <form onSubmit={verify}>
          <div style={{ fontSize: 14, color: C.dim, marginBottom: 12, textAlign: "center", lineHeight: 1.5 }}>
            Enter the 6-digit code we sent to<br /><b style={{ color: C.text }}>{email}</b>.
          </div>
          <input type="text" value={code} onChange={(e) => setCode(e.target.value.replace(/\D/g, "").slice(0, 6))}
            placeholder="123456" autoComplete="one-time-code" inputMode="numeric" maxLength={6}
            style={{ ...GATE.input, textAlign: "center", letterSpacing: 8, fontSize: 22 }} />
          {err && <div style={{ color: C.over, fontSize: 13, marginTop: 10 }}>{err}</div>}
          <button type="submit" disabled={busy} style={{ ...GATE.primary, opacity: busy ? 0.6 : 1 }}>
            {busy ? "Verifying…" : "Sign in"}
          </button>
          <div style={{ display: "flex", justifyContent: "space-between", marginTop: 16 }}>
            <button type="button" onClick={() => { setStep("email"); setErr(null); }} style={GATE.link}>Change email</button>
            <button type="button" onClick={() => sendCode()} disabled={busy} style={GATE.link}>Resend code</button>
          </div>
        </form>
      )}
    </Shell>
  );
}

// Decides what to show: splash while resolving, install gate (browser), sign-in,
// or the app. Mounts <App/> only after data is hydrated so its sync reads work.
function Root() {
  const [phase, setPhase] = useState("loading"); // loading | install | auth | app

  useEffect(() => {
    let alive = true;
    (async () => {
      if (!isInstalled()) { if (alive) setPhase("install"); return; }
      try {
        if (Auth.isLoggedIn()) {
          const me = await Auth.me();
          if (me.ok) { await Data.hydrate(); if (alive) setPhase("app"); return; }
        }
      } catch (_) {}
      if (alive) setPhase("auth");
    })();
    return () => { alive = false; };
  }, []);

  const enterApp = useCallback(async () => {
    try { await Data.hydrate(); } catch (_) {}
    setPhase("app");
  }, []);

  if (phase === "loading") return <Splash />;
  if (phase === "install") return <InstallGate />;
  if (phase === "app") return <App />;
  return <AuthScreen onAuthed={enterApp} />;
}

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