// ── bits.jsx ───────────────────────────────────────────────────────────────
// Small shared building blocks for the redesigned Skein flow: responsive sheet,
// segmented control, chips, yarn-thickness swatch, toast, icons, share helper.

const { useState: useStateB, useEffect: useEffectB, useRef: useRefB, useCallback: useCbB } = React

// ── responsive helper ───────────────────────────────────────────────────────
function useMedia(query) {
  const [m, setM] = useStateB(() =>
    typeof window !== "undefined" && window.matchMedia ? window.matchMedia(query).matches : false)
  useEffectB(() => {
    if (!window.matchMedia) return
    const mq = window.matchMedia(query)
    const on = () => setM(mq.matches)
    on()
    mq.addEventListener ? mq.addEventListener("change", on) : mq.addListener(on)
    return () => mq.removeEventListener ? mq.removeEventListener("change", on) : mq.removeListener(on)
  }, [query])
  return m
}

// ── icons (stroke, inherit color) ───────────────────────────────────────────
const Icon = {
  chevron: (p) => <svg viewBox="0 0 24 24" width={p?.s || 18} height={p?.s || 18} fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round" style={p?.style}><path d="M6 9l6 6 6-6" /></svg>,
  close: (p) => <svg viewBox="0 0 24 24" width={p?.s || 18} height={p?.s || 18} fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round"><path d="M6 6l12 12M18 6L6 18" /></svg>,
  menu: (p) => <svg viewBox="0 0 24 24" width={p?.s || 20} height={p?.s || 20} fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M4 7h16M4 12h16M4 17h16" /></svg>,
  share: (p) => <svg viewBox="0 0 24 24" width={p?.s || 18} height={p?.s || 18} fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><circle cx="18" cy="5" r="3" /><circle cx="6" cy="12" r="3" /><circle cx="18" cy="19" r="3" /><path d="M8.6 13.5l6.8 4M15.4 6.5l-6.8 4" /></svg>,
  save: (p) => <svg viewBox="0 0 24 24" width={p?.s || 18} height={p?.s || 18} fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M5 4h11l3 3v13H5z" /><path d="M8 4v5h7M8 20v-6h8v6" /></svg>,
  plus: (p) => <svg viewBox="0 0 24 24" width={p?.s || 18} height={p?.s || 18} fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round"><path d="M12 5v14M5 12h14" /></svg>,
  back: (p) => <svg viewBox="0 0 24 24" width={p?.s || 18} height={p?.s || 18} fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round"><path d="M15 6l-6 6 6 6" /></svg>,
  check: (p) => <svg viewBox="0 0 24 24" width={p?.s || 16} height={p?.s || 16} fill="none" stroke="currentColor" strokeWidth="2.6" strokeLinecap="round" strokeLinejoin="round"><path d="M5 12.5l4.5 4.5L19 6.5" /></svg>,
  trash: (p) => <svg viewBox="0 0 24 24" width={p?.s || 16} height={p?.s || 16} fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M4 7h16M9 7V5h6v2M6 7l1 13h10l1-13" /></svg>,
  reset: (p) => <svg viewBox="0 0 24 24" width={p?.s || 16} height={p?.s || 16} fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M4 4v6h6M4.5 13a8 8 0 1 0 1.5-6.5L4 10" /></svg>,
  pencil: (p) => <svg viewBox="0 0 24 24" width={p?.s || 16} height={p?.s || 16} fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M4 20h4l10-10-4-4L4 16v4z" /><path d="M13.5 6.5l4 4" /></svg>,
  tune: (p) => <svg viewBox="0 0 24 24" width={p?.s || 18} height={p?.s || 18} fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M5 8h9M18 8h1M5 16h1M10 16h9" /><circle cx="16" cy="8" r="2.3" fill="var(--card)" /><circle cx="8" cy="16" r="2.3" fill="var(--card)" /></svg>,
}

// ── yarn-thickness swatch — a quick visual cue for weight ───────────────────
const YARN_STRAND = { lace: 1.2, fingering: 1.9, sport: 2.7, dk: 3.6, worsted: 4.6, bulky: 6.4, superbulky: 9, custom: 4 }

