// Main editor app

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

const LAYOUTS = [
  { id: "fill",       label: "Fill (Big Bold)" },   // brand-defining default
  { id: "stacked",    label: "Stacked" },
  { id: "centered",   label: "Centered" },
  { id: "block",      label: "Block" },
  { id: "rule",       label: "Rule" },
  { id: "asterisk",   label: "Asterisk" },
  { id: "bracket",    label: "Field Notes" },
  { id: "marquee",    label: "Marquee" },
  { id: "left",       label: "Numbered" },
  { id: "stamp",      label: "Stamp" },
  { id: "ticket",     label: "Boarding Pass" },
];

const FONTS = [
  { id: "Special Elite", label: "Special Elite" },
  { id: "Courier Prime", label: "Courier Prime" },
  { id: "JetBrains Mono", label: "JetBrains Mono" },
  { id: "IBM Plex Mono", label: "IBM Plex Mono" },
];

// Launch palette + per-blank color whitelists live in blanks.jsx
// We build TEE_COLORS dynamically from window.LAUNCH_COLORS + window.BLANK_COLORS

const CATEGORIES = [
  { id: "all",       label: "All" },
  { id: "genx",      label: "Gen X" },
  { id: "translate", label: "Translator" },
  { id: "versus",    label: "Vs." },
  { id: "vsteen",    label: "Teen vs Parent" },
  { id: "genxsport", label: "Gen X Sports" },
  { id: "rizz",      label: "Rizz" },
  { id: "cap",       label: "No Cap" },
  { id: "cooked",    label: "Cooked" },
  { id: "mom",       label: "Mom" },
  { id: "dad",       label: "Dad" },
  { id: "era",       label: "Eras" },
  { id: "broke",     label: "Broke" },
  { id: "ump",       label: "Ump" },
  { id: "vibe",      label: "Vibes" },
  { id: "cringe",    label: "Cringe" },
  { id: "skibidi",   label: "Skibidi" },
  { id: "tired",     label: "Tired" },
  { id: "kid",       label: "Kid" },
  { id: "wins",      label: "W/L" },
  { id: "delulu",    label: "Delulu" },
  { id: "loadout",   label: "Loadout" },
  { id: "drama",     label: "Drama" },
  { id: "schedule",  label: "Bracket" },
  { id: "sport",     label: "Sport" },
];

