// MA-Trip — running web app shell.
// Stack-based screen router + event-delegated [data-nav] navigation.

const {
  HomeScreen, ItineraryScreen, MemoriesScreen, MemoryDetailScreen, AddMemoryScreen,
  MapScreen, RemindersScreen, BudgetScreen, DocumentsScreen, PackingScreen, PlanningScreen,
  SpotsScreen,
  useStore, A,
} = window;
const TripStatsScreen = window.TripStatsScreen;

// Universal media renderer — handles "idb://..." refs, dataURLs, and remote URLs.
// Falls back to a soft skeleton while async-resolving IDB blobs.
function MediaImg({ src, alt = "", style, className, onClick, draggable, fallback }) {
  const initial = (window.MaMedia && window.MaMedia.resolveSync)
    ? window.MaMedia.resolveSync(src) : (src || null);
  const [resolved, setResolved] = React.useState(initial);
  const [errored, setErrored] = React.useState(false);
  React.useEffect(() => {
    let alive = true;
    setErrored(false);
    if (!src) { setResolved(null); return; }
    if (window.MaMedia && window.MaMedia.isRef && window.MaMedia.isRef(src)) {
      const cached = window.MaMedia.resolveSync(src);
      if (cached) { setResolved(cached); return; }
      window.MaMedia.getURL(src).then(url => {
        if (!alive) return;
        if (url) setResolved(url);
        else { setErrored(true); console.warn("[MediaImg] IDB ref missing:", src); }
      }).catch(err => {
        if (!alive) return;
        setErrored(true);
        console.warn("[MediaImg] IDB resolve failed:", src, err);
      });
    } else {
      setResolved(src);
    }
    return () => { alive = false; };
  }, [src]);
  if (errored) {
    return (
      <div className={className || ""} style={{
        ...style,
        background: "linear-gradient(135deg, #f0f0f3, #e5e5ea)",
        display: "flex", alignItems: "center", justifyContent: "center",
        color: "var(--muted)", fontSize: 11, fontFamily: "var(--mono)",
      }} aria-label={alt || "תמונה אינה זמינה"} role="img">תמונה חסרה</div>
    );
  }
  if (!resolved) {
    return (
      <div className={"skel " + (className || "")} style={{ ...style }} aria-label={alt} role="img">
        {fallback || null}
      </div>
    );
  }
  return (
    <img src={resolved} alt={alt} style={style} className={className}
         onClick={onClick} draggable={draggable === false ? false : undefined}
         onError={() => {
           setErrored(true);
           // Drop the bad blob URL from the global cache so future reads of the
           // same ref don't keep returning a broken URL (and so it can be GC'd).
           if (window.MaMedia && window.MaMedia.isRef && window.MaMedia.isRef(src)) {
             const id = window.MaMedia.refToId(src);
             const cache = window.MaMedia._urlCache;
             if (id && cache && cache.has(id)) {
               try { URL.revokeObjectURL(cache.get(id)); } catch (e) {}
               cache.delete(id);
             }
           }
         }} />
  );
}
window.MediaImg = MediaImg;

function ColorSheet({ sheet }) {
  const COLORS = window.COLORS || [];
  return (
    <div onClick={() => A.closeSheet()} dir="rtl" style={{
      position: "fixed", inset: 0, zIndex: 9999,
      background: "rgba(0,0,0,.42)",
      display: "flex", flexDirection: "column", justifyContent: "flex-end",
      padding: "0 8px",
      paddingBottom: "calc(env(safe-area-inset-bottom, 0px) + 8px)",
    }}>
      <div className="lg-light" onClick={e => e.stopPropagation()} style={{
        borderRadius: 18, overflow: "hidden", marginBottom: 8,
      }}>
        <div style={{
          padding: "14px 16px 10px", textAlign: "center",
          fontSize: 13, color: "var(--muted)",
          borderBottom: "0.5px solid rgba(0,0,0,.18)",
        }}>בחר צבע · {sheet.title}</div>
        <div style={{
          display: "grid", gridTemplateColumns: "repeat(6, 1fr)",
          gap: 14, padding: 20,
          placeItems: "center",
        }}>
          {COLORS.map(c => {
            const selected = sheet.currentColor === c.hex;
            return (
              <button key={c.hex} onClick={() => {
                A.setItemColor(sheet.collection, sheet.itemId, c.hex);
                A.closeSheet();
              }} aria-label={c.name} style={{
                width: 40, height: 40, borderRadius: 9999,
                background: c.hex, border: "none", padding: 0,
                cursor: "pointer", position: "relative",
                boxShadow: selected
                  ? "inset 0 0 0 3px #fff, 0 0 0 2px " + c.hex
                  : "inset 0 0 0 0.5px rgba(0,0,0,.08)",
              }}>
                {selected && (
                  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" style={{ position: "absolute", top: "50%", left: "50%", transform: "translate(-50%, -50%)" }}>
                    <path d="M5 12l5 5 9-11" stroke="#fff" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round"/>
                  </svg>
                )}
              </button>
            );
          })}
          <button onClick={() => {
            A.setItemColor(sheet.collection, sheet.itemId, null);
            A.closeSheet();
          }} aria-label="ללא צבע" style={{
            width: 40, height: 40, borderRadius: 9999,
            background: "transparent",
            border: "1.5px dashed var(--border-strong, #c1c1c1)",
            padding: 0, cursor: "pointer",
            color: "var(--muted)",
            display: "flex", alignItems: "center", justifyContent: "center",
          }}>
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none"><path d="M6 6l12 12M18 6L6 18" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/></svg>
          </button>
        </div>
      </div>
      <button onClick={() => A.closeSheet()} style={{
        padding: "16px", borderRadius: 14, border: "none",
        background: "var(--canvas)", color: "#0a66d6",
        fontSize: 18, fontWeight: 600,
        cursor: "pointer", fontFamily: "inherit",
      }}>ביטול</button>
    </div>
  );
}