// Resolve a yarn key to how it should be DRAWN, kept identical everywhere it
// appears (selector chip + result buy-summary). Custom yarn borrows the strand
// thickness of the closest standard weight (honest) and is flagged so callers
// can add the distinguishing ★ badge + accent-2 treatment.
function yarnVisual(key) {
  if (key === "custom" && typeof YARN !== "undefined" && YARN.custom) {
    const order = ["lace", "fingering", "sport", "dk", "worsted", "bulky", "superbulky"]
    let best = "dk", bd = Infinity
    order.forEach(k => { const dd = Math.abs(YARN[k].mPer100g - YARN.custom.mPer100g); if (dd < bd) { bd = dd; best = k } })
    return { weight: best, custom: true }
  }
  return { weight: key, custom: false }
}

// Shared swatch + optional custom badge. `framed` wraps it in a field tile
// (used in the result panel); the selector draws it bare on the chip.
function YarnIndicator({ yarnKey, w = 42, h = 22, color, framed = false }) {
  const v = yarnVisual(yarnKey)
  const strokeColor = color || (v.custom ? "var(--accent-2)" : "var(--accent)")
  const inner = (
    <span style={{ position: "relative", display: "grid", placeItems: "center", height: h }}>
      <YarnSwatch weight={v.weight} w={w} h={h} color={strokeColor} />
      {v.custom && <span aria-hidden="true" style={{ position: "absolute", top: -5, right: -7, width: 13, height: 13,
        borderRadius: "50%", background: "var(--accent-2)", border: "2px solid var(--card)", color: "var(--on-accent)",
        display: "grid", placeItems: "center", fontSize: 8, fontWeight: 900, lineHeight: 1 }}>★</span>}
    </span>
  )
  if (!framed) return inner
  return (
    <span style={{ display: "grid", placeItems: "center", width: w + 4, height: h + 6, borderRadius: 8,
      background: "var(--field)", border: "1px solid var(--line)", flexShrink: 0 }}>{inner}</span>
  )
}
function YarnSwatch({ weight = "dk", w = 40, h = 26, color = "var(--accent)" }) {
  const sw = YARN_STRAND[weight] || 3.6
  const rows = []
  let y = h / 2
  const gap = sw + Math.max(2.2, sw * 0.7)
  // center a stack of strands
  const n = Math.max(1, Math.floor((h - 4) / gap))
  const startY = h / 2 - ((n - 1) * gap) / 2
  for (let i = 0; i < n; i++) {
    const yy = startY + i * gap
    rows.push(<path key={i} d={`M3 ${yy} q ${w / 4} ${-sw * 1.1} ${w / 2} 0 t ${w / 2} 0`}
      fill="none" stroke={color} strokeWidth={sw} strokeLinecap="round" opacity={0.85} />)
  }
  return <svg viewBox={`0 0 ${w} ${h}`} width={w} height={h} style={{ display: "block", overflow: "visible" }} aria-hidden="true">{rows}</svg>
}

// ── responsive sheet: bottom sheet on mobile, centered modal on desktop ──────
function Sheet({ open, onClose, title, children, footer, maxWidth = 460 }) {
  const isMobile = useMedia("(max-width: 760px)")
  const [mounted, setMounted] = useStateB(open)
  const [shown, setShown] = useStateB(false)
  useEffectB(() => {
    if (open) { setMounted(true); const r = setTimeout(() => setShown(true), 16); return () => clearTimeout(r) }
    else { setShown(false); const t = setTimeout(() => setMounted(false), 240); return () => clearTimeout(t) }
  }, [open])
  useEffectB(() => {
    if (!open) return
    const onKey = (e) => { if (e.key === "Escape") onClose() }
    window.addEventListener("keydown", onKey)
    return () => window.removeEventListener("keydown", onKey)
  }, [open, onClose])
  const inIframe = window.parent !== window
  if (!mounted) return null
  const panelStyle = isMobile
    ? { position: "absolute", left: 0, right: 0, bottom: 0, maxHeight: "92%", borderRadius: "20px 20px 0 0",
        transform: shown ? "translateY(0)" : "translateY(101%)" }
    : { position: "absolute", left: "50%", top: inIframe ? "20px" : "50%", width: `min(${maxWidth}px, 94%)`, maxHeight: "88%",
        borderRadius: 18, transform: `translate(-50%, ${inIframe ? "0" : "-50%"}) scale(${shown ? 1 : 0.96})`, opacity: shown ? 1 : 0 }
  return (
    <div onClick={onClose} style={{ position: "fixed", inset: 0, zIndex: 80, display: "flex",
      background: shown ? "rgba(20,14,6,.42)" : "rgba(20,14,6,0)", transition: "background .24s",
      alignItems: "flex-end", justifyContent: "center" }}>
      <div onClick={e => e.stopPropagation()} className="card"
        style={{ background: "var(--card)", border: "1px solid var(--line)", display: "flex", flexDirection: "column",
          boxShadow: "0 -8px 40px rgba(20,14,6,.28)", transition: "transform .26s cubic-bezier(.22,.7,.3,1), opacity .24s",
          ...panelStyle }}>
        {isMobile && <div style={{ display: "grid", placeItems: "center", padding: "9px 0 2px", flexShrink: 0 }}>
          <span style={{ width: 38, height: 4, borderRadius: 3, background: "var(--line-strong)" }} /></div>}
        {title && (
          <div style={{ display: "flex", alignItems: "center", gap: 10, padding: "12px 16px 10px", flexShrink: 0,
            borderBottom: "1px solid var(--line)" }}>
            <span style={{ fontFamily: "var(--font-display)", fontSize: 19, color: "var(--ink)", marginRight: "auto" }}>{title}</span>
            <button onClick={onClose} aria-label="Close" style={iconBtn}><Icon.close /></button>
          </div>
        )}
        <div style={{ overflowY: "auto", overflowX: "hidden", padding: 16,
          paddingBottom: isMobile && !footer ? "calc(16px + env(safe-area-inset-bottom))" : 16,
          display: "flex", flexDirection: "column",
          gap: 14, WebkitOverflowScrolling: "touch", flex: 1 }}>{children}</div>
        {footer && <div style={{ padding: "12px 16px", paddingBottom: isMobile ? "calc(12px + env(safe-area-inset-bottom))" : 12,
          borderTop: "1px solid var(--line)", flexShrink: 0,
          background: "var(--card)" }}>{footer}</div>}
      </div>
    </div>
  )
}