function App() {
  const [text, setText] = useState("NO CAP / I PAID THE / TOURNAMENT FEE");
  // Default layout: "fill" for customers (matches the storefront brand voice
  // — words fill the canvas, one per line). Admin keeps "stacked" for legacy.
  const isCustomer = typeof window !== "undefined" &&
    new URLSearchParams(window.location.search).get("customer") === "1";
  const [layout, setLayout] = useState(isCustomer ? "fill" : "stacked");
  const [font, setFont] = useState("Special Elite");
  const [fontSize, setFontSize] = useState(216);
  const [letterSpacing, setLetterSpacing] = useState(2);
  const [lineHeight, setLineHeight] = useState(1.05);
  const [inkColor, setInkColor] = useState("#111111");
  const [rotation, setRotation] = useState(0);
  const [distress, setDistress] = useState(0.0);
  const [offsetX, setOffsetX] = useState(0);
  const [offsetY, setOffsetY] = useState(0);
  const [uploadedImage, setUploadedImage] = useState(null);   // data URL or null
  const [uploadedImageDims, setUploadedImageDims] = useState(null); // {w, h} natural dimensions
  const [imageScale, setImageScale] = useState(0.6);          // 0..1
  const [imagePosition, setImagePosition] = useState("above");// "above" | "below" | "only"
  const [shirtSize, setShirtSize] = useState("M");

  // Customer mode (?customer=1) hides the admin push button and shows ADD TO CART
  const customerMode = useMemo(() =>
    new URLSearchParams(window.location.search).get("customer") === "1",
  []);
  // Wizard step (1..5) — only used in customer mode
  const [wizardStep, setWizardStep] = useState(1);
  // Did the customer just add this design to cart? Drives the CHECKOUT NOW button.
  const [cartAdded, setCartAdded] = useState(false);
  // Active category for the 🎲 Random saying button — "all" or a key from PHRASE_LIBRARY
  const [randomCategory, setRandomCategory] = useState("all");
  // Add a body class when in customer mode so CSS can target the simplified mobile layout
  useEffect(() => {
    if (customerMode) document.body.classList.add("customer-mode");
    return () => document.body.classList.remove("customer-mode");
  }, [customerMode]);
  useEffect(() => {
    document.body.classList.remove("step-1","step-2","step-3","step-4","step-5");
    if (customerMode) document.body.classList.add("step-" + wizardStep);
  }, [customerMode, wizardStep]);
  const [teeColorKey, setTeeColorKey] = useState("white");
  const [blank, setBlank] = useState("tee3001");
  const teeColor = useMemo(() => {
    return (window.LAUNCH_COLORS || {})[teeColorKey]?.hex || "#0e0e0e";
  }, [teeColorKey]);
  const [category, setCategory] = useState("all");
  const [exporting, setExporting] = useState(null);
  const [toast, setToast] = useState(null);
  const [availableMockups, setAvailableMockups] = useState(null); // Set of "blank:colorKey"
  const [photoMode, setPhotoMode] = useState("auto"); // "auto" | "off"

  // Probe for real photo mockups on mount
  useEffect(() => {
    let cancelled = false;
    (async () => {
      if (typeof window.detectAvailableMockups !== "function") return;
      const found = await window.detectAvailableMockups();
      if (!cancelled) setAvailableMockups(found);
    })();
    return () => { cancelled = true; };
  }, []);

  // Listen for ACK from the Shopify parent page on add-to-cart
  useEffect(() => {
    if (!customerMode) return;
    const onMsg = (e) => {
      const d = e.data;
      if (!d || d.type !== "nocap-cart-result") return;
      if (d.ok) {
        showToast("✓ Added to cart · " + (d.size || "") + " " + (d.color || ""));
        setCartAdded(true);
      } else {
        showToast("Cart failed: " + (d.error || "unknown error"));
      }
    };
    window.addEventListener("message", onMsg);
    return () => window.removeEventListener("message", onMsg);
  }, [customerMode]);

  // Auto-pick a default color key when blank changes (if current key isn't valid for this blank)
  useEffect(() => {
    const allowed = (window.BLANK_COLORS || {})[blank] || [];
    if (!allowed.includes(teeColorKey)) {
      const def = (window.BLANK_DEFAULTS || {})[blank];
      if (def) setTeeColorKey(def);
      else if (allowed[0]) setTeeColorKey(allowed[0]);
    }
  }, [blank]);

  // Auto-pick ink color based on tee for visibility
  useEffect(() => {
    const isDark = (hex) => {
      const h = hex.replace("#", "");
      const r = parseInt(h.slice(0, 2), 16);
      const g = parseInt(h.slice(2, 4), 16);
      const b = parseInt(h.slice(4, 6), 16);
      return (r * 0.299 + g * 0.587 + b * 0.114) < 130;
    };
    const dark = isDark(teeColor);
    if (dark && (inkColor === "#111111" || inkColor === "#1a1a1a")) setInkColor("#f5f1e8");
    if (!dark && (inkColor === "#f5f1e8" || inkColor === "#ffffff")) setInkColor("#111111");
  }, [teeColor]);

  const phrases = useMemo(() => {
    const lib = window.PHRASE_LIBRARY || {};
    if (category === "all") {
      return Object.values(lib).flat();
    }
    return lib[category] || [];
  }, [category]);

  const showToast = (msg) => {
    setToast(msg);
    setTimeout(() => setToast(null), 2400);
  };

  const handleExportDesign = async () => {
    setExporting("design");
    try {
      await window.exportDesignPNG({
        text, layout, font, fontSize, letterSpacing, lineHeight, inkColor, rotation, distress, offsetX, offsetY, uploadedImage, imageScale, imagePosition,
        width: 4500, height: 5400,
        filename: makeFilename(text) + "_design_4500x5400.png",
      });
      showToast("Design PNG saved · 4500×5400 · transparent");
    } catch (e) {
      console.error(e);
      showToast("Export failed: " + e.message);
    }
    setExporting(null);
  };

  const handleExportMockup = async () => {
    setExporting("mockup");
    try {
      const designBlob = await window.exportDesignPNG({
        text, layout, font, fontSize, letterSpacing, lineHeight, inkColor, rotation, distress, offsetX, offsetY, uploadedImage, imageScale, imagePosition,
        width: 1500, height: 1800,
        download: false,
      });
      const usePhoto = photoMode !== "off" && availableMockups?.has(blank + ":" + teeColorKey);
      await window.exportMockupPNG({
        designBlob, teeColor,
        photoUrl: usePhoto ? window.MOCKUP_MANIFEST[blank].variants[teeColorKey] : null,
        printRect: usePhoto ? window.getPrintRect(blank, teeColorKey) : null,
        blank,
        width: 2000, height: 2000,
        filename: makeFilename(text) + "_mockup_" + (window.MOCKUP_MANIFEST[blank]?.short || blank) + "_" + colorName(teeColor) + ".png",
      });
      showToast(usePhoto ? "Photo mockup saved · 2000×2000" : "Illustrated mockup saved · 2000×2000");
    } catch (e) {
      console.error(e);
      showToast("Export failed: " + e.message);
    }
    setExporting(null);
  };

  const handleAddToCart = async () => {
    if (!["tee3001", "cc1717", "tank3480", "cc9360", "perf350"].includes(blank)) {
      showToast("Pick a shirt type first"); return;
    }
    setExporting("cart");
    try {
      // Render the design at full print resolution. With Vercel Blob in the
      // upload path, the browser PUTs the bytes directly to blob storage so
      // we no longer hit the 4.5 MB serverless body cap. Fall back to the
      // adaptive base64 ladder if Blob isn't reachable.
      let blob = await window.exportDesignPNG({
        text, layout, font, fontSize, letterSpacing, lineHeight, inkColor,
        rotation, distress, offsetX, offsetY,
        uploadedImage, imageScale, imagePosition,
        width: 4500, height: 5400, download: false,
      });
      const blobToB64 = (b) => new Promise((res, rej) => {
        const r = new FileReader();
        r.onload = () => res(String(r.result).split(",")[1] || "");
        r.onerror = rej; r.readAsDataURL(b);
      });

      // Render the click-to-preview mockup at print quality.
      let mockupBlob = null;
      try {
        mockupBlob = await window.exportMockupPNG({
          designBlob: blob, teeColor,
          blank, width: 1800, height: 1800, download: false,
          format: "jpeg", quality: 0.9,
        });
      } catch (_) { /* fall back to design-only thumbnail */ }

      // Try Vercel Blob direct upload (no 4.5 MB cap). Fall back to inline
      // base64 if Blob isn't configured / reachable.
      let postBody = null;
      try {
        const blobClient = await import("https://esm.sh/@vercel/blob@0.27.3/client");
        const stamp = Date.now() + "_" + Math.random().toString(36).slice(2, 8);
        const designResult = await blobClient.upload(`design_${stamp}.png`, blob, {
          access: "public", handleUploadUrl: "/api/blob-upload",
        });
        let mockupUrl = null;
        if (mockupBlob) {
          const mockupResult = await blobClient.upload(`mockup_${stamp}.jpg`, mockupBlob, {
            access: "public", handleUploadUrl: "/api/blob-upload",
          });
          mockupUrl = mockupResult.url;
        }
        postBody = { design_url: designResult.url, mockup_url: mockupUrl };
      } catch (blobErr) {
        console.warn("[cart] blob upload unavailable, falling back to base64:", blobErr);
        // Fallback: shrink to fit Vercel's 4.5 MB body cap.
        if (blob.size > 2.8 * 1024 * 1024) {
          blob = await window.exportDesignPNG({
            text, layout, font, fontSize, letterSpacing, lineHeight, inkColor,
            rotation, distress, offsetX, offsetY,
            uploadedImage, imageScale, imagePosition,
            width: 2400, height: 2880, download: false,
          });
        }
        const png_b64 = await blobToB64(blob);
        const mockup_b64 = mockupBlob ? await blobToB64(mockupBlob) : "";
        postBody = { png_b64, mockup_b64 };
      }

      // Server-side: fetches blob URLs (or decodes base64), uploads to
      // Printify, returns small image_ids that we put on the cart line item.
      let upload;
      try {
        const r = await fetch("/api/save-design", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(postBody),
        });
        if (!r.ok) {
          const txt = await r.text();
          showToast("Upload failed: HTTP " + r.status + " " + txt.slice(0, 80));
          setExporting(null); return;
        }
        upload = await r.json();
      } catch (e) {
        showToast("Upload error: " + e.message);
        setExporting(null); return;
      }
      if (!upload.ok) {
        showToast("Upload failed: " + (upload.error || "")); setExporting(null); return;
      }
      // Send the order details to the parent Shopify page (which adds to cart).
      const blankLabels = {
        tee3001: "Bella+Canvas 3001 Classic Tee",
        cc1717: "Comfort Colors 1717 Vintage Tee",
        tank3480: "Bella+Canvas 3480 Tank",
        cc9360: "Comfort Colors 9360 Vintage Tank",
        perf350: "Sport-Tek ST350 Performance Tee",
      };
      const colorLabel = (window.LAUNCH_COLORS || {})[teeColorKey]?.label || teeColorKey;
      const payload = {
        type: "nocap-add-to-cart",
        title: text.split(/\s*\/\s*|\n/).filter(Boolean).join(" · "),
        blank, blank_label: blankLabels[blank] || blank,
        color: colorLabel, size: shirtSize,
        ink: inkColor,
        layout, font, fontSize, letterSpacing, lineHeight,
        rotation, distress, offsetX, offsetY,
        imagePosition: uploadedImage ? imagePosition : null,
        printify_image_dark:  upload.dark_id,
        printify_image_light: upload.light_id,
        printify_image_original: upload.original_id,
        preview_url: upload.preview_url || null,
      };
      // postMessage works when this generator is iframed by the Shopify page.
      // The PARENT will postMessage back with type "nocap-cart-result" once the
      // cart add succeeds or fails; we show a toast based on that ACK.
      try { window.parent.postMessage(payload, "*"); } catch (_) {}
      showToast("Sending to cart…");
    } catch (e) {
      console.error(e);
      showToast("Add to cart failed: " + e.message);
    }
    setExporting(null);
  };

  const handlePushToPrintify = async () => {
    if (!["tee3001", "cc1717", "tank3480", "cc9360", "perf350"].includes(blank)) {
      showToast("PUSH TO PRINTIFY supports the blanks in the picker");
      return;
    }
    setExporting("push");
    try {
      // Render at print resolution, transparent
      const blob = await window.exportDesignPNG({
        text, layout, font, fontSize, letterSpacing, lineHeight, inkColor, rotation, distress, offsetX, offsetY, uploadedImage, imageScale, imagePosition,
        width: 4500, height: 5400,
        download: false,
      });
      // Convert blob → base64
      const png_b64 = await new Promise((res, rej) => {
        const r = new FileReader();
        r.onload = () => res(String(r.result).split(",")[1] || "");
        r.onerror = rej;
        r.readAsDataURL(blob);
      });

      // Derive a title from the phrase
      const title = text
        .split(/\s*\/\s*|\n/).map(s => s.trim()).filter(Boolean)
        .map(s => s.split(" ").map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join(" "))
        .join(" · ");

      let data;
      try {
        const res = await fetch("/api/push-to-printify", {
          method: "POST",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ png_b64, title, blank, phrase: text }),
        });
        data = await res.json();
      } catch (fetchErr) {
        // If fetch threw (timeout, network), still show a toast so the user knows
        showToast("Push request failed — check Printify in a minute (it may have completed)");
        setExporting(null);
        return;
      }
      if (!data.ok) {
        showToast("Push failed: " + (data.error || res.status));
        console.error(data);
      } else {
        const prices = (data.prices || []).map(p => "$" + p.toFixed(2)).join("/");
        const statusStr = (data.status || []).join(", ");
        showToast("Pushed · " + prices + " · " + statusStr);
        console.log("Printify product page:", data.printify_url);
        // No auto-open — too intrusive when pushing many designs in a row.
        // Open it manually from the console log if you want to inspect.

        // If the publish step didn't finish (mockups still rendering or queue busy),
        // schedule a background retry so the product lands on Shopify automatically.
        const pending = (data.status || []).some(s =>
          s.includes("rendering") || s.includes("publish-pending"));
        if (pending) {
          showToast("Mockups rendering — retrying publish in 2 min");
          setTimeout(async () => {
            try {
              const r = await fetch("/api/publish-pending");
              const d = await r.json();
              const n = d.totals?.published || 0;
              if (n > 0) showToast("Auto-published " + n + " pending product(s)");
            } catch (_) {}
          }, 120000);  // 2 min — typical mockup-generation window
        }
      }
    } catch (e) {
      console.error(e);
      showToast("Push failed: " + e.message);
    }
    setExporting(null);
  };

  const handleExportBoth = async () => {
    setExporting("both");
    try {
      const designBlob = await window.exportDesignPNG({
        text, layout, font, fontSize, letterSpacing, lineHeight, inkColor, rotation, distress, offsetX, offsetY, uploadedImage, imageScale, imagePosition,
        width: 4500, height: 5400,
        filename: makeFilename(text) + "_design_4500x5400.png",
      });
      const usePhoto2 = photoMode !== "off" && availableMockups?.has(blank + ":" + teeColorKey);
      await window.exportMockupPNG({
        designBlob, teeColor,
        photoUrl: usePhoto2 ? window.MOCKUP_MANIFEST[blank].variants[teeColorKey] : null,
        printRect: usePhoto2 ? window.getPrintRect(blank, teeColorKey) : null,
        blank,
        width: 2000, height: 2000,
        filename: makeFilename(text) + "_mockup_" + (window.MOCKUP_MANIFEST[blank]?.short || blank) + "_" + colorName(teeColor) + ".png",
      });
      showToast("Both PNGs saved · ready for Printify");
    } catch (e) {
      console.error(e);
      showToast("Export failed: " + e.message);
    }
    setExporting(null);
  };

  // Preview canvas — measure actual card width so the print preview fills its container
  const printWrapRef = useRef(null);
  const [previewSize, setPreviewSize] = useState(240);
  useEffect(() => {
    if (!printWrapRef.current) return;
    const ro = new ResizeObserver((entries) => {
      const r = entries[0].contentRect;
      // Constrain by BOTH width and height (5:6 aspect), with hard cap
      const maxByW = r.width - 40;
      const maxByH = (r.height - 40) * (4500 / 5400);
      const size = Math.min(maxByW, maxByH);
      setPreviewSize(Math.min(520, Math.max(200, size)));
    });
    ro.observe(printWrapRef.current);
    return () => ro.disconnect();
  }, []);
  const previewScale = previewSize / 1500;

  // ───── Customer wizard renders a totally different UI than admin ─────
  if (customerMode) {
    return (
      <CustomerWizard
        text={text} setText={setText}
        blank={blank} setBlank={setBlank}
        teeColor={teeColor} teeColorKey={teeColorKey} setTeeColorKey={setTeeColorKey}
        inkColor={inkColor} setInkColor={setInkColor}
        layout={layout} setLayout={setLayout}
        font={font} setFont={setFont}
        fontSize={fontSize} setFontSize={setFontSize}
        letterSpacing={letterSpacing} setLetterSpacing={setLetterSpacing}
        lineHeight={lineHeight} setLineHeight={setLineHeight}
        rotation={rotation} setRotation={setRotation}
        distress={distress} setDistress={setDistress}
        offsetX={offsetX} setOffsetX={setOffsetX}
        offsetY={offsetY} setOffsetY={setOffsetY}
        uploadedImage={uploadedImage} setUploadedImage={(v) => { setUploadedImage(v); if (!v) setUploadedImageDims(null); }}
        uploadedImageDims={uploadedImageDims} setUploadedImageDims={setUploadedImageDims}
        showToast={showToast}
        imageScale={imageScale} setImageScale={setImageScale}
        imagePosition={imagePosition} setImagePosition={setImagePosition}
        shirtSize={shirtSize} setShirtSize={setShirtSize}
        wizardStep={wizardStep} setWizardStep={setWizardStep}
        randomCategory={randomCategory} setRandomCategory={setRandomCategory}
        exporting={exporting}
        handleAddToCart={handleAddToCart}
        cartAdded={cartAdded}
        onCheckoutNow={() => {
          // Tell the parent Shopify page to navigate to checkout.
          try { window.parent.postMessage({ type: "nocap-checkout" }, "*"); } catch (_) {}
        }}
        onDesignAnother={() => {
          setCartAdded(false);
          setWizardStep(1);
          setText("");
          setUploadedImage(null);
        }}
        toast={toast}
      />
    );
  }

  return (
    <div className="app">
      {/* HEADER */}
      <header className="topbar">
        <div className="brand">
          <div className="brand-mark" aria-hidden="true"><span></span><span></span></div>
          <div className="brand-meta">
            <div className="brand-name">NO CAP APPAREL</div>
            <div className="brand-tag">gen x sarcasm · gen a slang · travel sports ready</div>
          </div>
        </div>
        <div className="topbar-meta">
          <PhotoStatus available={availableMockups} />
          <span className="meta-cell">4500 × 5400 · 300 DPI</span>
          <button
            className={`meta-toggle ${photoMode === "off" ? "off" : ""}`}
            onClick={() => setPhotoMode(photoMode === "off" ? "auto" : "off")}
            title="Force illustrated mockups even when photos exist"
          >
            PHOTO MODE: {photoMode === "off" ? "OFF" : "AUTO"}
          </button>
        </div>
      </header>

      {customerMode && (
        <div className="wizard-bar">
          <div className="wizard-steps">
            {["STYLE","COLOR","DESIGN","TWEAK","CHECKOUT"].map((label, i) => (
              <button
                key={i}
                type="button"
                onClick={() => setWizardStep(i + 1)}
                className={`wizard-dot ${wizardStep === i + 1 ? "active" : ""} ${wizardStep > i + 1 ? "done" : ""}`}
                aria-label={`Step ${i + 1}: ${label}`}
              >
                <span className="wizard-dot-num">{i + 1}</span>
                <span className="wizard-dot-label">{label}</span>
              </button>
            ))}
          </div>
        </div>
      )}

      <div className="workspace">
        {/* LEFT — phrase library */}
        <aside className="panel panel-left">
          <div className="panel-head">
            <span className="panel-title">PHRASE LIBRARY</span>
            <span className="panel-count">{phrases.length}</span>
          </div>

          <div className="cat-tabs">
            {CATEGORIES.map(c => (
              <button
                key={c.id}
                className={`cat-tab ${category === c.id ? "active" : ""}`}
                onClick={() => setCategory(c.id)}
              >
                {c.label}
              </button>
            ))}
          </div>

          <div className="phrase-list">
            {phrases.map((p, i) => (
              <button
                key={i}
                className={`phrase-item ${text === p ? "active" : ""}`}
                onClick={() => setText(p)}
                title="Click to use"
              >
                <span className="phrase-num">{String(i + 1).padStart(2, "0")}</span>
                <span className="phrase-text">{p}</span>
              </button>
            ))}
          </div>

          <div className="custom-input" data-wstep="3">
            <label className="input-label">CUSTOM TEXT — use " / " for line breaks</label>
            <textarea
              value={text}
              onChange={(e) => setText(e.target.value)}
              rows={3}
              className="text-input"
            />
            <button
              type="button"
              onClick={() => setText(generateRandomSaying())}
              style={{
                marginTop: 10, width: "100%", padding: "8px 12px", fontSize: 11,
                letterSpacing: 1.5, fontFamily: "Special Elite, monospace",
                textTransform: "uppercase", background: "var(--accent)",
                color: "var(--paper)", border: "1px solid var(--accent)",
                cursor: "pointer",
              }}
            >🎲 RANDOM SAYING</button>
          </div>
        </aside>

        {/* CENTER — preview */}
        <main className="stage">
          <div className="stage-tabs">
            <span className="stage-tab active">PREVIEW</span>
            <span className="stage-spacer" />
            <span className="stage-meta">{layout.toUpperCase()} · {font.toUpperCase()}</span>
          </div>

          <div className="stage-grid">
            {/* Mockup view */}
            <div className="preview-card">
              <div className="preview-card-head">
                <span className="card-label">{(window.BLANKS_META || []).find(b => b.id === blank)?.label || "ON SHIRT"}</span>
                <span className="card-meta">{colorName(teeColor).toUpperCase()}</span>
              </div>
              <div className="blank-row" data-wstep="1">
                <div className="blank-row-label">SHIRT TYPE →</div>
                {(window.BLANKS_META || []).map(b => (
                  <button
                    key={b.id}
                    className={`blank-pill ${blank === b.id ? "active" : ""}`}
                    onClick={() => setBlank(b.id)}
                    title={b.sub}
                  >
                    <span className="blank-pill-label">{b.label}</span>
                    <span className="blank-pill-sub">{b.sub}</span>
                  </button>
                ))}
              </div>
              <div className="mockup-wrap" style={{ background: stageBg(teeColor) }}>
                {photoMode !== "off" && availableMockups?.has(blank + ":" + teeColorKey) ? (
                  <PhotoMockupWithDesign
                    blank={blank}
                    colorKey={teeColorKey}
                    teeColor={teeColor}
                    designProps={{ text, layout, font, fontSize, letterSpacing, lineHeight, inkColor, rotation, distress, offsetX, offsetY, uploadedImage, imageScale, imagePosition }}
                  />
                ) : (
                  <BlankWithDesign
                    blank={blank}
                    teeColor={teeColor}
                    designProps={{ text, layout, font, fontSize, letterSpacing, lineHeight, inkColor, rotation, distress, offsetX, offsetY, uploadedImage, imageScale, imagePosition }}
                  />
                )}
                <div className="mockup-mode-badge">
                  {photoMode !== "off" && availableMockups?.has(blank + ":" + teeColorKey)
                    ? "PHOTO"
                    : "ILLUSTRATED"}
                </div>
              </div>
              <div className="tee-color-row" data-wstep="2">
                {(window.BLANK_COLORS[blank] || []).map(key => {
                  const c = window.LAUNCH_COLORS[key];
                  if (!c) return null;
                  const hasPhoto = availableMockups?.has(blank + ":" + key);
                  return (
                    <button
                      key={key}
                      className={`tee-swatch ${teeColorKey === key ? "active" : ""} ${hasPhoto ? "has-photo" : ""}`}
                      style={{ background: c.hex }}
                      onClick={() => setTeeColorKey(key)}
                      title={c.label + (hasPhoto ? " · photo mockup ready" : " · illustrated")}
                    >
                      {hasPhoto && <span className="photo-dot" />}
                    </button>
                  );
                })}
              </div>
            </div>

            {/* Print view — accurate print area for the selected blank */}
            <div className="preview-card">
              <div className="preview-card-head">
                <span className="card-label">PRINT AREA</span>
                <span className="card-meta">
                  {(() => {
                    const pa = (window.PRINT_AREAS || {})[blank];
                    if (!pa) return "—";
                    const inW = (pa.w / 300).toFixed(1);
                    const inH = (pa.h / 300).toFixed(1);
                    return `${pa.w} × ${pa.h} · ${inW}" × ${inH}"`;
                  })()}
                </span>
              </div>
              <div style={{
                fontSize: 10, letterSpacing: 1.5, color: "var(--ink-3)",
                textAlign: "center", padding: "6px 12px",
                borderBottom: "1px dashed var(--rule)", textTransform: "uppercase",
              }}>
                {(window.PRINT_AREAS || {})[blank]?.label || ""}
              </div>
              <div className="print-wrap" ref={printWrapRef}>
                <div
                  className="print-canvas"
                  style={{
                    width: previewSize,
                    height: (() => {
                      const pa = (window.PRINT_AREAS || {})[blank];
                      const ratio = pa ? (pa.h / pa.w) : (5400 / 4500);
                      return previewSize * ratio;
                    })(),
                    backgroundImage: `
                      linear-gradient(45deg, #e8e3d6 25%, transparent 25%),
                      linear-gradient(-45deg, #e8e3d6 25%, transparent 25%),
                      linear-gradient(45deg, transparent 75%, #e8e3d6 75%),
                      linear-gradient(-45deg, transparent 75%, #e8e3d6 75%)
                    `,
                    backgroundSize: "16px 16px",
                    backgroundPosition: "0 0, 0 8px, 8px -8px, -8px 0",
                    backgroundColor: "#f5f1e8",
                  }}
                >
                  <DesignCanvas
                    text={text}
                    layout={layout}
                    font={font}
                    fontSize={fontSize * previewScale}
                    letterSpacing={letterSpacing * previewScale}
                    lineHeight={lineHeight}
                    inkColor={inkColor}
                    rotation={rotation}
                    distress={distress}
                    offsetX={offsetX}
                    offsetY={offsetY}
                    width={previewSize}
                    height={(() => {
                      const pa = (window.PRINT_AREAS || {})[blank];
                      const r = pa ? (pa.h / pa.w) : (5400 / 4500);
                      return previewSize * r;
                    })()}
                    showFrame={true}
                  />
                </div>
              </div>
            </div>
          </div>

          {/* Export bar — customer mode shows wizard nav + ADD TO CART; admin shows export buttons */}
          <div className="export-bar">
            {customerMode ? (
              <>
                {wizardStep > 1 && (
                  <button
                    className="btn btn-ghost"
                    onClick={() => setWizardStep(s => Math.max(1, s - 1))}
                    disabled={!!exporting}
                    style={{ flex: "0 0 auto", fontSize: 12, letterSpacing: 2 }}
                  >← BACK</button>
                )}
                {wizardStep < 5 ? (
                  <button
                    className="btn btn-primary"
                    onClick={() => setWizardStep(s => Math.min(5, s + 1))}
                    style={{ flex: 1, fontSize: 14, letterSpacing: 3 }}
                  >NEXT →</button>
                ) : (
                  <button
                    className="btn btn-primary btn-push"
                    onClick={handleAddToCart}
                    disabled={!!exporting}
                    style={{ flex: 1, fontSize: 14, letterSpacing: 3 }}
                  >
                    {exporting === "cart" ? "ADDING…" : `↑ ADD TO CART · ${shirtSize}`}
                  </button>
                )}
              </>
            ) : (
              <>
                <button className="btn btn-ghost" onClick={handleExportDesign} disabled={!!exporting}>
                  {exporting === "design" ? "EXPORTING…" : "EXPORT DESIGN PNG"}
                </button>
                <button className="btn btn-ghost" onClick={handleExportMockup} disabled={!!exporting}>
                  {exporting === "mockup" ? "EXPORTING…" : "EXPORT MOCKUP PNG"}
                </button>
                <button className="btn btn-ghost" onClick={handleExportBoth} disabled={!!exporting}>
                  {exporting === "both" ? "EXPORTING…" : "EXPORT BOTH ↓"}
                </button>
                <button
                  className="btn btn-primary btn-push"
                  onClick={handlePushToPrintify}
                  disabled={!!exporting || !["tee3001","cc1717","tank3480","cc9360","perf350"].includes(blank)}
                  title={!["tee3001","cc1717","tank3480","cc9360","perf350"].includes(blank)
                    ? "Pick a blank from the picker first"
                    : "Upload to Printify and publish to Shopify"}
                >
                  {exporting === "push" ? "PUSHING…" : "↑ PUSH TO PRINTIFY"}
                </button>
              </>
            )}
          </div>
        </main>

        {/* RIGHT — controls */}
        <aside className="panel panel-right">
          <div className="panel-head">
            <span className="panel-title">CONTROLS</span>
          </div>

          <Section title="LAYOUT" wstep="4">
            <div className="grid-options">
              {LAYOUTS.map(l => (
                <button
                  key={l.id}
                  className={`opt-btn ${layout === l.id ? "active" : ""}`}
                  onClick={() => setLayout(l.id)}
                >
                  {l.label}
                </button>
              ))}
            </div>
          </Section>

          <Section title="TYPEFACE" wstep="4">
            <div className="grid-options">
              {FONTS.map(f => (
                <button
                  key={f.id}
                  className={`opt-btn font-btn ${font === f.id ? "active" : ""}`}
                  onClick={() => setFont(f.id)}
                  style={{ fontFamily: `"${f.id}", monospace` }}
                >
                  {f.label}
                </button>
              ))}
            </div>
          </Section>

          <Section title="TYPOGRAPHY" wstep="4">
            <Slider label="SIZE"     value={fontSize}     onChange={setFontSize}     min={24}  max={400} step={2}    suffix="px" />
            <Slider label="TRACKING" value={letterSpacing} onChange={setLetterSpacing} min={-4} max={20}  step={0.5}  suffix="px" />
            <Slider label="LEADING"  value={lineHeight}   onChange={setLineHeight}   min={0.8} max={2}   step={0.05}              />
            <Slider label="ROTATE"   value={rotation}     onChange={setRotation}     min={-15} max={15}  step={1}    suffix="°"  />
            <Slider label="DISTRESS" value={distress}     onChange={setDistress}     min={0}   max={0.8} step={0.05}              />
          </Section>

          <Section title="POSITION" wstep="4">
            <Slider label="LEFT ↔ RIGHT" value={offsetX} onChange={setOffsetX} min={-40} max={40} step={1} suffix="%" />
            <Slider label="UP ↕ DOWN"   value={offsetY} onChange={setOffsetY} min={-40} max={40} step={1} suffix="%" />
            <button
              type="button"
              onClick={() => { setOffsetX(0); setOffsetY(0); setRotation(0); }}
              style={{
                marginTop: 8, padding: "6px 10px", fontSize: 10, letterSpacing: 1.5,
                fontFamily: "Special Elite, monospace", textTransform: "uppercase",
                border: "1px solid var(--rule)", background: "var(--paper)",
                color: "var(--ink-3)", cursor: "pointer", width: "100%",
              }}
            >RECENTER ↺</button>
          </Section>

          <Section title="UPLOAD IMAGE" wstep="3">
            <input
              type="file"
              accept="image/png,image/jpeg,image/jpg,image/webp,image/svg+xml"
              onChange={(e) => {
                const f = e.target.files?.[0];
                if (!f) return;
                if (f.size > 10 * 1024 * 1024) {
                  showToast("Image too big (10 MB max)");
                  return;
                }
                const r = new FileReader();
                r.onload = () => {
                  const dataUrl = String(r.result);
                  const probe = new Image();
                  probe.onload = () => {
                    setUploadedImage(dataUrl);
                    setUploadedImageDims({ w: probe.naturalWidth, h: probe.naturalHeight });
                  };
                  probe.src = dataUrl;
                };
                r.readAsDataURL(f);
              }}
              style={{ width: "100%", fontSize: 11, marginBottom: 8 }}
            />
            {uploadedImage && (
              <>
                <Slider label="IMAGE SIZE" value={imageScale} onChange={setImageScale}
                        min={0.2} max={1.0} step={0.05} />
                <div className="grid-options" style={{ marginTop: 8 }}>
                  {[
                    {id:"above", label:"Above text"},
                    {id:"only",  label:"Image only"},
                    {id:"below", label:"Below text"},
                  ].map(o => (
                    <button key={o.id}
                      className={`opt-btn ${imagePosition === o.id ? "active" : ""}`}
                      onClick={() => setImagePosition(o.id)}>{o.label}</button>
                  ))}
                </div>
                <button
                  type="button"
                  onClick={() => setUploadedImage(null)}
                  style={{
                    marginTop: 8, padding: "6px 10px", fontSize: 10, letterSpacing: 1.5,
                    fontFamily: "Special Elite, monospace", textTransform: "uppercase",
                    border: "1px solid var(--rule)", background: "var(--paper)",
                    color: "var(--ink-3)", cursor: "pointer", width: "100%",
                  }}
                >REMOVE IMAGE ✕</button>
              </>
            )}
          </Section>

          {customerMode && (
            <Section title="SIZE" wstep="5">
              <div className="grid-options" style={{ gridTemplateColumns: "repeat(5, 1fr)" }}>
                {["S","M","L","XL","2XL"].map(s => (
                  <button key={s}
                    className={`opt-btn ${shirtSize === s ? "active" : ""}`}
                    onClick={() => setShirtSize(s)}>{s}</button>
                ))}
              </div>
            </Section>
          )}

          <Section title="INK" wstep="4">
            <div className="ink-row">
              {["#111111", "#f5f1e8", "#c0392b", "#1a4d8c", "#3d6e3d", "#d97706"].map(c => (
                <button
                  key={c}
                  className={`ink-swatch ${inkColor === c ? "active" : ""}`}
                  style={{ background: c }}
                  onClick={() => setInkColor(c)}
                />
              ))}
              <input
                type="color"
                value={inkColor}
                onChange={(e) => setInkColor(e.target.value)}
                className="ink-picker"
                title="Custom ink color"
              />
            </div>
          </Section>

          <div className="footer-note">
            ＊ Output is 4500×5400 transparent PNG —<br/>
            drop directly into Printify.
          </div>
        </aside>
      </div>

      {toast && <div className="toast">{toast}</div>}
    </div>
  );
}