/* iOS-style EditSheet — modal form with text/number/select/textarea fields */
function EditSheet() {
  const sheet = useStore(s => s.editSheet);
  // All hooks must run on every render in the same order — keep them ABOVE
  // any early return. Putting useState below `if (!sheet) return null` was
  // the bug that made the sheet unusable: React threw "hook order mismatch"
  // and the redesigned form silently failed to mount its input state.
  const [values, setValues] = React.useState({});
  const [focusedKey, setFocusedKey] = React.useState(null);
  // iOS keyboard fix: track visualViewport so the sheet shrinks above the
  // on-screen keyboard. Without this, iOS PWA standalone leaves the keyboard
  // covering the input + submit and the user feels stuck.
  const [vvHeight, setVvHeight] = React.useState(null);
  const firstInputRef = React.useRef(null);
  const dialogRef = React.useRef(null);
  const previouslyFocusedRef = React.useRef(null);
  const titleId = React.useId ? React.useId() : "edit-sheet-title";

  React.useEffect(() => {
    if (!sheet) return;
    previouslyFocusedRef.current = document.activeElement;
    const init = {};
    (sheet.fields || []).forEach(f => { init[f.key] = f.value != null ? f.value : ""; });
    setValues(init);
    setFocusedKey(null);
    // Skip auto-focus on touch devices — iOS won't open the keyboard without
    // a user gesture, so the focused-but-blank input just confuses the user.
    const isTouch = typeof window !== "undefined" && window.matchMedia && window.matchMedia("(pointer: coarse)").matches;
    const raf = isTouch ? 0 : requestAnimationFrame(() => firstInputRef.current?.focus());
    return () => {
      if (raf) cancelAnimationFrame(raf);
      const prev = previouslyFocusedRef.current;
      if (prev && typeof prev.focus === "function") {
        try { prev.focus(); } catch (e) {}
      }
    };
  }, [sheet]);

  React.useEffect(() => {
    if (!sheet || typeof window === "undefined" || !window.visualViewport) return;
    const vv = window.visualViewport;
    const update = () => setVvHeight(vv.height);
    update();
    vv.addEventListener("resize", update);
    vv.addEventListener("scroll", update);
    return () => {
      vv.removeEventListener("resize", update);
      vv.removeEventListener("scroll", update);
      setVvHeight(null);
    };
  }, [sheet]);

  if (!sheet) return null;

  const submit = async () => {
    const f = sheet.fields || [];
    for (const fld of f) {
      if (fld.required && !(values[fld.key] || "").toString().trim()) {
        A.showToast("חסר: " + fld.label, "error");
        return;
      }
    }
    A.closeEditSheet();
    // Await async onSubmit so failures surface (silent-failure fix F10).
    try { await (sheet.onSubmit && sheet.onSubmit(values)); }
    catch (e) { console.error("[EditSheet.onSubmit]", e); A.showToast("שגיאה בשמירה", "error"); }
  };

  // Focus trap — keep Tab cycle inside the dialog.
  const trapFocus = (e) => {
    if (e.key !== "Tab" || !dialogRef.current) return;
    const focusables = dialogRef.current.querySelectorAll(
      'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]):not([type="hidden"]), select:not([disabled]), [tabindex]:not([tabindex="-1"])'
    );
    if (!focusables.length) return;
    const first = focusables[0], last = focusables[focusables.length - 1];
    if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
    else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
  };

  return (
    <div onClick={() => A.closeEditSheet()} dir="rtl"
      className="ma-sheet-backdrop"
      style={{
        position: "fixed", inset: 0, zIndex: 9999,
        background: "rgba(0,0,0,.42)",
        display: "flex", flexDirection: "column", justifyContent: "flex-end",
        padding: 0,
      }}>
      <div className="lg-sheet ma-sheet-slide" ref={dialogRef}
        role="dialog" aria-modal="true" aria-labelledby={titleId}
        onClick={e => e.stopPropagation()} onKeyDown={e => {
          trapFocus(e);
          // Enter submits — but skip when focus is inside a textarea or a select
          // (select uses Enter to confirm dropdown choice on desktop).
          if (e.key === "Enter" && e.target.tagName !== "TEXTAREA" && e.target.tagName !== "SELECT") { e.preventDefault(); submit(); }
          if (e.key === "Escape") { e.preventDefault(); A.closeEditSheet(); }
        }} style={{
          overflow: "hidden",
          maxHeight: vvHeight ? Math.max(260, vvHeight - 16) + "px" : "92vh",
          display: "flex", flexDirection: "column",
          paddingBottom: "calc(env(safe-area-inset-bottom, 0px) + 8px)",
        }}>
        {/* Drag handle */}
        <div className="ma-sheet-handle" aria-hidden="true" />

        {/* Header — title + close (X) */}
        <div style={{
          padding: "8px 20px 14px",
          display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12,
        }}>
          <div style={{ display: "flex", flexDirection: "column", flex: 1, minWidth: 0 }}>
            <div className="eyebrow" style={{ color: "var(--coral)" }}>טופס</div>
            <div id={titleId} style={{
              fontSize: 22, fontWeight: 700, color: "var(--ink)",
              letterSpacing: "-.022em", marginTop: 2, lineHeight: 1.15,
            }}>{sheet.title}</div>
          </div>
          <button onClick={() => A.closeEditSheet()} aria-label="סגירה" style={{
            width: 36, height: 36, borderRadius: 9999, padding: 0,
            border: "none", background: "var(--surface-soft)", color: "var(--ink)",
            display: "flex", alignItems: "center", justifyContent: "center",
            cursor: "pointer", fontFamily: "inherit", flexShrink: 0,
          }}>
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none">
              <path d="M6 6l12 12M18 6L6 18" stroke="currentColor" strokeWidth="2" strokeLinecap="round"/>
            </svg>
          </button>
        </div>

        {/* Fields */}
        <div style={{
          padding: "4px 20px 8px",
          overflowY: "auto",
          display: "flex", flexDirection: "column", gap: 16,
          WebkitOverflowScrolling: "touch",
        }}>
          {(sheet.fields || []).map((f, i) => {
            const val = values[f.key] != null ? values[f.key] : "";
            const onChange = (e) => setValues(v => ({ ...v, [f.key]: e.target.value }));
            const refProp = i === 0 || f.autoFocus ? { ref: firstInputRef } : {};
            const focused = focusedKey === f.key;
            const common = {
              ...refProp,
              className: "ma-field-input",
              value: val, onChange,
              onFocus: (e) => {
                setFocusedKey(f.key);
                // iOS PWA: scroll input into view after the keyboard finishes opening.
                const el = e.currentTarget;
                setTimeout(() => {
                  try { el.scrollIntoView({ block: "center", behavior: "smooth" }); } catch (_) {}
                }, 250);
              },
              onBlur: () => setFocusedKey(k => k === f.key ? null : k),
              placeholder: f.placeholder || "",
            };
            return (
              <label key={f.key} className={"ma-field" + (focused ? " is-focused" : "")}>
                <span className="ma-field-label">{f.label}{f.required ? " *" : ""}</span>
                {f.type === "textarea" ? (
                  <textarea {...common} />
                ) : f.type === "select" ? (
                  <select {...common}>
                    {(f.options || []).map(o => <option key={o} value={o}>{o}</option>)}
                  </select>
                ) : f.type === "combo" ? (
                  <>
                    <input type="text" list={"combo-" + f.key} {...common} />
                    <datalist id={"combo-" + f.key}>
                      {(f.options || []).map(o => <option key={o} value={o} />)}
                    </datalist>
                  </>
                ) : (
                  <input
                    type={f.type === "number" ? "number" : f.type === "date" ? "date" : "text"}
                    inputMode={f.type === "number" ? "decimal" : undefined}
                    {...common}
                  />
                )}
              </label>
            );
          })}
        </div>

        {/* CTA bar — coral submit + ghost cancel */}
        <div style={{
          padding: "14px 20px 6px",
          display: "flex", gap: 10,
          borderTop: "0.5px solid rgba(0,0,0,.06)",
          background: "linear-gradient(180deg, rgba(255,255,255,0) 0%, rgba(255,255,255,.85) 100%)",
        }}>
          <button onClick={() => A.closeEditSheet()} type="button" style={{
            padding: "16px 22px", borderRadius: 16, border: "none",
            background: "var(--surface-soft)", color: "var(--ink)",
            fontFamily: "inherit", fontSize: 15, fontWeight: 600, cursor: "pointer",
            minHeight: 52,
          }}>ביטול</button>
          <button onClick={submit} type="button" className="lg-coral ma-sheet-cta" style={{ flex: 1 }}>
            {sheet.submitLabel || "שמרי"}
          </button>
        </div>
      </div>
    </div>
  );
}