const iconBtn = {
  display: "grid", placeItems: "center", width: 38, height: 38, borderRadius: 11, flexShrink: 0,
  background: "transparent", border: "1px solid var(--line)", color: "var(--muted)", cursor: "pointer",
}

// ── segmented control — accessible, animated thumb ──────────────────────────
function Segmented({ value, options, onChange, size = "md", grow = true }) {
  // options: [{value,label,sub?}] or [value,...]
  const opts = options.map(o => (typeof o === "object" ? o : { value: o, label: o }))
  const pad = size === "sm" ? "6px 10px" : "9px 12px"
  const fs = size === "sm" ? 12.5 : 13.5
  return (
    <div role="radiogroup" style={{ display: "flex", gap: 3, background: "var(--field-dim)", borderRadius: 12,
      padding: 3, border: "1px solid var(--line)", flexWrap: grow ? "nowrap" : "wrap" }}>
      {opts.map(o => {
        const on = o.value === value
        return (
          <button key={o.value} role="radio" aria-checked={on} onClick={() => onChange(o.value)}
            style={{ flex: grow ? "1 1 0" : "0 0 auto", minWidth: 0, border: "none", cursor: "pointer",
              background: on ? "var(--card)" : "transparent", borderRadius: 9, padding: pad,
              boxShadow: on ? "0 1px 3px rgba(40,30,15,.16)" : "none", fontFamily: "var(--font-ui)",
              color: on ? "var(--ink)" : "var(--muted)", fontWeight: on ? 700 : 600, fontSize: fs,
              transition: "background .16s, color .16s, box-shadow .16s", display: "flex",
              flexDirection: "column", alignItems: "center", gap: 1, whiteSpace: "nowrap", lineHeight: 1.2 }}>
            <span>{o.label}</span>
            {o.sub && <span style={{ fontSize: 9.5, fontWeight: 600, color: on ? "var(--accent)" : "var(--muted)",
              opacity: on ? 1 : .7 }}>{o.sub}</span>}
          </button>
        )
      })}
    </div>
  )
}