// Photo-status pill — shows X / Y mockups detected, with a tooltip listing missing
function PhotoStatus({ available }) {
  if (!available) return <span className="meta-cell">SCANNING MOCKUPS…</span>;
  let total = 0, found = 0;
  const missing = [];
  for (const blankId in window.MOCKUP_MANIFEST) {
    const m = window.MOCKUP_MANIFEST[blankId];
    for (const colorKey in m.variants) {
      total++;
      if (available.has(blankId + ":" + colorKey)) found++;
      else missing.push(m.variants[colorKey].replace("mockups/", ""));
    }
  }
  const pct = total ? Math.round((found / total) * 100) : 0;
  const tt = found === total
    ? "All mockups present"
    : `Missing ${missing.length}:\n` + missing.slice(0, 12).join("\n") + (missing.length > 12 ? `\n…+${missing.length - 12} more` : "");
  return (
    <span className={`meta-cell photo-status ${found === 0 ? "none" : found === total ? "all" : "some"}`} title={tt}>
      <span className="status-dot" />
      PHOTOS {found}/{total}
    </span>
  );
}

// Section wrapper
function Section({ title, children, wstep }) {
  return (
    <div className="section" data-wstep={wstep}>
      <div className="section-head">{title}</div>
      <div className="section-body">{children}</div>
    </div>
  );
}