/* iOS-style Confirm — two-button modal, red destructive */
function ConfirmSheet() {
  const sheet = useStore(s => s.confirmSheet);
  const cancelRef = React.useRef(null);
  const confirmRef = React.useRef(null);
  const dialogRef = React.useRef(null);
  const previouslyFocusedRef = React.useRef(null);
  const titleId = React.useId ? React.useId() : "confirm-sheet-title";

  React.useEffect(() => {
    if (!sheet) return;
    previouslyFocusedRef.current = document.activeElement;
    const raf = requestAnimationFrame(() => cancelRef.current?.focus());
    return () => {
      cancelAnimationFrame(raf);
      const prev = previouslyFocusedRef.current;
      if (prev && typeof prev.focus === "function") { try { prev.focus(); } catch (e) {} }
    };
  }, [sheet]);

  if (!sheet) return null;
  const go = async () => {
    A.closeConfirm();
    try { await (sheet.onConfirm && sheet.onConfirm()); }
    catch (e) { console.error("[ConfirmSheet.onConfirm]", e); A.showToast("הפעולה נכשלה", "error"); }
  };

  const onKey = (e) => {
    if (e.key === "Escape") { e.preventDefault(); A.closeConfirm(); return; }
    if (e.key === "Enter")  { e.preventDefault(); go(); return; }
    if (e.key !== "Tab" || !dialogRef.current) return;
    // Focus trap between two buttons
    const focusables = dialogRef.current.querySelectorAll("button");
    if (focusables.length < 2) return;
    const first = focusables[0], last = focusables[focusables.length - 1];
    if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
    else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
  };

  return (
    <div onClick={() => A.closeConfirm()} dir="rtl"
      className="ma-sheet-backdrop"
      style={{
        position: "fixed", inset: 0, zIndex: 10000,
        background: "rgba(0,0,0,.5)",
        display: "flex", alignItems: "center", justifyContent: "center", padding: 24,
      }}>
      <div className="lg-bento ma-sheet-pop" ref={dialogRef}
        role="alertdialog" aria-modal="true" aria-labelledby={titleId}
        onClick={e => e.stopPropagation()} onKeyDown={onKey} style={{
          width: "100%", maxWidth: 340, padding: "26px 22px 18px",
          display: "flex", flexDirection: "column", gap: 14,
        }}>
        {/* Icon — small destructive bubble or info bubble */}
        <div style={{
          width: 52, height: 52, borderRadius: 9999, alignSelf: "center",
          background: sheet.destructive
            ? "linear-gradient(135deg, #ff7575, #ff3b30)"
            : "linear-gradient(135deg, #2d83ee, #0852b5)",
          color: "#fff",
          display: "flex", alignItems: "center", justifyContent: "center",
          boxShadow: sheet.destructive
            ? "0 10px 24px -6px rgba(255,59,48,.45)"
            : "0 10px 24px -6px rgba(10,102,214,.45)",
        }} aria-hidden="true">
          {sheet.destructive
            ? <svg width="22" height="22" viewBox="0 0 24 24" fill="none"><path d="M4 7h16M9 7V4h6v3M6 7l1 13h10l1-13" stroke="#fff" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"/></svg>
            : <svg width="22" height="22" viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="9" stroke="#fff" strokeWidth="1.8"/><path d="M12 8v5M12 16.5v.5" stroke="#fff" strokeWidth="2" strokeLinecap="round"/></svg>}
        </div>

        <div style={{ textAlign: "center", display: "flex", flexDirection: "column", gap: 6 }}>
          <div id={titleId} style={{
            fontSize: 19, fontWeight: 700, color: "var(--ink)",
            letterSpacing: "-.018em",
          }}>{sheet.title}</div>
          {sheet.body && (
            <div style={{ fontSize: 14, color: "var(--muted)", lineHeight: 1.5 }}>{sheet.body}</div>
          )}
        </div>

        <div style={{ display: "flex", gap: 10, marginTop: 4 }}>
          <button ref={cancelRef} onClick={() => A.closeConfirm()} type="button" style={{
            flex: 1, padding: "14px", border: "none",
            background: "var(--surface-soft)", color: "var(--ink)",
            fontSize: 15, fontWeight: 600, fontFamily: "inherit", cursor: "pointer",
            borderRadius: 14, minHeight: 50,
          }}>{sheet.cancelLabel || "ביטול"}</button>
          <button ref={confirmRef} onClick={go} type="button"
            className={sheet.destructive ? "" : "lg-coral"}
            style={{
              flex: 1, padding: "14px", border: "none",
              background: sheet.destructive ? "linear-gradient(180deg, #ff5b52, #ff3b30)" : undefined,
              color: "#fff",
              fontSize: 15, fontWeight: 700, fontFamily: "inherit", cursor: "pointer",
              borderRadius: 14, minHeight: 50,
              boxShadow: sheet.destructive
                ? "0 10px 24px -6px rgba(255,59,48,.45), inset 0 1px 0 rgba(255,255,255,.3)"
                : undefined,
            }}>{sheet.confirmLabel || "אישור"}</button>
        </div>
      </div>
    </div>
  );
}