// ── horizontal scroll row with edge-fade affordance ────────────────────────
// Fades the content at an edge ONLY when there's more to scroll that way, so a
// partial card peeks + fades = "there's more". Uses a mask on the content
// (fades to transparent, not a color) so it's identical across all themes.
function ScrollRow({ children, gap = 7, pad = 22, snap = false, style, className }) {
  const ref = useRefB(null)
  const [edges, setEdges] = useStateB({ l: false, r: false })
  const [hover, setHover] = useStateB(false)
  const update = useCbB(() => {
    const el = ref.current; if (!el) return
    const l = el.scrollLeft > 2
    const r = el.scrollLeft + el.clientWidth < el.scrollWidth - 2
    setEdges(prev => (prev.l === l && prev.r === r) ? prev : { l, r })
  }, [])
  useEffectB(() => {
    update()
    const el = ref.current; if (!el) return
    el.addEventListener("scroll", update, { passive: true })
    window.addEventListener("resize", update)
    // let a vertical mouse wheel scroll the row horizontally (desktop mice)
    const onWheel = (e) => {
      if (e.deltaX !== 0) return
      const canScroll = el.scrollWidth > el.clientWidth + 2
      if (!canScroll) return
      e.preventDefault()
      el.scrollLeft += e.deltaY
    }
    el.addEventListener("wheel", onWheel, { passive: false })
    let ro
    if (typeof ResizeObserver !== "undefined") { ro = new ResizeObserver(update); ro.observe(el) }
    return () => { el.removeEventListener("scroll", update); el.removeEventListener("wheel", onWheel); window.removeEventListener("resize", update); ro && ro.disconnect() }
  }, [update, children])
  const nudge = (dir) => {
    const el = ref.current; if (!el) return
    const amount = dir * Math.max(140, el.clientWidth * 0.7)
    const start = el.scrollLeft
    const max = el.scrollWidth - el.clientWidth
    const target = Math.max(0, Math.min(max, start + amount))
    const dist = target - start
    if (Math.abs(dist) < 1) return
    const dur = 280, t0 = performance.now()
    const ease = (p) => 1 - Math.pow(1 - p, 3)
    const step = (now) => {
      const p = Math.min(1, (now - t0) / dur)
      el.scrollLeft = start + dist * ease(p)
      if (p < 1) requestAnimationFrame(step)
    }
    requestAnimationFrame(step)
  }
  const mask = `linear-gradient(to right, ${edges.l ? "transparent" : "#000"} 0, #000 ${pad}px, #000 calc(100% - ${pad}px), ${edges.r ? "transparent" : "#000"} 100%)`
  const arrow = (side) => {
    const visible = (side === "l" ? edges.l : edges.r) && hover
    return (
      <button aria-label={side === "l" ? "Scroll left" : "Scroll right"} tabIndex={-1}
        onClick={() => nudge(side === "l" ? -1 : 1)}
        style={{ position: "absolute", top: "50%", [side === "l" ? "left" : "right"]: 2, zIndex: 3,
          transform: `translateY(-50%) scale(${visible ? 1 : .8})`, opacity: visible ? 1 : 0,
          pointerEvents: visible ? "auto" : "none", transition: "opacity .15s, transform .15s",
          width: 30, height: 30, borderRadius: "50%", display: "grid", placeItems: "center", cursor: "pointer",
          background: "var(--card)", border: "1px solid var(--line-strong)", color: "var(--ink)",
          boxShadow: "0 2px 8px rgba(20,14,6,.18)" }}>
        <Icon.chevron s={17} style={{ transform: `rotate(${side === "l" ? 90 : -90}deg)` }} />
      </button>
    )
  }
  return (
    <div style={{ position: "relative" }}
      onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}>
      <div ref={ref} className={"noscroll" + (className ? " " + className : "")}
        style={{ display: "flex", gap, overflowX: "auto", paddingBottom: 3,
          scrollSnapType: snap ? "x proximity" : undefined,
          WebkitMaskImage: mask, maskImage: mask, ...style }}>
        {children}
      </div>
      {arrow("l")}
      {arrow("r")}
    </div>
  )
}

// ── shop brand logo (theme-aware) — the real Narplioju.lt wordmark ──────────
// Renders both ink variants; CSS shows the right one per theme. Used wherever
// the shop should be visible (footer of the estimate, landing, share surfaces).
function ShopLogo({ height = 22, style }) {
  return (
    <span className="brand-logo" style={{ display: "inline-flex", alignItems: "center", lineHeight: 0, ...style }}>
      <img className="logo-dark" src={(window.__resources && window.__resources.logoDark) || "assets/logo.png"} alt="Narplioju.lt"
        style={{ height, width: "auto" }} />
      <img className="logo-light" src={(window.__resources && window.__resources.logoLight) || "assets/logo-light.png"} alt="Narplioju.lt"
        style={{ height, width: "auto" }} />
    </span>
  )
}

const FieldLabel = ({ children, accent, style }) => (
  <span style={{ fontSize: 10.5, letterSpacing: ".09em", textTransform: "uppercase", fontWeight: 700,
    whiteSpace: "nowrap", color: accent ? "var(--accent)" : "var(--muted)", fontFamily: "var(--font-ui)", ...style }}>{children}</span>
)