function Slider({ label, value, onChange, min, max, step = 1, suffix = "" }) {
  const dec = () => onChange(Math.max(min, +(value - step).toFixed(4)));
  const inc = () => onChange(Math.min(max, +(value + step).toFixed(4)));
  return (
    <div className="slider-row">
      <div className="slider-meta">
        <span className="slider-label">{label}</span>
        <span className="slider-val">{Number(value).toFixed(step < 1 ? 2 : 0)}{suffix}</span>
      </div>
      <div className="stepper">
        <button type="button" className="stepper-btn" onClick={dec} aria-label="decrease">−</button>
        <input
          type="range"
          min={min}
          max={max}
          step={step}
          value={value}
          onChange={(e) => onChange(parseFloat(e.target.value))}
        />
        <button type="button" className="stepper-btn" onClick={inc} aria-label="increase">+</button>
      </div>
    </div>
  );
}

// PhotoMockupWithDesign — real product photo with design overlaid at chest area
function PhotoMockupWithDesign({ blank, colorKey, teeColor, designProps }) {
  const url = window.MOCKUP_MANIFEST?.[blank]?.variants?.[colorKey];
  const rect = window.getPrintRect ? window.getPrintRect(blank, colorKey) : { x: 0.36, y: 0.30, w: 0.28, h: 0.34 };
  const isDark = (hex) => {
    const h = hex.replace("#", "");
    const r = parseInt(h.slice(0, 2), 16);
    const g = parseInt(h.slice(2, 4), 16);
    const b = parseInt(h.slice(4, 6), 16);
    return (r * 0.299 + g * 0.587 + b * 0.114) < 130;
  };
  const wrapRef = useRef(null);
  const [box, setBox] = useState({ w: 0, h: 0 });
  useEffect(() => {
    if (!wrapRef.current) return;
    const ro = new ResizeObserver((entries) => {
      const r = entries[0].contentRect;
      setBox({ w: r.width, h: r.height });
    });
    ro.observe(wrapRef.current);
    return () => ro.disconnect();
  }, []);

  // The photo itself sets the aspect ratio of the wrap; we overlay with absolute positions in % so it scales perfectly.
  return (
    <div ref={wrapRef} className="photo-mockup">
      <img src={url} alt="" className="photo-mockup-img" />
      <div
        className="photo-mockup-overlay"
        style={{
          left: (rect.x * 100) + "%",
          top: (rect.y * 100) + "%",
          width: (rect.w * 100) + "%",
          height: (rect.h * 100) + "%",
          mixBlendMode: isDark(teeColor) ? "screen" : "multiply",
        }}
      >
        {box.w > 0 && (
          <DesignCanvas
            {...designProps}
            fontSize={designProps.layout === "fill"
              ? designProps.fontSize
              : designProps.fontSize * (box.w * rect.w / 1500)}
            letterSpacing={designProps.letterSpacing * (box.w * rect.w / 1500)}
            width={box.w * rect.w}
            height={box.h * rect.h}
          />
        )}
      </div>
    </div>
  );
}