/* Toast — auto-dismissing snackbar (positioned above tab bar) */
function Toast() {
  const t = useStore(s => s.toast);
  if (!t) return null;
  const hasAction = t.action && typeof t.action.fn === "function";
  const isError = t.kind === "error";
  return (
    <div dir="rtl"
      role={isError ? "alert" : "status"}
      aria-live={isError ? "assertive" : "polite"}
      aria-atomic="true"
      style={{
      position: "fixed", left: "50%", transform: "translateX(-50%)",
      bottom: "calc(110px + env(safe-area-inset-bottom, 0px))",
      zIndex: 10001,
      padding: hasAction ? "8px 8px 8px 18px" : "12px 18px",
      borderRadius: 9999,
      background: isError ? "rgba(255,59,48,.96)" : "rgba(20,20,22,.94)",
      color: "#fff", fontSize: 14, fontWeight: 500,
      WebkitBackdropFilter: "blur(12px)", backdropFilter: "blur(12px)",
      boxShadow: "0 8px 24px rgba(0,0,0,.3)",
      maxWidth: "85vw", display: "flex", alignItems: "center", gap: 10,
      animation: "toast-in .22s ease-out",
    }}>
      <span style={{ flex: 1, pointerEvents: "none" }}>{t.message}</span>
      {hasAction && (
        <button onClick={(e) => {
          e.stopPropagation();
          try { t.action.fn(); } catch (err) { console.error(err); }
          if (window._haptic) window._haptic("tap");
          window.Store.set({ toast: null });
        }} style={{
          background: "rgba(255,255,255,.18)", border: "1px solid rgba(255,255,255,.3)",
          color: "#fff", fontSize: 13, fontWeight: 700, padding: "8px 14px",
          borderRadius: 9999, cursor: "pointer", fontFamily: "inherit",
        }}>{t.action.label}</button>
      )}
    </div>
  );
}

/* iOS-style ActionSheet — bottom-anchored, frosted background, red destructive */
function ActionSheet() {
  const sheet = useStore(s => s.actionSheet);
  const dialogRef = React.useRef(null);
  const restoreRef = React.useRef(null);
  const titleId = React.useId ? React.useId() : "ma-sheet-title";

  React.useEffect(() => {
    if (!sheet) return;
    restoreRef.current = document.activeElement;
    const onKey = (e) => {
      if (e.key === "Escape") { e.preventDefault(); A.closeSheet(); return; }
      if (e.key === "Tab" && dialogRef.current) {
        const focusables = dialogRef.current.querySelectorAll(
          'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
        );
        if (!focusables.length) return;
        const first = focusables[0];
        const last = focusables[focusables.length - 1];
        if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }
        else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }
      }
    };
    document.addEventListener("keydown", onKey);
    const t = setTimeout(() => {
      const btn = dialogRef.current && dialogRef.current.querySelector("button");
      if (btn) btn.focus();
    }, 30);
    return () => {
      document.removeEventListener("keydown", onKey);
      clearTimeout(t);
      const prior = restoreRef.current;
      if (prior && typeof prior.focus === "function") {
        try { prior.focus(); } catch (_) {}
      }
    };
  }, [!!sheet]);

  if (!sheet) return null;
  if (sheet.type === "color") return <ColorSheet sheet={sheet} />;
  return (
    <div onClick={() => A.closeSheet()} dir="rtl"
      className="ma-sheet-backdrop"
      style={{
        position: "fixed", inset: 0, zIndex: 9999,
        background: "rgba(0,0,0,.42)",
        display: "flex", flexDirection: "column", justifyContent: "flex-end",
        padding: 0,
      }}>
      <div ref={dialogRef} onClick={e => e.stopPropagation()} className="ma-sheet-slide"
        role="dialog" aria-modal="true"
        aria-labelledby={sheet.title ? titleId : undefined}
        aria-label={sheet.title ? undefined : "תפריט פעולות"}
        style={{
        display: "flex", flexDirection: "column", gap: 8,
        padding: "0 8px",
        paddingBottom: "calc(env(safe-area-inset-bottom, 0px) + 8px)",
      }}>
        <div className="lg-sheet" style={{
          overflow: "hidden",
          borderRadius: 22,
        }}>
          <div className="ma-sheet-handle" aria-hidden="true" />
          {sheet.title && (
            <div id={titleId} style={{
              padding: "6px 20px 14px", textAlign: "center",
              fontSize: 13, color: "var(--muted)", fontWeight: 500,
              borderBottom: "0.5px solid rgba(0,0,0,.06)",
            }}>{sheet.title}</div>
          )}
          {sheet.options.map((opt, i) => (
            <button key={i} onClick={() => {
              const fn = opt.action;
              A.closeSheet();
              if (fn) setTimeout(fn, 0);
            }} type="button" style={{
              display: "block", width: "100%",
              padding: "17px 20px",
              border: "none",
              background: "transparent",
              fontSize: 17,
              fontWeight: opt.destructive ? 600 : 500,
              color: opt.destructive ? "#ff3b30" : "var(--action)",
              borderTop: (i > 0) ? "0.5px solid rgba(0,0,0,.06)" : (sheet.title ? "none" : "none"),
              cursor: "pointer",
              fontFamily: "inherit",
              textAlign: "center",
              letterSpacing: "-.012em",
              minHeight: 54,
              transition: "background .15s ease",
            }} onMouseDown={(e) => e.currentTarget.style.background = "rgba(0,0,0,.04)"}
               onMouseUp={(e) => e.currentTarget.style.background = "transparent"}
               onMouseLeave={(e) => e.currentTarget.style.background = "transparent"}
               onTouchStart={(e) => e.currentTarget.style.background = "rgba(0,0,0,.04)"}
               onTouchEnd={(e) => e.currentTarget.style.background = "transparent"}>{opt.label}</button>
          ))}
        </div>
        <button onClick={() => A.closeSheet()} type="button" style={{
          padding: "17px",
          borderRadius: 18,
          border: "none",
          background: "linear-gradient(180deg, rgba(255,255,255,0.97) 0%, rgba(252,247,238,0.94) 100%)",
          backdropFilter: "saturate(180%) blur(20px)",
          WebkitBackdropFilter: "saturate(180%) blur(20px)",
          color: "var(--ink)",
          fontSize: 17,
          fontWeight: 700,
          cursor: "pointer",
          fontFamily: "inherit",
          boxShadow:
            "inset 0 1px 0 rgba(255,255,255,1)," +
            "0 8px 20px -4px rgba(0,0,0,.18)",
          minHeight: 54,
        }}>ביטול</button>
      </div>
    </div>
  );
}