// ── toast ────────────────────────────────────────────────────────────────────
function useToast() {
  const [msg, setMsg] = useStateB(null)
  const tRef = useRefB(null)
  const show = useCbB((m) => {
    setMsg(m); clearTimeout(tRef.current); tRef.current = setTimeout(() => setMsg(null), 2200)
  }, [])
  const node = msg ? (
    <div style={{ position: "fixed", left: "50%", bottom: 96, transform: "translateX(-50%)", zIndex: 120,
      background: "var(--ink)", color: "var(--card)", padding: "10px 16px", borderRadius: 12, fontSize: 13,
      fontFamily: "var(--font-ui)", fontWeight: 600, boxShadow: "0 8px 30px rgba(20,14,6,.34)",
      display: "flex", alignItems: "center", gap: 8, maxWidth: "86%", pointerEvents: "none" }}>
      <span style={{ color: "var(--ok)", display: "grid", placeItems: "center" }}><Icon.check s={15} /></span>{msg}
    </div>
  ) : null
  return [node, show]
}

// ── share helper: native share (image + text) → download → clipboard ────────
// Tries the richest path the device supports:
//   1. native share sheet WITH the generated image card (phones, Pinterest, etc.)
//   2. native share sheet with text only (older mobile)
//   3. desktop: download the image card + copy the text summary
//   4. last resort: copy text
async function shareSummary({ title, text, file }) {
  try {
    if (file && navigator.canShare && navigator.canShare({ files: [file] })) {
      await navigator.share({ title, text, files: [file] }); return "shared"
    }
  } catch (e) { if (e && e.name === "AbortError") return "cancel" }
  try {
    if (navigator.share) { await navigator.share({ title, text }); return "shared" }
  } catch (e) { if (e && e.name === "AbortError") return "cancel" }
  try {
    if (file) {
      const url = URL.createObjectURL(file)
      const a = document.createElement("a"); a.href = url; a.download = file.name || "skein.png"
      document.body.appendChild(a); a.click(); a.remove()
      setTimeout(() => URL.revokeObjectURL(url), 5000)
      try { await navigator.clipboard.writeText(text) } catch (e) {}
      return "saved"
    }
  } catch (e) {}
  try {
    await navigator.clipboard.writeText(text); return "copied"
  } catch (e) {}
  try {
    const ta = document.createElement("textarea"); ta.value = text
    ta.style.position = "fixed"; ta.style.opacity = "0"; document.body.appendChild(ta); ta.select()
    document.execCommand("copy"); document.body.removeChild(ta); return "copied"
  } catch (e) { return "fail" }
}