// BlankWithDesign — render the SVG silhouette as a background, then overlay
// the DesignCanvas as an absolute-positioned HTML element ON TOP of the SVG.
// This avoids using SVG foreignObject — which has known rendering bugs in
// iOS Safari (text inside foreignObject is invisible on some devices).
function BlankWithDesign({ blank, teeColor, designProps }) {
  const isDark = (hex) => {
    const h = hex.replace("#", "");
    const r = parseInt(h.slice(0, 2), 16);
    const g = parseInt(h.slice(2, 4), 16);
    const b = parseInt(h.slice(4, 6), 16);
    return (r * 0.299 + g * 0.587 + b * 0.114) < 130;
  };
  const Silhouette = (window.BlankSilhouettes || {})[blank] || (window.BlankSilhouettes || {}).tee3001;
  // Design area is in SVG viewBox coords (1000x1000). Convert to % for overlay.
  const da = (window.BLANK_DESIGN_AREA || {})[blank] || { x: 330, y: 290, w: 340, h: 420 };
  const wrapRef = useRef(null);
  const [box, setBox] = useState({ w: 0, h: 0 });
  useEffect(() => {
    if (!wrapRef.current) return;
    const ro = new ResizeObserver((entries) => {
      const r = entries[0].contentRect;
      setBox({ w: r.width, h: r.height });
    });
    ro.observe(wrapRef.current);
    return () => ro.disconnect();
  }, []);
  if (!Silhouette) return null;

  // SVG has 1:1 aspect (viewBox 0 0 1000 1000). The actual rendered size is the
  // smaller of box.w / box.h since the SVG preserves aspect.
  const svgSize = Math.min(box.w, box.h) || 1;
  const overlayLeft   = (da.x / 1000) * svgSize + (box.w - svgSize) / 2;
  const overlayTop    = (da.y / 1000) * svgSize + (box.h - svgSize) / 2;
  const overlayWidth  = (da.w / 1000) * svgSize;
  const overlayHeight = (da.h / 1000) * svgSize;

  return (
    <div ref={wrapRef} style={{
      position: "relative",
      width: "100%", height: "100%",
      display: "flex", alignItems: "center", justifyContent: "center",
    }}>
      <Silhouette teeColor={teeColor} />
      {box.w > 0 && (
        <div style={{
          position: "absolute",
          left: overlayLeft, top: overlayTop,
          width: overlayWidth, height: overlayHeight,
          mixBlendMode: isDark(teeColor) ? "screen" : "multiply",
          pointerEvents: "none",
        }}>
          <DesignCanvas
            {...designProps}
            fontSize={designProps.layout === "fill"
              ? designProps.fontSize
              : designProps.fontSize * (overlayWidth / 1500)}
            letterSpacing={designProps.letterSpacing * (overlayWidth / 1500)}
            width={overlayWidth}
            height={overlayHeight}
          />
        </div>
      )}
    </div>
  );
}