const ROUTES = {
  home:      HomeScreen,
  plan:      ItineraryScreen,
  mem:       MemoriesScreen,
  memory:    MemoryDetailScreen,
  add:       AddMemoryScreen,
  planning:  PlanningScreen,
  map:       MapScreen,
  reminders: RemindersScreen,
  budget:    BudgetScreen,
  docs:      DocumentsScreen,
  packing:   PackingScreen,
  spots:     SpotsScreen,
  stats:     TripStatsScreen,
};

// Which tab to highlight for each route. null = no tab bar (e.g., modal-style screens).
const TAB_BY_ROUTE = {
  home:      { active: "home" },
  plan:      { active: "plan" },
  mem:       { active: "mem" },
  budget:    { active: "budget" },
  planning:  { active: "plan" },
  map:       { active: "plan" },
  reminders: { active: "home" },
  docs:      { active: "home" },
  packing:   { active: "home" },
  spots:     { active: "plan" },
  stats:     { active: "home" },
  memory:    null,
  add:       null,
};

const SCREEN_TITLES = {
  home:      "בית",
  plan:      "מסלול",
  mem:       "זיכרונות",
  memory:    "זיכרון",
  add:       "זיכרון חדש",
  planning:  "תכנון",
  map:       "מפה",
  reminders: "תזכורות",
  budget:    "תקציב",
  docs:      "מסמכים",
  packing:   "אריזה",
  spots:     "מקומות",
  stats:     "סיכום",
};