// ── share card: render the estimate as a branded PNG for sharing/saving ──────
// Reads the LIVE theme's CSS variables so the card always matches what's on screen,
// and embeds the garment SCHEMATIC illustration so the card looks like the app.
// data: { wordmark, tagline, garment, sizeStr, label, num, unit, sub, yarns:[…],
//         footer, schematicSvg }
async function buildShareCard(data) {
  const root = document.querySelector(".yarn-root") || document.documentElement
  const cs = getComputedStyle(root)
  const v = (n, fb) => (cs.getPropertyValue(n) || "").trim() || fb
  const C = {
    bg: v("--paper-bg", "#F6F0E4"), card: v("--card", "#FBF7EF"),
    ink: v("--ink", "#2C2620"), muted: v("--muted", "#9A8C77"),
    line: v("--line", "#E5DAC7"), lineStrong: v("--line-strong", "#D6C6AC"),
    accent: v("--accent", "#B05C43"), field: v("--field", "#FDFBF5"),
  }
  const fUI = v("--font-ui", "system-ui, sans-serif")
  const fDisp = v("--font-display", "Georgia, serif")
  const hexToRgba = (hex, a) => {
    const h = (hex || "").replace("#", "")
    if (h.length < 6) return `rgba(176,92,67,${a})`
    return `rgba(${parseInt(h.slice(0,2),16)},${parseInt(h.slice(2,4),16)},${parseInt(h.slice(4,6),16)},${a})`
  }

  const S = 2, W = 1080, H = 1350
  const cv = document.createElement("canvas")
  cv.width = W * S; cv.height = H * S
  const ctx = cv.getContext("2d"); ctx.scale(S, S)
  const rr = (x, y, w, h, r) => { ctx.beginPath(); ctx.moveTo(x + r, y);
    ctx.arcTo(x + w, y, x + w, y + h, r); ctx.arcTo(x + w, y + h, x, y + h, r);
    ctx.arcTo(x, y + h, x, y, r); ctx.arcTo(x, y, x + w, y, r); ctx.closePath() }

  try { if (document.fonts) await document.fonts.ready } catch (e) {}

  // rasterise the schematic SVG (plain shapes — safe to draw without tainting)
  let schImg = null
  if (data.schematicSvg) {
    try {
      const url = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(data.schematicSvg)
      const im = new Image()
      im.src = url
      await (im.decode ? im.decode() : new Promise((r) => { im.onload = r; im.onerror = r }))
      if (im.width) schImg = im
    } catch (e) {}
  }

  const M = 84
  // background + subtle paper texture (matches the app's .paper stripes)
  ctx.fillStyle = C.bg; ctx.fillRect(0, 0, W, H)
  ctx.fillStyle = hexToRgba("#000000", 0.018)
  for (let yy = 0; yy < H; yy += 7) ctx.fillRect(0, yy, W, 1)

  // ── header: yarn-ball mark + wordmark, shop tag on the right ────────────────
  ctx.save(); ctx.translate(M + 21, 104)
  ctx.fillStyle = hexToRgba(C.accent, 0.14); ctx.beginPath(); ctx.arc(0, 0, 22, 0, Math.PI * 2); ctx.fill()
  ctx.strokeStyle = C.accent; ctx.lineWidth = 2.4; ctx.globalAlpha = 0.85
  for (let a = -2; a <= 2; a++) { ctx.beginPath(); ctx.ellipse(0, 0, 22, 9, a * 0.5, 0, Math.PI * 2); ctx.stroke() }
  ctx.restore()
  ctx.textBaseline = "alphabetic"; ctx.textAlign = "left"
  ctx.fillStyle = C.ink; ctx.font = `600 46px ${fDisp}`
  ctx.fillText(data.wordmark || "Skein", M + 56, 120)
  ctx.fillStyle = C.muted; ctx.font = `700 24px ${fUI}`; ctx.textAlign = "right"
  ctx.fillText((data.tagline || "narplioju.lt").toUpperCase(), W - M, 116)
  ctx.strokeStyle = C.line; ctx.lineWidth = 2
  ctx.beginPath(); ctx.moveTo(M, 156); ctx.lineTo(W - M, 156); ctx.stroke()

  // ── garment + size, centred ──────────────────────────────────────────────────
  ctx.textAlign = "center"
  ctx.fillStyle = C.ink; ctx.font = `600 62px ${fDisp}`
  ctx.fillText(data.garment || "", W / 2, 250)
  if (data.sizeStr) { ctx.fillStyle = C.muted; ctx.font = `500 30px ${fUI}`
    ctx.fillText(data.sizeStr, W / 2, 298) }

  // ── hero panel: the garment schematic illustration ──────────────────────────
  const hx = M, hy = 336, hw = W - M * 2, hh = 560
  rr(hx, hy, hw, hh, 34); ctx.fillStyle = C.card; ctx.fill()
  ctx.strokeStyle = C.line; ctx.lineWidth = 2; ctx.stroke()
  // paper stripes inside the panel
  ctx.save(); rr(hx, hy, hw, hh, 34); ctx.clip()
  ctx.fillStyle = hexToRgba("#000000", 0.02)
  for (let yy = hy; yy < hy + hh; yy += 7) ctx.fillRect(hx, yy, hw, 1)
  if (schImg) {
    const pad = 64, aw = hw - pad * 2, ah = hh - pad * 2
    const ar = schImg.width / schImg.height
    let dw = aw, dh = dw / ar
    if (dh > ah) { dh = ah; dw = dh * ar }
    ctx.drawImage(schImg, hx + (hw - dw) / 2, hy + (hh - dh) / 2, dw, dh)
  }
  ctx.restore()

  // ── headline number block, centred ──────────────────────────────────────────
  ctx.textAlign = "center"
  ctx.fillStyle = C.muted; ctx.font = `700 26px ${fUI}`
  ctx.fillText((data.label || "Yarn to buy").toUpperCase(), W / 2, 985)
  ctx.fillStyle = C.accent; ctx.font = `800 132px ${fUI}`
  const numStr = String(data.num != null ? data.num : "—")
  const numW = ctx.measureText(numStr).width
  ctx.font = `700 44px ${fUI}`; const unitW = ctx.measureText(" " + (data.unit || "")).width
  const groupX = W / 2 - (numW + unitW) / 2
  ctx.textAlign = "left"
  ctx.fillStyle = C.accent; ctx.font = `800 132px ${fUI}`
  ctx.fillText(numStr, groupX, 1110)
  ctx.fillStyle = C.muted; ctx.font = `700 44px ${fUI}`
  ctx.fillText(" " + (data.unit || ""), groupX + numW, 1110)
  if (data.sub) { ctx.textAlign = "center"; ctx.fillStyle = C.muted; ctx.font = `600 30px ${fUI}`
    ctx.fillText(data.sub, W / 2, 1162) }

  // ── yarn swatch chips (only when >1 yarn) ────────────────────────────────────
  const yarns = data.yarns || []
  if (yarns.length > 1) {
    ctx.font = `600 28px ${fUI}`
    const chips = yarns.map(y => ({ ...y, w: ctx.measureText(y.label).width + 64 }))
    const total = chips.reduce((s, c) => s + c.w, 0) + (chips.length - 1) * 16
    let x = W / 2 - total / 2, cy = 1212
    chips.forEach(c => {
      rr(x, cy, c.w, 48, 24); ctx.fillStyle = C.field; ctx.fill()
      ctx.strokeStyle = C.line; ctx.lineWidth = 1.5; ctx.stroke()
      ctx.fillStyle = c.color || C.accent; ctx.beginPath(); ctx.arc(x + 24, cy + 24, 9, 0, Math.PI * 2); ctx.fill()
      ctx.fillStyle = C.ink; ctx.textAlign = "left"; ctx.textBaseline = "middle"
      ctx.fillText(c.label, x + 42, cy + 25); ctx.textBaseline = "alphabetic"
      x += c.w + 16
    })
  }

  // ── footer ───────────────────────────────────────────────────────────────────
  ctx.fillStyle = C.muted; ctx.font = `700 26px ${fUI}`; ctx.textAlign = "center"
  ctx.fillText((data.footer || "narplioju.lt").toUpperCase(), W / 2, H - 64)

  const blob = await new Promise(res => cv.toBlob(res, "image/png"))
  return blob
}