// TeeMockupWithDesign — legacy fallback
function TeeMockupWithDesign({ teeColor, designProps }) {
  return <BlankWithDesign blank="tee3001" teeColor={teeColor} designProps={designProps} />;
}

// Random saying — picked from the existing 280-phrase library so every
// pull is a vetted, on-brand phrase rather than a procedural mash-up.
function generateRandomSaying(category) {
  const lib = window.PHRASE_LIBRARY || {};
  const pool = (category && category !== "all" && Array.isArray(lib[category]))
    ? lib[category]
    : Object.values(lib).flat();
  if (!pool.length) return "NO CAP / I PAID THE / TOURNAMENT FEE";
  return pool[Math.floor(Math.random() * pool.length)];
}

// Human-readable labels for the customer-facing category pills
const PHRASE_CATEGORY_LABELS = {
  all:        "ALL",
  rizz:       "Rizz",
  cap:        "No Cap",
  cooked:     "Cooked",
  mom:        "Mom Life",
  dad:        "Dad Life",
  era:        "Era",
  broke:      "Broke",
  ump:        "Ump Calls",
  vibe:       "Vibes",
  cringe:     "Cringe",
  skibidi:    "Skibidi",
  tired:      "Tired",
  kid:        "Kid",
  wins:       "Wins/Ws",
  delulu:     "Delulu",
  loadout:    "Loadout",
  drama:      "Drama",
  schedule:   "Schedule",
  genx:       "Gen X",
  translate:  "Translate",
  versus:     "Versus",
  vsteen:     "vs Teen",
  genxsport:  "Gen X Sport",
  sport:      "Sport-Coded",
  coach:      "Coach",
  tournament: "Tournament",
  weekend:    "Weekend",
  snack:      "Snacks",
  ref:        "Ref/Blue",
  field:      "Field Life",
  carpool:    "Carpool",
  money:      "Money",
  energy:     "Energy",
};