function App() {
  const [stack, setStack] = React.useState(["home"]);
  const current = stack[stack.length - 1];
  const frameRef = React.useRef(null);
  const screenWrapRef = React.useRef(null);
  const prevDepthRef = React.useRef(1);
  const [navDir, setNavDir] = React.useState("fwd");
  const currentUserId = useStore(s => s.currentUserId);

  const isFirstNavRef = React.useRef(true);
  React.useEffect(() => {
    if (frameRef.current) frameRef.current.scrollTop = 0;
    // Skip the slide-in animation on the very first mount (otherwise Home
    // gets a forward slide animation that feels like a "navigation" instead
    // of the initial app paint).
    if (isFirstNavRef.current) {
      isFirstNavRef.current = false;
      prevDepthRef.current = stack.length;
    } else {
      const depth = stack.length;
      if (depth < prevDepthRef.current) setNavDir("back");
      else setNavDir("fwd");
      prevDepthRef.current = depth;
    }
    // Move focus to the new screen so screen-reader / keyboard users land
    // in the new context instead of staying on the previous (now-hidden)
    // element. tabIndex=-1 is set on the wrapper so .focus() works.
    const rafId = requestAnimationFrame(() => {
      const el = screenWrapRef.current;
      if (el && typeof el.focus === "function") {
        try { el.focus({ preventScroll: true }); } catch (_) { try { el.focus(); } catch (_) {} }
      }
    });
    // Per-route document title so browser history / VoiceOver page-title
    // announcements reflect the current screen.
    const label = SCREEN_TITLES[current];
    document.title = label ? `${label} — MA-Trip` : "MA-Trip";
    return () => cancelAnimationFrame(rafId);
  }, [current, stack.length]);

  React.useEffect(() => {
    // Surface any state-load error captured before A existed (see store.jsx loadState).
    if (window.__maTripLoadError && A && A.showToast) {
      A.showToast("שגיאה בטעינת הנתונים — חלק מהמידע אולי לא נטען", "error");
      window.__maTripLoadError = null;
    }
    if (A && A.tryImportFromHash) setTimeout(() => A.tryImportFromHash(), 200);
    // fetchRates now throws on network failure (so PTR can surface it).
    // Boot path is fire-and-forget — swallow here.
    if (A && A.fetchRates) A.fetchRates().catch(e => console.warn("[fetchRates boot]", e));
    // Migrations chain: legacy dataURL → IDB blobs first, THEN encrypt any
    // remaining plaintext. Sequencing prevents a race where the encrypt sweep
    // snapshots IDB before legacy migration is done writing new plaintext records.
    if (A && A.migrateLegacyMedia) {
      setTimeout(async () => {
        try { await A.migrateLegacyMedia(); }
        catch (e) { console.warn("[MaMedia] legacy migration failed:", e); }
        if (window.MaMedia && window.MaMedia.migrateUnencrypted) {
          try { await window.MaMedia.migrateUnencrypted(); }
          catch (e) { console.warn("[MaMedia] encrypt migration failed:", e); }
        }
      }, 600);
    }

    // Email-confirmation handler — when the user clicks the link in their
    // signup email, they land at `<app>/#confirm=<token>`. Detect that,
    // call the worker to mark the account active, then clear the hash + toast.
    (async () => {
      try {
        const m = /^#?confirm=(.+)$/.exec(location.hash || "");
        if (!m || !window.MaApi || !window.MaApi.confirmEmail) return;
        const token = decodeURIComponent(m[1]);
        try { history.replaceState(null, "", location.pathname + location.search); } catch (e) {}
        const resp = await window.MaApi.confirmEmail(token);
        if (resp && resp.ok) {
          A.showToast("המייל אומת — אפשר להתחבר עם המייל והסיסמא", "info");
        }
      } catch (e) {
        const map = {
          "missing-token":     "קישור אישור חסר",
          "invalid-token":     "קישור אישור לא תקין",
          "token-already-used":"הקישור כבר נוצל — אפשר להתחבר",
          "token-expired":     "קישור פג תוקף — הירשמי שוב",
        };
        A.showToast(map[e.code] || ("אישור נכשל: " + (e.code || e.message)), "error");
      }
    })();
    // Skip the boot me() call when auth is bypassed — guaranteed 401 for
    // local-only users, wasted round-trip on every load.
    const realAuth = !!(window.MA_REAL_AUTH);
    if (window.MaApi && realAuth) {
      window.MaApi.me().then(
        (resp) => {
          if (resp && resp.user) {
            window.Store.set(s => ({
              ...s,
              currentUserId: resp.user.id,
              accounts: [
                ...(s.accounts || []).filter(a => a.id !== resp.user.id),
                { id: resp.user.id, name: resp.user.name, email: resp.user.email, color: resp.user.color, createdAt: Date.now() },
              ],
            }));
          }
        },
        (err) => {
          if (err && err.status && err.status !== 401) {
            console.warn("[MaApi] me() failed:", err);
          }
        },
      );
    }
  }, []);

  // A11y: promote every `data-nav` div to a focusable button-like element
  // so VoiceOver/TalkBack/Tab nav can reach it. Re-runs on DOM mutations,
  // debounced with requestAnimationFrame so typing in a sheet doesn't trigger
  // a querySelectorAll per keystroke.
  React.useEffect(() => {
    if (!frameRef.current) return;
    const frame = frameRef.current;
    let scheduled = false;
    const promote = () => {
      scheduled = false;
      frame.querySelectorAll("[data-nav]").forEach(el => {
        const tag = el.tagName;
        if (tag === "BUTTON" || tag === "A") return;
        if (!el.hasAttribute("tabindex")) el.setAttribute("tabindex", "0");
        if (!el.hasAttribute("role")) el.setAttribute("role", "button");
      });
    };
    const schedule = () => {
      if (scheduled) return;
      scheduled = true;
      requestAnimationFrame(promote);
    };
    promote();
    const obs = new MutationObserver(schedule);
    obs.observe(frame, { childList: true, subtree: true, attributeFilter: ["data-nav"] });
    return () => obs.disconnect();
  }, [currentUserId]);

  // Pull-to-refresh: only triggers when scrollTop is 0 at touchstart AND the
  // touch did NOT start on a horizontally-scrollable child (day strip, photo
  // strip). Surface real success/failure instead of a constant success toast.
  React.useEffect(() => {
    if (!frameRef.current) return;
    const frame = frameRef.current;
    const indicator = document.createElement("div");
    indicator.className = "ma-ptr-indicator";
    indicator.setAttribute("aria-hidden", "true");
    frame.appendChild(indicator);

    let startY = null;
    let startX = null;
    let dragging = false;
    let dy = 0;
    const TRIGGER = 64;
    const MAX = 110;
    const H_TOLERANCE = 12; // px — if horizontal travel exceeds this, bail (horizontal scroll wins)

    const isInsideHorizontalScroller = (target) => {
      let el = target;
      while (el && el !== frame) {
        if (el.scrollWidth > el.clientWidth + 4) return true;
        el = el.parentElement;
      }
      return false;
    };

    const resetIndicator = () => {
      indicator.style.transition = "transform .25s ease, opacity .25s ease";
      indicator.style.transform = "translateX(-50%) scaleY(0)";
      indicator.style.opacity = "0";
      indicator.classList.remove("ready");
      setTimeout(() => { indicator.style.transition = ""; }, 280);
    };

    const onTouchStart = (e) => {
      if (frame.scrollTop > 2) return;
      if (isInsideHorizontalScroller(e.target)) return;
      startY = e.touches[0].clientY;
      startX = e.touches[0].clientX;
      dragging = true;
      dy = 0;
    };
    const onTouchMove = (e) => {
      if (!dragging || startY == null) return;
      const cy = e.touches[0].clientY;
      const cx = e.touches[0].clientX;
      const dx = Math.abs(cx - startX);
      const raw = cy - startY;
      // Cancel pull if horizontal travel exceeded tolerance — user is scrolling sideways.
      if (dx > H_TOLERANCE && raw < dx) {
        dragging = false; startY = null; dy = 0;
        indicator.style.transform = "translateX(-50%) scaleY(0)";
        indicator.style.opacity = "0";
        indicator.classList.remove("ready");
        return;
      }
      if (raw <= 0) { dy = 0; indicator.style.transform = "translateX(-50%) scaleY(0)"; indicator.style.opacity = "0"; return; }
      dy = Math.min(MAX, raw * 0.55);
      const ratio = Math.min(1, dy / TRIGGER);
      indicator.style.transform = `translateX(-50%) scaleY(${ratio})`;
      indicator.style.opacity = String(ratio);
      indicator.classList.toggle("ready", dy >= TRIGGER);
    };
    const onTouchEnd = async () => {
      if (!dragging) { resetIndicator(); return; }
      const fired = dy >= TRIGGER;
      dragging = false;
      startY = null;
      dy = 0;
      resetIndicator();
      if (!fired) return;
      if (window._haptic) window._haptic("tap");
      let okCount = 0, errCount = 0;
      try { if (A && A.fetchRates) { await A.fetchRates(); okCount++; } } catch (e) { console.warn("[ptr] fetchRates:", e); errCount++; }
      try { if (A && A.pullSync)   { await A.pullSync();   okCount++; } } catch (e) { console.warn("[ptr] pullSync:", e); errCount++; }
      if (A && A.showToast) {
        if (errCount > 0 && okCount === 0) A.showToast("עדכון נכשל", "error");
        else A.showToast("עודכן", "info");
      }
    };
    frame.addEventListener("touchstart", onTouchStart, { passive: true });
    frame.addEventListener("touchmove", onTouchMove, { passive: true });
    frame.addEventListener("touchend", onTouchEnd, { passive: true });
    frame.addEventListener("touchcancel", onTouchEnd, { passive: true });
    return () => {
      // Reset all drag state on cleanup so a re-mounted indicator doesn't
      // inherit a stuck "ready" state from an in-flight drag.
      dragging = false; startY = null; startX = null; dy = 0;
      frame.removeEventListener("touchstart", onTouchStart);
      frame.removeEventListener("touchmove", onTouchMove);
      frame.removeEventListener("touchend", onTouchEnd);
      frame.removeEventListener("touchcancel", onTouchEnd);
      try { frame.removeChild(indicator); } catch (e) {}
    };
    // No real deps — `A`, `frameRef` are stable; rebinding on currentUserId
    // change was thrashing the listeners.
  }, []);

  // Expose programmatic back navigation so non-React code (store actions)
  // can pop the route after destructive mutations (e.g. deleteMemory while
  // the user is on the memory detail screen) — was causing "stuck on the
  // deleted item" feel because the route stack still pointed at memory.
  React.useEffect(() => {
    window._maNavBack = () => setStack(s => (s.length > 1 ? s.slice(0, -1) : s));
    window._maNavTo = (id) => { if (ROUTES[id]) setStack([id]); };
    return () => {
      if (window._maNavBack) delete window._maNavBack;
      if (window._maNavTo)   delete window._maNavTo;
    };
  }, []);

  // ─── Swipe-back gesture (iOS-native pattern) ────────────────────
  // Left-edge touch + drag right within 500ms = pop stack. Disabled when
  // a sheet is open or only one screen on the stack. Honors horizontal
  // scrollers (day picker, spots row) by ignoring touches that started
  // inside them — matches the PTR effect's behavior.
  React.useEffect(() => {
    const frame = frameRef.current;
    if (!frame) return;
    let sx = 0, sy = 0, st = 0, armed = false;
    const isInsideHScroll = (target) => {
      let el = target;
      while (el && el !== frame) {
        if (el.scrollWidth > el.clientWidth + 4) return true;
        el = el.parentElement;
      }
      return false;
    };
    const sheetOpen = () => {
      const s = window.Store && window.Store.state;
      return !!(s && (s.actionSheet || s.editSheet || s.confirmSheet || s.spotMapPicker));
    };
    const onStart = (e) => {
      if (sheetOpen()) { armed = false; return; }
      const t = e.touches[0];
      sx = t.clientX; sy = t.clientY; st = Date.now();
      // Edge-arm: only fire on touches that started within 28px of the
      // left edge so day-strip / horizontal lists keep their swipe.
      armed = sx < 28 && !isInsideHScroll(e.target);
    };
    const onEnd = (e) => {
      if (!armed) return;
      armed = false;
      const t = e.changedTouches[0];
      const dx = t.clientX - sx;
      const dy = Math.abs(t.clientY - sy);
      const dt = Date.now() - st;
      if (dx > 80 && dy < 60 && dt < 500) {
        setStack(s => (s.length > 1 ? s.slice(0, -1) : s));
        if (window._haptic) window._haptic("tap");
      }
    };
    frame.addEventListener("touchstart", onStart, { passive: true });
    frame.addEventListener("touchend", onEnd, { passive: true });
    frame.addEventListener("touchcancel", () => { armed = false; }, { passive: true });
    return () => {
      frame.removeEventListener("touchstart", onStart);
      frame.removeEventListener("touchend", onEnd);
    };
  }, []);

  function onClick(e) {
    const el = e.target.closest("[data-nav]");
    if (!el) return;
    const id = el.getAttribute("data-nav");
    const replace = el.getAttribute("data-nav-replace") === "1";

    if (id === "back") {
      e.preventDefault();
      if (window._haptic) window._haptic("tap");
      setStack(s => {
        if (s.length > 1) return s.slice(0, -1);
        if (s[0] !== "home") return ["home"];
        return s;
      });
      return;
    }
    if (!ROUTES[id]) return;
    e.preventDefault();
    if (window._haptic) window._haptic("tap");
    if (replace) {
      if (current !== id) setStack([id]);
      // Tapping the active tab scrolls the current screen back to the top —
      // iOS-native pattern, helps when the user is deep in a long list.
      else if (frameRef.current && frameRef.current.scrollTop > 0) {
        frameRef.current.scrollTo({ top: 0, behavior: "smooth" });
      }
    } else if (current !== id) {
      setStack(s => [...s, id]);
    }
  }

  // Keyboard activation for non-button [data-nav] elements.
  // Enter/Space triggers the same click flow above.
  function onKeyDown(e) {
    if (e.key !== "Enter" && e.key !== " " && e.key !== "Spacebar") return;
    const t = e.target;
    const tag = t.tagName;
    if (tag === "BUTTON" || tag === "A" || tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return;
    const el = t.closest("[data-nav]");
    if (!el) return;
    e.preventDefault();
    el.click();
  }

  const Render = ROUTES[current] || HomeScreen;
  const tabCfg = TAB_BY_ROUTE[current];
  const TabBar = window.TabBar;
  const seenOnboarding = useStore(s => s.seenOnboarding);

  if (!currentUserId && window.AuthScreen) {
    return (
      <div className="ma-app">
        <div className="ma-frame" ref={frameRef} style={{ position: "relative" }}>
          <window.AuthScreen />
        </div>
        <EditSheet />
        <ConfirmSheet />
        <ActionSheet />
        <Toast />
      </div>
    );
  }

  // First-time experience — show OnboardingScreen until user dismisses or completes
  // (it stores seenOnboarding=true on either Skip or "let's start" CTA).
  if (!seenOnboarding && window.OnboardingScreen) {
    return (
      <div className="ma-app">
        <div className="ma-frame" ref={frameRef} style={{ position: "relative" }}>
          <window.OnboardingScreen />
        </div>
        <EditSheet />
        <ConfirmSheet />
        <ActionSheet />
        <Toast />
      </div>
    );
  }

  return (
    <div className="ma-app">
      <div className="ma-frame" ref={frameRef} onClick={onClick} onKeyDown={onKeyDown} style={{ position: "relative" }}>
        <div style={{ display: "flex", flexDirection: "column", minHeight: "100%" }}>
          <div
            key={current}
            ref={screenWrapRef}
            tabIndex={-1}
            aria-label={SCREEN_TITLES[current] || undefined}
            className="ma-screen-anim"
            data-dir={navDir}
            style={{ flex: 1, minHeight: 0, outline: "none" }}
          >
            <Render />
          </div>
        </div>
      </div>
      {/* TabBar is HOISTED outside .ma-frame so the screen-transition animation
          (transform + filter on .ma-screen-anim) can't drag it into a temporary
          compositor layer on iOS Safari. Stays viewport-fixed and rock-stable
          across all home↔plan↔mem↔budget swaps. */}
      {tabCfg && TabBar && (
        <div onClick={onClick} onKeyDown={onKeyDown} style={{
          position: "fixed", bottom: 0, left: 0, right: 0, zIndex: 100,
          pointerEvents: "none",
        }}>
          <div style={{ pointerEvents: "auto", display: "flex", justifyContent: "center" }}>
            <TabBar active={tabCfg.active} dark={tabCfg.dark} />
          </div>
        </div>
      )}
      <ActionSheet />
      <EditSheet />
      <ConfirmSheet />
      <Toast />
      <SpotMapPickerHost />
    </div>
  );
}

function SpotMapPickerHost() {
  const picker = useStore(s => s.spotMapPicker);
  const LocationPicker = window.LocationPicker;
  if (!picker || !LocationPicker) return null;
  return (
    <LocationPicker
      initial={null}
      onSave={async ({ lat, lng, name }) => {
        const dests = (window.Store.state.trip.destinations || []).map(d => window.destName(d));
        const cats = (window.SPOT_CATEGORIES || []).map(c => c.label);
        A.openEditSheet({
          title: "פרטי המקום",
          submitLabel: "שמרי",
          fields: [
            { key: "title", label: "שם המקום", type: "text", value: name || "", required: true, autoFocus: true },
            { key: "cat",   label: "קטגוריה",  type: "select", options: cats, value: cats[0] || "אחר" },
            { key: "dest",  label: "יעד",       type: "select", options: ["—", ...dests], value: dests[0] || "—" },
          ],
          onSubmit: (v) => {
            const meta = (window.SPOT_CATEGORIES || []).find(c => c.label === v.cat) || (window.SPOT_CATEGORIES || [])[7];
            A.addSpotEntry({
              title: v.title.trim(), sub: "", lat, lng, address: name || "",
              cat: meta ? meta.key : "other",
              dest: v.dest && v.dest !== "—" ? v.dest : null,
              source: "map", link: null,
            });
          },
        });
      }}
      onClose={() => A.closeSpotMapPicker()}
    />
  );
}

class ErrorBoundary extends React.Component {
  constructor(p) { super(p); this.state = { err: null, info: null }; }
  static getDerivedStateFromError(err) { return { err }; }
  componentDidCatch(err, info) { this.setState({ err, info }); console.error("MA-Trip render error:", err, info); }
  render() {
    if (this.state.err) {
      return (
        <div dir="ltr" style={{
          padding: 20, fontFamily: "monospace", fontSize: 12,
          color: "#fff", background: "#1a1a1c", minHeight: "100vh",
          whiteSpace: "pre-wrap", overflow: "auto", wordBreak: "break-word",
        }}>
          <div style={{ fontSize: 16, fontWeight: 700, color: "#ff385c", marginBottom: 12 }}>
            Render error
          </div>
          <div style={{ marginBottom: 12 }}>{String(this.state.err && this.state.err.message || this.state.err)}</div>
          <div style={{ opacity: .7, fontSize: 11 }}>{String(this.state.err && this.state.err.stack || "")}</div>
          {this.state.info && this.state.info.componentStack && (
            <div style={{ marginTop: 16, opacity: .6, fontSize: 11 }}>{this.state.info.componentStack}</div>
          )}
        </div>
      );
    }
    return this.props.children;
  }
}

window.addEventListener("error", (e) => {
  const el = document.getElementById("root");
  if (!el || el.firstChild) return;
  // Build with createElement + textContent — never innerHTML with attacker-
  // influenced values (e.message can come from poisoned cross-origin scripts).
  const wrap = document.createElement("div");
  wrap.style.cssText = "padding:20px;color:#fff;background:#1a1a1c;font-family:monospace;font-size:12px;min-height:100vh;white-space:pre-wrap;";
  const title = document.createElement("div");
  title.style.cssText = "color:#ff385c;font-size:16px;font-weight:700;margin-bottom:12px";
  title.textContent = "JS load error";
  const body = document.createElement("div");
  body.textContent = (e.message || "") + "\n\n" + (e.filename || "") + ":" + (e.lineno || "");
  wrap.appendChild(title);
  wrap.appendChild(body);
  el.appendChild(wrap);
});

// Init guard: validate every global that the App tree dereferences at boot.
// Any miss = a `<script>` upstream failed to register, almost always a
// load-order regression. Bailing here with a friendly screen beats a
// confusing React render crash inside Babel-transpiled code.
function _maTripValidateGlobals() {
  const required = [
    "React", "ReactDOM", "Store", "useStore", "A",
    "HomeScreen", "ItineraryScreen", "MemoriesScreen", "MapScreen",
    "RemindersScreen", "BudgetScreen", "DocumentsScreen", "PackingScreen",
    "MaMedia", "MaCrypto", "MaApi",
  ];
  return required.filter(k => typeof window[k] === "undefined");
}
const _maTripMissing = _maTripValidateGlobals();
if (_maTripMissing.length) {
  const el = document.getElementById("root");
  if (el && !el.firstChild) {
    const wrap = document.createElement("div");
    wrap.style.cssText = "padding:20px;color:#fff;background:#1a1a1c;font-family:monospace;font-size:12px;min-height:100vh;white-space:pre-wrap;";
    const title = document.createElement("div");
    title.style.cssText = "color:#ff385c;font-size:16px;font-weight:700;margin-bottom:12px";
    title.textContent = "Init guard: missing globals";
    const body = document.createElement("div");
    body.textContent = "Failed to register: " + _maTripMissing.join(", ") +
      "\n\nUsually means an earlier <script> threw. Open DevTools console for the first error.";
    wrap.appendChild(title);
    wrap.appendChild(body);
    el.appendChild(wrap);
  }
  console.error("[MA-Trip] init guard — missing globals:", _maTripMissing);
} else {
  ReactDOM.createRoot(document.getElementById("root")).render(<ErrorBoundary><App /></ErrorBoundary>);
}