// ── share menu: per-platform share targets (Pinterest-style) ────────────────
// Opens each service's web share dialog in a new tab with the summary pre-filled.
// Plain links can't attach our generated image (no public per-result URL), so the
// services pull their own preview from the shared narplioju.lt link; "Save image"
// downloads the branded card and "More…" uses the native sheet (image + text).
const SHARE_ICONS = {
  copy: <svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="#fff" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M9 15l6-6M10.5 7.5l1.2-1.2a3.5 3.5 0 015 5l-1.2 1.2M13.5 16.5l-1.2 1.2a3.5 3.5 0 01-5-5l1.2-1.2" /></svg>,
  whatsapp: <svg viewBox="0 0 24 24" width="24" height="24" fill="#fff"><path d="M12 2a10 10 0 00-8.6 15l-1.3 4.7 4.8-1.3A10 10 0 1012 2zm0 1.8a8.2 8.2 0 11-4.3 15.2l-.3-.2-2.8.8.8-2.8-.2-.3A8.2 8.2 0 0112 3.8zm4.7 10.3c-.3-.1-1.5-.7-1.7-.8s-.4-.1-.6.1-.7.8-.8 1-.3.2-.5.1a6.7 6.7 0 01-2-1.2 7.5 7.5 0 01-1.4-1.7c-.1-.3 0-.4.1-.5l.4-.5.3-.4v-.4l-.8-1.9c-.2-.5-.4-.4-.6-.4h-.5a1 1 0 00-.7.3 3 3 0 00-1 2.3 5.3 5.3 0 001.1 2.8c.1.2 1.9 3 4.7 4.2.7.3 1.2.5 1.6.6.7.2 1.3.2 1.8.1.5-.1 1.5-.6 1.7-1.2s.2-1.1.2-1.2z"/></svg>,
  facebook: <svg viewBox="0 0 24 24" width="24" height="24" fill="#fff"><path d="M13.5 21v-8h2.7l.4-3.1h-3.1V7.9c0-.9.3-1.5 1.6-1.5h1.6V3.6A22 22 0 0014.7 3.5c-2.4 0-4 1.5-4 4.1v2.3H8v3.1h2.7V21z"/></svg>,
  x: <svg viewBox="0 0 24 24" width="22" height="22" fill="#fff"><path d="M17.5 3h3l-6.6 7.6L21.8 21h-6.1l-4.8-6.3L5.4 21h-3l7.1-8.1L2.5 3h6.2l4.3 5.7zm-1.1 16.2h1.7L7.7 4.7H5.9z"/></svg>,
  telegram: <svg viewBox="0 0 24 24" width="24" height="24" fill="#fff"><path d="M21.9 4.3l-3 14.2c-.2 1-.8 1.2-1.7.8l-4.6-3.4-2.2 2.1c-.2.2-.4.4-.9.4l.3-4.7 8.5-7.7c.4-.3-.1-.5-.6-.2L7.4 13 2.9 11.6c-1-.3-1-1 .2-1.5L20.6 3c.8-.3 1.5.2 1.3 1.3z"/></svg>,
  email: <svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="#fff" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="M3.5 6.5L12 12.5l8.5-6"/></svg>,
  image: <svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="#fff" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M12 4v9m0 0l-3.2-3.2M12 13l3.2-3.2"/><path d="M5 16v2.5A1.5 1.5 0 006.5 20h11a1.5 1.5 0 001.5-1.5V16"/></svg>,
}
function ShareMenu({ open, onClose, url, text, subject, onCopy, onSaveImage, onNative }) {
  const t = useT()
  const enc = encodeURIComponent
  const full = text + (url ? "\n" + url : "")
  const targets = [
    { key: "copy", label: t("Copy"), bg: "var(--accent)", act: () => { onCopy && onCopy(); onClose() } },
    { key: "whatsapp", label: "WhatsApp", bg: "#25D366", href: `https://wa.me/?text=${enc(full)}` },
    { key: "facebook", label: "Facebook", bg: "#1877F2", href: `https://www.facebook.com/sharer/sharer.php?u=${enc(url || "")}&quote=${enc(text)}` },
    { key: "x", label: "X", bg: "#0A0A0A", href: `https://twitter.com/intent/tweet?text=${enc(text)}&url=${enc(url || "")}` },
    { key: "telegram", label: "Telegram", bg: "#229ED9", href: `https://t.me/share/url?url=${enc(url || "")}&text=${enc(text)}` },
    { key: "email", label: t("Email"), bg: "#7A6CA8", href: `mailto:?subject=${enc(subject || "")}&body=${enc(full)}` },
    { key: "image", label: t("Save image"), bg: "var(--accent-2)", act: () => { onSaveImage && onSaveImage() } },
  ]
  const click = (tg) => {
    if (tg.href) { window.open(tg.href, "_blank", "noopener,noreferrer"); onClose() }
    else if (tg.act) tg.act()
  }
  const canNative = typeof navigator !== "undefined" && !!navigator.share
  return (
    <Sheet open={open} onClose={onClose} title={t("Share")} maxWidth={420}>
      <div style={{ display: "grid", gridTemplateColumns: "repeat(4, 1fr)", gap: 12 }}>
        {targets.map(tg => (
          <button key={tg.key} onClick={() => click(tg)} style={{ background: "transparent", border: "none",
            cursor: "pointer", display: "flex", flexDirection: "column", alignItems: "center", gap: 7, padding: "4px 0",
            fontFamily: "var(--font-ui)" }}>
            <span style={{ width: 54, height: 54, borderRadius: "50%", background: tg.bg, display: "grid",
              placeItems: "center", boxShadow: "0 2px 8px rgba(20,14,6,.18)" }}>{SHARE_ICONS[tg.key]}</span>
            <span style={{ fontSize: 11.5, color: "var(--ink)", fontWeight: 600, textAlign: "center" }}>{tg.label}</span>
          </button>
        ))}
      </div>
      {canNative && (
        <button onClick={() => { onNative && onNative(); }} style={{ marginTop: 4, width: "100%", padding: "12px",
          borderRadius: 12, border: "1px solid var(--line-strong)", background: "var(--field)", color: "var(--ink)",
          fontWeight: 700, fontSize: 13.5, fontFamily: "var(--font-ui)", cursor: "pointer", display: "flex",
          alignItems: "center", justifyContent: "center", gap: 8 }}>
          <Icon.share s={16} />{t("More options…")}
        </button>
      )}
    </Sheet>
  )
}

Object.assign(window, { useMedia, Icon, YarnSwatch, YARN_STRAND, yarnVisual, YarnIndicator, ShopLogo, Sheet, ShareMenu, iconBtn, Segmented, FieldLabel, ScrollRow, useToast, shareSummary, buildShareCard })