function makeFilename(text) {
  const slug = text
    .replace(/[\/\\]/g, "-")
    .replace(/[^a-z0-9-_ ]/gi, "")
    .trim()
    .replace(/\s+/g, "_")
    .toLowerCase()
    .slice(0, 48) || "design";
  return "nocap_" + slug;
}

function colorName(hex) {
  const lc = hex.toLowerCase();
  const colors = window.LAUNCH_COLORS || {};
  for (const k in colors) {
    if (colors[k].hex.toLowerCase() === lc) return colors[k].label;
  }
  return hex.replace("#", "");
}

function stageBg(teeColor) {
  // Always use a soft neutral so any tee color reads clearly
  return "linear-gradient(180deg, #f4efe6 0%, #e6dfd1 100%)";
}

// ─────────────────────────────────────────────────────────────────────────────
// CustomerWizard — dedicated, polished customer UI (5 steps, big preview)
// ─────────────────────────────────────────────────────────────────────────────
function CustomerWizard(p) {
  const STEPS = [
    { id: 1, label: "STYLE" },
    { id: 2, label: "COLOR" },
    { id: 3, label: "DESIGN" },
    { id: 4, label: "TWEAK" },
    { id: 5, label: "SIZE" },
  ];

  const blanks = (window.BLANKS_META || []);
  const blankColors = (window.BLANK_COLORS || {})[p.blank] || [];
  const launchColors = window.LAUNCH_COLORS || {};

  const next = () => p.setWizardStep(s => Math.min(5, s + 1));
  const back = () => p.setWizardStep(s => Math.max(1, s - 1));

  const designProps = {
    text: p.text, layout: p.layout, font: p.font, fontSize: p.fontSize,
    letterSpacing: p.letterSpacing, lineHeight: p.lineHeight,
    inkColor: p.inkColor, rotation: p.rotation, distress: p.distress,
    offsetX: p.offsetX, offsetY: p.offsetY,
    uploadedImage: p.uploadedImage, imageScale: p.imageScale, imagePosition: p.imagePosition,
  };

  return (
    <div className="cw">
      {/* PREVIEW — sticky at top */}
      <div className="cw-preview">
        <BlankWithDesign blank={p.blank} teeColor={p.teeColor} designProps={designProps} />
        {(() => {
          // Print-quality badge: green when text-only or uploaded image is high-res,
          // yellow when the uploaded image will likely look blurry on the shirt.
          // Threshold: image natural dimension × imageScale ≥ 1200px → safe.
          const dims = p.uploadedImageDims;
          let label = "PRINT-READY", cls = "ok";
          if (p.uploadedImage && dims) {
            const eff = Math.min(dims.w, dims.h) * (p.imageScale || 0.6);
            if (eff < 700) { label = "LOW-RES IMAGE"; cls = "warn"; }
            else if (eff < 1100) { label = "OK QUALITY"; cls = "mid"; }
          }
          return <div className={`cw-quality cw-quality-${cls}`}>{label}</div>;
        })()}
      </div>

      {/* STEP INDICATOR */}
      <div className="cw-steps">
        {STEPS.map(s => (
          <button key={s.id} type="button"
            onClick={() => p.setWizardStep(s.id)}
            className={`cw-step ${p.wizardStep === s.id ? "active" : ""} ${p.wizardStep > s.id ? "done" : ""}`}>
            <span className="cw-step-num">{s.id}</span>
            <span className="cw-step-label">{s.label}</span>
          </button>
        ))}
      </div>

      <div className="cw-content">
        {p.wizardStep === 1 && (
          <div className="cw-card">
            <h2 className="cw-h2">Pick your shirt</h2>
            <div className="cw-tiles">
              {blanks.map(b => (
                <button key={b.id} type="button"
                  className={`cw-tile ${p.blank === b.id ? "active" : ""}`}
                  onClick={() => p.setBlank(b.id)}>
                  <div className="cw-tile-label">{b.label}</div>
                  <div className="cw-tile-sub">{b.sub}</div>
                </button>
              ))}
            </div>
          </div>
        )}

        {p.wizardStep === 2 && (
          <div className="cw-card">
            <h2 className="cw-h2">Pick a color</h2>
            <div className="cw-colors">
              {blankColors.map(key => {
                const c = launchColors[key];
                if (!c) return null;
                return (
                  <button key={key} type="button"
                    className={`cw-color ${p.teeColorKey === key ? "active" : ""}`}
                    style={{ background: c.hex }}
                    onClick={() => p.setTeeColorKey(key)}
                    title={c.label}>
                    <span className="cw-color-name">{c.label}</span>
                  </button>
                );
              })}
            </div>
          </div>
        )}

        {p.wizardStep === 3 && (
          <div className="cw-card">
            <h2 className="cw-h2">Your design</h2>
            <label className="cw-label">TEXT — use " / " for new lines</label>
            <textarea
              className="cw-textarea"
              value={p.text}
              rows={3}
              onChange={(e) => p.setText(e.target.value)}
              placeholder="Type your phrase..."
            />

            <label className="cw-label" style={{ marginTop: 12 }}>RANDOM FROM CATEGORY</label>
            <div className="cw-cat-row">
              {["all", ...Object.keys(window.PHRASE_LIBRARY || {})].map(cat => (
                <button key={cat} type="button"
                  className={`cw-cat-pill ${p.randomCategory === cat ? "active" : ""}`}
                  onClick={() => p.setRandomCategory(cat)}>
                  {PHRASE_CATEGORY_LABELS[cat] || cat}
                </button>
              ))}
            </div>

            <button type="button" className="cw-btn cw-btn-accent"
              onClick={() => p.setText(generateRandomSaying(p.randomCategory))}>
              🎲 Random {p.randomCategory === "all" ? "saying" : (PHRASE_CATEGORY_LABELS[p.randomCategory] || p.randomCategory).toLowerCase() + " saying"}
            </button>
            <div className="cw-divider">— OR / AND —</div>
            <label className="cw-label">UPLOAD A LOGO OR IMAGE</label>
            <input type="file"
              accept="image/png,image/jpeg,image/jpg,image/webp"
              className="cw-file"
              onChange={(e) => {
                const f = e.target.files?.[0];
                if (!f) return;
                const r = new FileReader();
                r.onload = () => {
                  const dataUrl = String(r.result);
                  const probe = new Image();
                  probe.onload = () => {
                    if (probe.naturalWidth < 1200 || probe.naturalHeight < 1200) {
                      p.showToast && p.showToast(
                        `Image too low-res (${probe.naturalWidth}×${probe.naturalHeight}). Use one ≥ 1200×1200 px.`
                      );
                      return;
                    }
                    p.setUploadedImage(dataUrl);
                    p.setUploadedImageDims && p.setUploadedImageDims({ w: probe.naturalWidth, h: probe.naturalHeight });
                  };
                  probe.src = dataUrl;
                };
                r.readAsDataURL(f);
              }}
            />
            {p.uploadedImage && (
              <>
                <div className="cw-row">
                  {[
                    { id: "above", label: "Above text" },
                    { id: "only",  label: "Image only" },
                    { id: "below", label: "Below text" },
                  ].map(o => (
                    <button key={o.id} type="button"
                      className={`cw-pill ${p.imagePosition === o.id ? "active" : ""}`}
                      onClick={() => p.setImagePosition(o.id)}>{o.label}</button>
                  ))}
                </div>
                <button type="button" className="cw-btn cw-btn-ghost"
                  onClick={() => p.setUploadedImage(null)}>Remove image</button>
              </>
            )}
          </div>
        )}

        {p.wizardStep === 4 && (
          <div className="cw-card">
            <h2 className="cw-h2">Make it yours</h2>

            <label className="cw-label">LAYOUT</label>
            <div className="cw-grid-3">
              {LAYOUTS.map(l => (
                <button key={l.id} type="button"
                  className={`cw-pill ${p.layout === l.id ? "active" : ""}`}
                  onClick={() => p.setLayout(l.id)}>{l.label}</button>
              ))}
            </div>

            <label className="cw-label">INK COLOR</label>
            <div className="cw-row cw-row-wrap">
              {["#111111", "#f5f1e8", "#c0392b", "#1a4d8c", "#3d6e3d", "#d97706"].map(c => (
                <button key={c} type="button"
                  className={`cw-ink ${p.inkColor === c ? "active" : ""}`}
                  style={{ background: c }}
                  onClick={() => p.setInkColor(c)} />
              ))}
              <input type="color" value={p.inkColor}
                onChange={(e) => p.setInkColor(e.target.value)}
                className="cw-ink cw-ink-picker" />
            </div>

            {p.uploadedImage && p.imagePosition === "only" ? (
              <>
                <label className="cw-label">IMAGE SIZE</label>
                <CWStepper
                  value={Math.round(p.imageScale * 100)}
                  onChange={(v) => p.setImageScale(Math.max(0.1, Math.min(1, v / 100)))}
                  min={10} max={100} step={5} suffix="%" />
              </>
            ) : (
              <>
                <label className="cw-label">SIZE</label>
                <CWStepper value={p.fontSize} onChange={p.setFontSize} min={48} max={400} step={4} suffix="px" />
              </>
            )}

            <label className="cw-label">POSITION</label>
            <div className="cw-stepper-stack">
              <CWStepper value={p.offsetY} onChange={p.setOffsetY} min={-40} max={40} step={2} suffix="%" prefix="↕" />
              <CWStepper value={p.offsetX} onChange={p.setOffsetX} min={-40} max={40} step={2} suffix="%" prefix="↔" />
            </div>
          </div>
        )}

        {p.wizardStep === 5 && (
          <div className="cw-card">
            <h2 className="cw-h2">Pick your size</h2>
            <div className="cw-sizes">
              {["S","M","L","XL","2XL"].map(s => (
                <button key={s} type="button"
                  className={`cw-size ${p.shirtSize === s ? "active" : ""}`}
                  onClick={() => p.setShirtSize(s)}>{s}</button>
              ))}
            </div>
            <div className="cw-summary">
              <div><span className="cw-label">PHRASE</span><span>{p.text}</span></div>
              <div><span className="cw-label">SHIRT</span><span>{(blanks.find(b=>b.id===p.blank)||{}).label} · {(launchColors[p.teeColorKey]||{}).label} · {p.shirtSize}</span></div>
            </div>
          </div>
        )}
      </div>

      {/* NAV BAR */}
      <div className="cw-nav">
        {p.cartAdded ? (
          <>
            <button type="button" className="cw-btn cw-btn-ghost" onClick={p.onDesignAnother}>
              + DESIGN ANOTHER
            </button>
            <button type="button" className="cw-btn cw-btn-primary cw-btn-checkout"
              onClick={p.onCheckoutNow}>
              CHECKOUT NOW →
            </button>
          </>
        ) : (
          <>
            {p.wizardStep > 1 && (
              <button type="button" className="cw-btn cw-btn-ghost" onClick={back}>
                ← BACK
              </button>
            )}
            {p.wizardStep < 5 ? (
              <button type="button" className="cw-btn cw-btn-primary" onClick={next}>
                NEXT →
              </button>
            ) : (
              <button type="button" className="cw-btn cw-btn-primary cw-btn-cart"
                onClick={p.handleAddToCart} disabled={!!p.exporting}>
                {p.exporting === "cart" ? "ADDING…" : `↑ ADD TO CART · ${p.shirtSize}`}
              </button>
            )}
          </>
        )}
      </div>

      {p.toast && <div className="toast">{p.toast}</div>}
    </div>
  );
}

function CWStepper({ value, onChange, min, max, step, suffix = "", prefix = "" }) {
  const dec = () => onChange(Math.max(min, +(value - step).toFixed(4)));
  const inc = () => onChange(Math.min(max, +(value + step).toFixed(4)));
  return (
    <div className="cw-stepper">
      <button type="button" className="cw-stepper-btn" onClick={dec}>−</button>
      <div className="cw-stepper-val">{prefix && (prefix + " ")}{Number(value).toFixed(0)}{suffix}</div>
      <button type="button" className="cw-stepper-btn" onClick={inc}>+</button>
    </div>
  );
}

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