// ── app.jsx ────────────────────────────────────────────────────────────────
// Redesigned Skein shell. Mobile-first, progressive disclosure. Reuses the
// untouched math engine (data.jsx) + schematic visuals (schematic.jsx) and the
// new flow/piece building blocks. The estimate is always visible; depth reveals
// one tap at a time.

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

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "theme": "oatmeal",
  "density": "regular",
  "flow": "guided"
}/*EDITMODE-END*/;

const DENSITY = {
  compact: { stage: 250, pad: 12 },
  regular: { stage: 300, pad: 16 },
  roomy:   { stage: 360, pad: 20 },
}

// ── new-string translations (merged into the existing LT map) ────────────────
const LT_EXTRA = {
  "How much yarn?": "Kiek siūlų reikės?",
  "Pick what you're making. You'll get an estimate right away — fine-tune only if you want to.":
    "Pasirinkite, ką mezgate. Įvertinimą gausite iškart — tikslinkite tik jei norite.",
  "or start from a blank piece": "arba pradėkite nuo tuščios dalies",
  "Custom build": "Savas mezginys", "Making": "Mezgu", "tap to compare": "palieskite palyginti",
  "Using a fuzzy yarn (mohair, alpaca, cashmere…)? We recommend entering the gauge you plan to knit at, or the gauge from the label, in Fine-tune below.":
    "Naudojate pūkuotus siūlus (mohera, alpaka, kašmyras ir pan.)? Rekomenduojame įvesti planuojamą tankumą arba tankumą iš etiketės skiltyje „Tikslinti“ žemiau.",
  "Yarn weight": "Siūlų storis", "My yarn": "Mano siūlai", "from your yarn": "pagal jūsų siūlus",
  "from yarn ({n})": "iš siūlų ({n})",
  "Knit a swatch? Set its gauge here — it's the biggest factor in how much yarn you'll use.":
    "Numezgėte bandinį? Įveskite jo tankį — tai svarbiausias veiksnys skaičiuojant siūlus.",
  "Fine-tune": "Tikslinti", "optional": "nebūtina", "You'll need about": "Jums reikės maždaug", "You'll need": "Jums reikės",
  "of {x}": "iš {x}", "Already have some?": "Jau turite siūlų?",
  "You're covered — {n}m to spare": "Užteks — lieka {n}m", "Buy about {n}m more": "Pirkite dar ~{n}m",
  "Just enough — grab {n}m more for safety": "Vos užteks — saugumui pirkite dar {n}m",
  "covered ✓": "užteks ✓", "+{n}m": "+{n}m", "Pieces": "Dalys", "Details & ball size": "Plačiau ir kamuoliuko dydis",
  "Name": "Pavadinimas", "override": "pakeista", "inherits": "paveldima",
  "Read it off the ball band, or measure a ball you have.": "Nurodyta etiketėje arba pamatuokite turimą kamuoliuką.",
  "Start over": "Pradėti iš naujo", "Share": "Dalintis", "Copied to clipboard": "Nukopijuota į iškarpinę",
  "Shared": "Pasidalinta", "Image saved": "Paveikslėlis išsaugotas", "Add a piece": "Pridėti dalį", "Change project": "Keisti mezginį",
  "Copy": "Kopijuoti", "Email": "El. paštas", "Save image": "Išsaugoti paveikslėlį", "More options…": "Daugiau…",
  "Menu": "Meniu", "Theme": "Tema", "for a": "—", "in": "iš",
  "Yarn estimate": "Siūlų įvertinimas",
  "Flow": "Eiga", "Guided": "Vedama", "Open": "Atvira",
  "tap a piece to edit · drag to move it": "spustelėkite redaguoti · tempkite perkelti",
  "Rename": "Pervadinti", "Reset position": "Atstatyti vietą",
  "Enough to knit it — {n}m short of your spare": "Numegzti užteks — iki atsargos trūksta {n}m",
  "Yarn to buy": "Jums reikės apie",
  "ball": "kamuoliukas", "balls": "kamuoliukai", "100g balls": "100g kamuoliukai",
  "Ball size": "Kamuoliuko dydis", "{g}g balls": "{g}g kamuoliukai",
  "2 strands held together — total single-strand metres": "Dvi gijos kartu — bendras vienos gijos ilgis",
  "Spare": "Atsarga",
  "our estimate — add a little spare with the slider above": "rekomenduojame pridėti papildomai su slankikliu viršuje",
  "+{m}m spare{b}": "+{m}m atsargos{b}",
  "This is the yarn to buy": "Tiek siūlų pirkti",
  "An estimate — real use shifts with tension, blocking and ends. A little spare keeps you from running short.":
    "Tai apytikslis kiekis — tikras kinta dėl tankumo, skalbimo ir blokavimo. Nedidelė atsarga apsaugo, kad nepritrūktų.",
  "Enough to knit the piece at your gauge, plus the spare you add. Real use shifts a little with tension, blocking and weaving in ends — a small spare covers it.":
    "Tiek, kiek reikia numegzti jūsų tankiu, plius pridėta atsarga. Tikras kiekis šiek tiek kinta dėl tankio, skalbimo ir galų užmezgimo — nedidelė atsarga tai padengia.",
  "Sleeve length": "Rankovės ilgis",
  // ── v2 engine UI ──
  "Stitch gauge": "Akių tankumas", "Row gauge": "Eilių tankumas", "estimated": "apytikslis",
  "estimated {n}": "apytiksliai {n}", "rows / 10cm": "eilės / 10cm", "Construction": "Konstrukcija",
  "Knit a swatch? Set its gauge here — it's the biggest factor in how much yarn you'll use.":
    "Numezgėte bandinį? Įveskite jo tankį — tai svarbiausias veiksnys skaičiuojant siūlus.",
  "Rows can't be read from the ball band — we estimate them. A row-dense fabric (lace, slip-stitch) uses more yarn; set yours if you counted it.":
    "Eilučių tankio etiketė nenurodo — jį įvertiname. Tankesnis eilėmis raštas (ažūras, perkeltos akys) naudoja daugiau siūlų; jei skaičiavote, įveskite savo.",
  "How the sleeves join the body. Set-in and raglan sit closest to the body; a circular yoke deepens the upper body, while a drop shoulder swaps shaping for a wider body and roomier sleeves — each spends yarn a little differently.":
    "Kaip rankovės jungiasi prie liemens. Įsiūta ir reglanas glaudžiausi; ketė pailgina viršutinę dalį, o nuleistas petys vietoj formavimo siūlo platesnį liemenį ir laisvesnes rankoves — kiekviena siūlus naudoja kiek kitaip.",
  "Raglan": "Reglanas", "Circular yoke": "Apvali koketė", "Drop shoulder": "Nuleistas petys", "Modified drop": "Pusiau nuleistas",
  "Neckline": "Iškirptė", "None": "Nėra", "Crew": "Apvali", "Scoop": "Gili apvali", "V-neck": "V formos", "Boat": "Laivo",
  "Ball band": "Etiketė", "Held double": "Dviguba gija", "Swatch": "Bandinys",
  "Strand A": "Gija A", "Strand B": "Gija B", "held together": "kartu",
  "≈ {n} sts/10cm at the doubled gauge": "≈ {n} akių/10cm dvigubu tankiu",
  "Unravelled": "Išardyta", "Stitches / 10cm": "Akys / 10cm", "Rows / 10cm": "Eilės / 10cm",
  "Width": "Plotis", "Height": "Aukštis", "From swatch": "Iš bandinio",
  "Two strands held together knit as one yarn. Enter each strand's m/100g — we combine them.":
    "Dvi gijos, laikomos kartu, mezgamos kaip vieni siūlai. Įveskite kiekvienos m/100g — sujungsime.",
  "Most accurate. Knit a swatch, wash & block it, then measure, weigh & unravel it. Captures your real tension and fibre.":
    "Tiksliausia. Numegzkite bandinį, išskalbkite, išmatuokite, pasverkite ir išardykite. Įvertina jūsų tankį ir pluoštą.",
  "Upper circumference": "Viršaus apimtis", "Cuff circumference": "Rankogalio apimtis", "Crown depth": "Viršaus gylis", "Wingspan": "Plotis (galas–galas)",
  "Length (shoulder–hem)": "Ilgis (petys–rankogalis)",
  "Heel": "Kulnas", "Toe": "Pirštai", "Sleeve": "Rankovė", "Crown": "Viršus",
  "Hat body · Sock leg · Cowl": "Kepurės korpusas · Kojinės blauzda · Apykaklė",
  "Sleeve · Tapered tube": "Rankovė · Siaurėjantis vamzdis",
  "Hat top — dome": "Kepurės viršus — kupolas",
  "Heel flap + gusset": "Kulno atvartas + įvaras",
  "Decrease wedge": "Siaurėjimo pleištas",
  "Brim, body & dome crown — worked in the round.": "Kraštas, korpusas ir kupolinis viršus — mezgama ratu.",
  "Cuff down — leg, heel, foot & toe. Pair counted automatically.": "Nuo aukšto — blauzda, kulnas, pėda ir pirštai. Pora skaičiuojama automatiškai.",
  "Body, two tapered sleeves & a ribbed hem.": "Korpusas, dvi siaurėjančios rankovės ir gumytės kraštas.",
}

function Flag({ code }) {
  if (code === "lt") return (
    <svg width="20" height="14" viewBox="0 0 20 14" style={{ borderRadius: 2, display: "block" }} aria-hidden="true">
      <rect width="20" height="14" fill="#FDB913" /><rect y="4.67" width="20" height="4.66" fill="#006A44" />
      <rect y="9.33" width="20" height="4.67" fill="#C1272D" /></svg>)
  return (
    <svg width="20" height="14" viewBox="0 0 60 42" style={{ borderRadius: 2, display: "block" }} aria-hidden="true">
      <clipPath id="ukclip2"><rect width="60" height="42" rx="3" /></clipPath>
      <g clipPath="url(#ukclip2)"><rect width="60" height="42" fill="#012169" />
        <path d="M0,0 L60,42 M60,0 L0,42" stroke="#fff" strokeWidth="8" />
        <path d="M0,0 L60,42 M60,0 L0,42" stroke="#C8102E" strokeWidth="4" />
        <rect x="25" width="10" height="42" fill="#fff" /><rect y="16" width="60" height="10" fill="#fff" />
        <rect x="27" width="6" height="42" fill="#C8102E" /><rect y="18" width="60" height="6" fill="#C8102E" /></g></svg>)
}

function HeaderMenu({ lang, onLang, onSaved, onStartOver, onUndo, canUndo, hasBuild }) {
  const t = useT()
  const [open, setOpen] = useState(false)
  useEffect(() => {
    if (!open) return
    const close = () => setOpen(false)
    window.addEventListener("click", close)
    return () => window.removeEventListener("click", close)
  }, [open])
  return (
    <div style={{ position: "relative" }} onClick={e => e.stopPropagation()}>
      <button onClick={() => setOpen(o => !o)} aria-label={t("Menu")} style={iconBtn}><Icon.menu /></button>
      {open && (
        <div style={{ position: "absolute", right: 0, top: "calc(100% + 8px)", zIndex: 50, width: 220,
          background: "var(--card)", border: "1px solid var(--line-strong)", borderRadius: 14,
          boxShadow: "0 18px 44px rgba(20,14,6,.22)", padding: 8 }}>
          <div style={{ display: "flex", gap: 4, background: "var(--field-dim)", borderRadius: 10, padding: 3, marginBottom: 6 }}>
            {[["en", "English"], ["lt", "Lietuvių"]].map(([c, name]) => (
              <button key={c} onClick={() => onLang(c)} style={{ flex: 1, display: "flex", alignItems: "center",
                justifyContent: "center", gap: 6, padding: "7px 6px", borderRadius: 8, border: "none", cursor: "pointer",
                background: lang === c ? "var(--card)" : "transparent", boxShadow: lang === c ? "0 1px 2px rgba(0,0,0,.12)" : "none",
                opacity: lang === c ? 1 : .6, fontFamily: "var(--font-ui)" }}>
                <Flag code={c} /><span style={{ fontSize: 11.5, fontWeight: 700, color: "var(--ink)" }}>{name}</span></button>
            ))}
          </div>
          {[
            [t("Saved projects"), () => { onSaved(); setOpen(false) }, true],
            canUndo && [t("Undo"), () => { onUndo(); setOpen(false) }, true],
            hasBuild && [t("Start over"), () => { onStartOver(); setOpen(false) }, false],
          ].filter(Boolean).map(([label, fn, keep], i) => (
            <button key={i} onClick={fn} style={{ display: "block", width: "100%", textAlign: "left",
              background: "transparent", border: "none", borderRadius: 9, padding: "10px 10px", fontSize: 13.5,
              color: keep ? "var(--ink)" : "var(--danger)", cursor: "pointer", fontFamily: "var(--font-ui)", fontWeight: 600 }}>
              {label}</button>
          ))}
        </div>
      )}
    </div>
  )
}

function SavedSheet({ open, onClose, projects, onLoad, onDelete, onRename }) {
  const t = useT()
  const [editId, setEditId] = useState(null)
  const [draft, setDraft] = useState("")
  const beginEdit = (p) => { setEditId(p.id); setDraft(p.name) }
  const commit = () => { if (editId != null) onRename(editId, draft); setEditId(null) }
  return (
    <Sheet open={open} onClose={onClose} title={t("Saved projects")} maxWidth={420}>
      {projects.length === 0 && (
        <div style={{ textAlign: "center", color: "var(--muted)", fontSize: 13, padding: "24px 0",
          fontFamily: "var(--font-ui)" }}>{t("No saved projects yet")}</div>
      )}
      {projects.map(p => (
        <div key={p.id} style={{ display: "flex", alignItems: "center", gap: 8, background: "var(--field)",
          border: "1px solid var(--line)", borderRadius: 12, padding: "10px 12px" }}>
          {editId === p.id ? (
            <input autoFocus value={draft} onChange={e => setDraft(e.target.value)} onBlur={commit}
              onKeyDown={e => { if (e.key === "Enter") commit(); if (e.key === "Escape") setEditId(null) }}
              style={{ flex: 1, minWidth: 0, background: "var(--card)", border: "1px solid var(--accent)", borderRadius: 8,
                padding: "8px 10px", fontSize: 14, fontWeight: 700, color: "var(--ink)", outline: "none",
                fontFamily: "var(--font-ui)" }} />
          ) : (
            <button onClick={() => onLoad(p)} style={{ flex: 1, minWidth: 0, textAlign: "left", background: "transparent",
              border: "none", cursor: "pointer", fontFamily: "var(--font-ui)", padding: 0 }}>
              <div style={{ fontSize: 14, color: "var(--ink)", fontWeight: 700, overflow: "hidden", textOverflow: "ellipsis",
                whiteSpace: "nowrap" }}>{p.name}</div>
              <div style={{ fontSize: 11, color: "var(--muted)" }}>{new Date(p.ts).toLocaleDateString()} · {(p.state.blocks || []).length} {t("Pieces").toLowerCase()}</div>
            </button>
          )}
          {editId === p.id ? (
            <button onMouseDown={e => e.preventDefault()} onClick={commit} aria-label={t("Save")}
              style={{ ...iconBtn, width: 34, height: 34, color: "var(--accent)" }}><Icon.check /></button>
          ) : (
            <button onClick={() => beginEdit(p)} aria-label={t("Rename")} style={{ ...iconBtn, width: 34, height: 34,
              color: "var(--muted)" }}><Icon.pencil /></button>
          )}
          <button onClick={() => onDelete(p.id)} aria-label={t("Delete")} style={{ ...iconBtn, width: 34, height: 34,
            color: "var(--muted)" }}><Icon.trash /></button>
        </div>
      ))}
    </Sheet>
  )
}

// Name-on-save dialog: prefilled with a sensible default, editable before commit.
function SaveSheet({ open, onClose, defaultName, onSave }) {
  const t = useT()
  const [name, setName] = useState(defaultName || "")
  useEffect(() => { if (open) setName(defaultName || "") }, [open, defaultName])
  return (
    <Sheet open={open} onClose={onClose} title={t("Save project")} maxWidth={400}
      footer={
        <button onClick={() => onSave(name)} style={{ width: "100%", background: "var(--accent)",
          border: "1px solid var(--accent)", borderRadius: 12, padding: "13px 0", color: "var(--on-accent)",
          fontSize: 14.5, fontWeight: 700, cursor: "pointer", fontFamily: "var(--font-ui)" }}>{t("Save project")}</button>}>
      <label style={{ display: "flex", flexDirection: "column", gap: 7 }}>
        <FieldLabel>{t("Name your project")}</FieldLabel>
        <input autoFocus value={name} onChange={e => setName(e.target.value)}
          onKeyDown={e => { if (e.key === "Enter") onSave(name) }}
          style={{ width: "100%", background: "var(--field)", border: "1px solid var(--line-strong)", borderRadius: 10,
            padding: "12px 12px", fontSize: 16, fontWeight: 600, color: "var(--ink)", outline: "none",
            fontFamily: "var(--font-ui)" }} />
      </label>
    </Sheet>
  )
}

function App() {
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS)
  const [lang, setLang] = useState(() => { try { return localStorage.getItem("skein_lang") || "en" } catch (e) { return "en" } })
  const tr = useCallback((en, vars) => translate(lang, en, vars), [lang])
  const changeLang = useCallback((l) => { setLang(l); try { localStorage.setItem("skein_lang", l) } catch (e) {} }, [])

  const [garmentYarn, setGarmentYarn] = useState("dk")
  const [stash, setStash] = useState("")
  const [buffer, setBuffer] = useState(() => { try { const v = localStorage.getItem("skein_buffer"); return v != null ? Number(v) : 0 } catch (e) { return 0 } })
  const changeBuffer = useCallback((v) => { setBuffer(v); try { localStorage.setItem("skein_buffer", String(v)) } catch (e) {} }, [])
  const [ballGrams, setBallGrams] = useState(() => { try { const v = localStorage.getItem("skein_ballgrams"); return v != null ? Number(v) : 100 } catch (e) { return 100 } })
  const changeBallGrams = useCallback((v) => { const n = Math.max(1, v || 100); setBallGrams(n); try { localStorage.setItem("skein_ballgrams", String(n)) } catch (e) {} }, [])
  const [blocks, setBlocks] = useState([])
  const [lastPreset, setLastPreset] = useState(null)
  const [selectedId, setSelectedId] = useState(null)
  const [size, setSize] = useState(null)
  const [fit, setFit] = useState(null)
  const [customYarn, setCustomYarn] = useState(null)
  const [construction, setConstruction] = useState("set_in")
  const [gaugeOverride, setGaugeOverride] = useState(null)
  const [rowOverride, setRowOverride] = useState(null)

  // UI sheets / panels
  const [showPicker, setShowPicker] = useState(false)
  const [pieceOpen, setPieceOpen] = useState(false)
  const [addOpen, setAddOpen] = useState(false)
  const [breakdownOpen, setBreakdownOpen] = useState(false)
  const [savedOpen, setSavedOpen] = useState(false)
  const [saveOpen, setSaveOpen] = useState(false)
  const [fineOpen, setFineOpen] = useState(false)
  const [customTarget, setCustomTarget] = useState(null)  // null | 'garment' | blockId
  const [toastNode, toast] = useToast()

  const isMobile = useMedia("(max-width: 760px)")

  useEffect(() => {
    if (customYarn) YARN.custom = { ...customYarn, stdSts: customYarn.gauge || customYarn.stdSts }
    else delete YARN.custom
  }, [customYarn])

  // ── persistence ───────────────────────────────────────────────────────────
  const [projects, setProjects] = useState(() => { try { return JSON.parse(localStorage.getItem("skein_projects") || "[]") } catch (e) { return [] } })
  const [justSaved, setJustSaved] = useState(false)
  const restoredRef = useRef(false)
  const snapshot = useCallback(() => ({ blocks, lastPreset, selectedId, size, fit, garmentYarn, stash, customYarn, construction, gaugeOverride, rowOverride }),
    [blocks, lastPreset, selectedId, size, fit, garmentYarn, stash, customYarn, construction, gaugeOverride, rowOverride])
  const restore = useCallback((s) => {
    if (!s) return
    if (s.customYarn) { YARN.custom = { ...s.customYarn }; setCustomYarn(s.customYarn) } else { delete YARN.custom; setCustomYarn(null) }
    setBlocks(s.blocks || []); setLastPreset(s.lastPreset ?? null); setSize(s.size ?? null); setFit(s.fit ?? null)
    setGarmentYarn(s.garmentYarn || "dk"); setStash(s.stash || ""); setConstruction(s.construction || "set_in")
    setGaugeOverride(s.gaugeOverride ?? null); setRowOverride(s.rowOverride ?? null); setSelectedId(null); setShowPicker(false); setPieceOpen(false)
    const maxId = (s.blocks || []).reduce((m, b) => Math.max(m, b.id || 0), 0); setUidFloor(maxId)
  }, [])
  useEffect(() => {
    if (restoredRef.current) return
    restoredRef.current = true
    try { const sess = JSON.parse(localStorage.getItem("skein_session") || "null"); if (sess && sess.blocks && sess.blocks.length) restore(sess) } catch (e) {}
  }, [restore])
  useEffect(() => {
    if (!restoredRef.current) return
    const id = setTimeout(() => { try { localStorage.setItem("skein_session", JSON.stringify(snapshot())) } catch (e) {} }, 250)
    return () => clearTimeout(id)
  }, [snapshot])

  // ── undo ──────────────────────────────────────────────────────────────────
  const stateRef = useRef({}); stateRef.current = { blocks, lastPreset, selectedId, size, fit, garmentYarn, gaugeOverride, rowOverride, construction, customYarn, stash }
  const historyRef = useRef([]); const coalesceRef = useRef({ tag: null, time: 0 })
  const [histLen, setHistLen] = useState(0)
  const record = useCallback((tag) => {
    const now = Date.now(), c = coalesceRef.current
    if (tag && c.tag === tag && now - c.time < 700) { c.time = now; return }
    coalesceRef.current = { tag: tag || null, time: now }
    historyRef.current.push({ ...stateRef.current })
    if (historyRef.current.length > 80) historyRef.current.shift()
    setHistLen(historyRef.current.length)
  }, [])
  const undo = useCallback(() => {
    const h = historyRef.current; if (!h.length) return
    const prev = h.pop(); coalesceRef.current = { tag: null, time: 0 }
    setBlocks(prev.blocks); setLastPreset(prev.lastPreset); setSelectedId(prev.selectedId); setSize(prev.size); setFit(prev.fit)
    setGarmentYarn(prev.garmentYarn ?? "dk"); setGaugeOverride(prev.gaugeOverride ?? null); setRowOverride(prev.rowOverride ?? null)
    setConstruction(prev.construction ?? "set_in"); setStash(prev.stash ?? "")
    if (prev.customYarn) { YARN.custom = { ...prev.customYarn } } 
    setCustomYarn(prev.customYarn ?? null)
    setHistLen(h.length)
  }, [])
  useEffect(() => {
    const onKey = (e) => { if ((e.metaKey || e.ctrlKey) && (e.key === "z" || e.key === "Z") && !e.shiftKey) { e.preventDefault(); undo() } }
    window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey)
  }, [undo])

  // ── estimate ────────────────────────────────────────────────────────────────
  const est = useMemo(() => computeEstimate({ blocks, lastPreset, garmentYarn, customYarn, gaugeOverride, rowOverride, construction }),
    [blocks, lastPreset, garmentYarn, customYarn, gaugeOverride, rowOverride, construction])
  const { effDims, effGauge, effRows, blockMeters, baseTotal, pieces, byYarn, strands } = est
  const baseGauge = baselineGauge(garmentYarn, customYarn)
  const selected = blocks.find(b => b.id === selectedId)
  const selIdx = blocks.findIndex(b => b.id === selectedId)
  const anchorBlock = blocks.find(b => b.isAnchor)

  // ── actions ──────────────────────────────────────────────────────────────────
  const loadPreset = useCallback((key) => {
    record("preset"); setShowPicker(false); setLastPreset(key); setConstruction("set_in"); setGaugeOverride(null); setRowOverride(null)
    const S = SIZING[key]; const sz = S ? S.defaultSize : null; const ft = S ? S.defaultFit : null
    setSize(sz); setFit(ft)
    let made = PRESETS[key].blocks.map(makeBlock); if (S) made = applySizing(made, key, sz, ft)
    setBlocks(made); setSelectedId(null)
  }, [record])
  const changeSize = useCallback((sz) => { record("size"); setSize(sz); setBlocks(prev => applySizing(prev, lastPreset, sz, fit)) }, [record, lastPreset, fit])
  const changeFit = useCallback((ft) => { record("fit"); setFit(ft); setBlocks(prev => applySizing(prev, lastPreset, size, ft)) }, [record, lastPreset, size])
  const changeYarn = useCallback((v) => { record("yarn"); setGarmentYarn(v) }, [record])
  const changeGauge = useCallback((v) => { record("gauge"); setGaugeOverride(v) }, [record])
  const changeRows = useCallback((v) => { record("rows"); setRowOverride(v) }, [record])
  const changeConstruction = useCallback((v) => { record("construction"); setConstruction(v) }, [record])
  const changeStash = useCallback((v) => { record("stash"); setStash(v) }, [record])
  const addBlock = useCallback((type) => {
    record("add"); setAddOpen(false); setShowPicker(false)
    const b = makeBlock({ type }); setBlocks(prev => [...prev, b]); setSelectedId(b.id); setPieceOpen(true)
  }, [record])
  const updateBlock = useCallback((id, field, value) => { record("field:" + field + ":" + id); setBlocks(prev => prev.map(b => b.id === id ? { ...b, [field]: value } : b)) }, [record])
  const updateDim = useCallback((id, key, value) => {
    record("dim:" + id + ":" + key)
    setBlocks(prev => prev.map(b => b.id === id ? { ...b, dims: { ...b.dims, [key]: value }, pinnedDims: { ...b.pinnedDims, [key]: true } } : b))
  }, [record])
  const mirrorAll = useCallback((id, targetId) => {
    record("link:" + id)
    setBlocks(prev => prev.map(b => {
      if (b.id !== id) return b
      if (targetId == null) return { ...b, links: {} }
      const target = prev.find(x => x.id === targetId); const links = {}
      if (target) Object.keys(b.dims).forEach(k => { if (k in target.dims) links[k] = targetId })
      return { ...b, links }
    }))
  }, [record])
  const resetBlock = useCallback((id) => {
    record("reset")
    setBlocks(prev => prev.map(b => {
      if (b.id !== id) return b
      const cleared = { ...b, pinnedDims: {}, pos: null }
      if (SIZING[lastPreset] && b.role) { const [one] = applySizing([cleared], lastPreset, size, fit); return one }
      return { ...cleared, dims: { ...b.defaultDims } }
    }))
  }, [record, lastPreset, size, fit])
  const removeBlock = useCallback((id) => { record("remove"); setBlocks(prev => prev.filter(b => b.id !== id)); setPieceOpen(false); setSelectedId(null) }, [record])
  const moveBlock = useCallback((id, pos) => { record("move:" + id); setBlocks(prev => prev.map(b => b.id === id ? { ...b, pos } : b)) }, [record])
  const resetPos = useCallback((id) => { record("resetpos:" + id); setBlocks(prev => prev.map(b => b.id === id ? { ...b, pos: null } : b)) }, [record])
  const startOver = useCallback(() => { record("clear"); setBlocks([]); setLastPreset(null); setSelectedId(null); setConstruction("set_in"); setGaugeOverride(null); setRowOverride(null); setShowPicker(false) }, [record])

  // custom yarn
  const applyCustomYarn = useCallback((obj) => {
    record("yarn"); YARN.custom = { ...obj }; setCustomYarn(obj)
    if (customTarget === "garment") setGarmentYarn("custom")
    else if (customTarget != null) setBlocks(prev => prev.map(b => b.id === customTarget ? { ...b, yarnOverride: "custom" } : b))
    setCustomTarget(null)
  }, [customTarget])

  // selection → open piece editor
  const openPiece = useCallback((id) => { setSelectedId(id); setPieceOpen(true); setBreakdownOpen(false) }, [])
  const closePiece = useCallback(() => { setPieceOpen(false); setSelectedId(null) }, [])

  // save / share
  const defaultProjectName = useCallback(() => (lastPreset ? tr(PRESETS[lastPreset].label) : tr("Custom build")) + (size ? " · " + tr(size) : ""), [lastPreset, size, tr])
  const commitSave = useCallback((name) => {
    const proj = { id: Date.now(), name: (name || "").trim() || defaultProjectName(), ts: Date.now(), state: snapshot() }
    setProjects(prev => { const next = [proj, ...prev].slice(0, 24); try { localStorage.setItem("skein_projects", JSON.stringify(next)) } catch (e) {}; return next })
    setSaveOpen(false); setJustSaved(true); setTimeout(() => setJustSaved(false), 1700); toast(tr("Saved"))
  }, [snapshot, defaultProjectName, tr, toast])
  const renameProject = useCallback((id, name) => {
    setProjects(prev => { const next = prev.map(p => p.id === id ? { ...p, name: (name || "").trim() || p.name } : p); try { localStorage.setItem("skein_projects", JSON.stringify(next)) } catch (e) {}; return next })
  }, [])
  const loadProject = useCallback((p) => { restore(p.state); setSavedOpen(false) }, [restore])
  const deleteProject = useCallback((id) => { setProjects(prev => { const next = prev.filter(p => p.id !== id); try { localStorage.setItem("skein_projects", JSON.stringify(next)) } catch (e) {}; return next }) }, [])

  const [shareOpen, setShareOpen] = useState(false)

  // assemble the summary text + the branded image card for any share path
  const buildShareText = useCallback(() => {
    const garment = lastPreset ? tr(PRESETS[lastPreset].label) : tr("Custom build")
    const sizeStr = size ? `${tr(size)} · ${tr(fit)}` : ""
    const spareFrac = Math.max(0, Math.min(25, buffer ?? 0)) / 100
    const buy = buyFromYarns(byYarn, spareFrac, ballGrams)
    const lines = [`🧶 ${tr("Yarn to buy")} — ${garment}${sizeStr ? " (" + sizeStr + ")" : ""}`,
      `${buy.balls} × ${ballGrams}g ${buy.balls === 1 ? tr("ball") : tr("balls")} ≈ ${buy.metres} m · ${buy.grams} g`]
    const yarnRows = byYarn.map(({ key, metres, basis }) => {
      const mm = metres * (1 + spareFrac)
      const g = basis ? Math.round(mm * 100 / basis) : 0
      const balls = basis ? Math.ceil(mm / (basis * ballGrams / 100)) : 0
      const label = key === "custom" ? tr("My yarn") : YARN[key].label
      lines.push(`• ${label}: ${Math.round(mm)} m ≈ ${g} g`)
      return { label, color: YARN_STRAND[key] || (YARN[key] && YARN[key].color) || undefined,
        detail: `${Math.round(mm)} m · ${balls}×${ballGrams}g` }
    })
    return { garment, sizeStr, buy, yarnRows, text: lines.join("\n"),
      subject: "Narplioju.lt · " + garment, url: "https://narplioju.lt" }
  }, [byYarn, buffer, ballGrams, lastPreset, size, fit, tr])

  const makeCardFile = useCallback(async (d) => {
    try {
      // grab the live garment schematic so the card shows the actual illustration
      let schematicSvg = null
      const svgEl = document.querySelector(".paper svg")
      if (svgEl) {
        const c = svgEl.cloneNode(true)
        // drop the link-group badges (⛓) — their color uses a CSS var that won't resolve
        c.querySelectorAll("text").forEach(tn => {
          if ((tn.textContent || "").includes("⛓")) { (tn.closest("g") || tn).remove() }
        })
        const vb = (c.getAttribute("viewBox") || "").split(/\s+/).map(Number)
        if (vb.length === 4 && vb[2] > 0) {
          const ww = 680; c.setAttribute("width", ww); c.setAttribute("height", Math.round(ww * vb[3] / vb[2]))
        }
        try { schematicSvg = new XMLSerializer().serializeToString(c) } catch (e) {}
      }
      const blob = await buildShareCard({
        wordmark: "Skein", tagline: "narplioju.lt", garment: d.garment, sizeStr: d.sizeStr,
        label: tr("Yarn to buy"), num: d.buy.metres, unit: "m",
        sub: `${d.buy.balls} × ${ballGrams}g ${d.buy.balls === 1 ? tr("ball") : tr("balls")} · ${d.buy.grams} g`,
        yarns: d.yarnRows.length > 1 ? d.yarnRows : [], footer: "narplioju.lt",
        schematicSvg,
      })
      return blob ? new File([blob], "skein-yarn.png", { type: "image/png" }) : null
    } catch (e) { return null }
  }, [tr, ballGrams])

  // the Share button now opens the platform menu
  const doShare = useCallback(() => setShareOpen(true), [])

  const shareCopy = useCallback(async () => {
    const d = buildShareText()
    try { await navigator.clipboard.writeText(d.text + "\n" + d.url); toast(tr("Copied to clipboard")) }
    catch (e) {
      try {
        const ta = document.createElement("textarea"); ta.value = d.text + "\n" + d.url
        ta.style.position = "fixed"; ta.style.opacity = "0"; document.body.appendChild(ta); ta.select()
        document.execCommand("copy"); document.body.removeChild(ta); toast(tr("Copied to clipboard"))
      } catch (e2) {}
    }
  }, [buildShareText, tr, toast])

  const shareSaveImage = useCallback(async () => {
    const d = buildShareText()
    const file = await makeCardFile(d)
    if (!file) return
    const url = URL.createObjectURL(file)
    const a = document.createElement("a"); a.href = url; a.download = file.name
    document.body.appendChild(a); a.click(); a.remove()
    setTimeout(() => URL.revokeObjectURL(url), 5000)
    toast(tr("Image saved")); setShareOpen(false)
  }, [buildShareText, makeCardFile, tr, toast])

  const shareNative = useCallback(async () => {
    const d = buildShareText()
    const file = await makeCardFile(d)
    const res = await shareSummary({ title: d.subject, text: d.text, file })
    if (res === "shared") toast(tr("Shared"))
    else if (res === "saved") toast(tr("Image saved"))
    else if (res === "copied") toast(tr("Copied to clipboard"))
    setShareOpen(false)
  }, [buildShareText, makeCardFile, tr, toast])

  const d = DENSITY[t.density] || DENSITY.regular
  const guided = t.flow === "guided"
  const hasSize = !!SIZING[lastPreset]

  // shared estimate panel content
  const resultProps = {
    pieces, baseTotal, strands, byYarn, garmentYarn, stash, onStash: changeStash, selectedId,
    buffer, onBuffer: changeBuffer,
    ballGrams, onBallGrams: changeBallGrams,
    onSelect: openPiece, onShare: doShare, onSave: () => setSaveOpen(true), onAddPiece: () => setAddOpen(true), justSaved,
  }

  const schematicCard = (
    <div className="card" style={{ background: "var(--card)", border: "1px solid var(--line)", borderRadius: 18,
      padding: d.pad, display: "flex", flexDirection: "column", gap: 10, overflow: "hidden" }}>
      <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
        <GarmentChip presetKey={lastPreset} onClick={() => setShowPicker(true)} />
        <button onClick={() => setAddOpen(true)} title={tr("Add a piece")}
          style={{ marginLeft: "auto", display: "inline-flex", alignItems: "center", gap: 6, background: "var(--field)",
            border: "1px solid var(--line-strong)", borderRadius: 11, color: "var(--ink)", padding: "9px 12px",
            fontSize: 12.5, fontWeight: 700, cursor: "pointer", fontFamily: "var(--font-ui)" }}>
          <Icon.plus s={15} />{!isMobile && tr("Add piece")}</button>
      </div>
      <div className="paper" style={{ borderRadius: 14, padding: "12px 8px", display: "grid", placeItems: "center", minHeight: d.stage }}>
        <GarmentSchematic presetKey={lastPreset} blocks={blocks} effDims={effDims} selectedId={selectedId}
          onSelect={(id) => id == null ? setSelectedId(null) : openPiece(id)} density={t.density} height={d.stage}
          draggable onMove={moveBlock} />
      </div>
      <div style={{ fontSize: 11, color: "var(--muted)", textAlign: "center", fontFamily: "var(--font-ui)", fontStyle: "italic" }}>
        {tr("tap a piece to edit · drag to move it")}</div>
    </div>
  )

  // gating for guided flow: numbered steps + fine-tune expanded in "open" mode
  const fineEffectiveOpen = guided ? fineOpen : true
  let stepN = 0
  const buildSections = (
    <>
      {hasSize && <SizeFit presetKey={lastPreset} size={size} fit={fit} onSize={changeSize} onFit={changeFit}
        blocks={blocks} effDims={effDims} step={guided ? ++stepN : null} />}
      <YarnRow value={garmentYarn} customYarn={customYarn} step={guided ? ++stepN : null}
        onChange={changeYarn} onCustom={() => { setCustomTarget("garment") }} />
      <FineTune presetKey={lastPreset} effGauge={effGauge} baseGauge={baseGauge} gaugeOverride={gaugeOverride}
        onGauge={changeGauge} effRows={effRows} rowOverride={rowOverride} onRows={changeRows}
        construction={construction} onConstruction={changeConstruction}
        step={guided ? ++stepN : null}
        open={fineEffectiveOpen} onToggle={() => setFineOpen(o => !o)} />
    </>
  )

  return (
    <LangCtx.Provider value={lang}>
    <div data-theme={t.theme} className="yarn-root" style={{ minHeight: "100vh", background: "var(--bg)", color: "var(--ink)",
      display: "flex", flexDirection: "column" }}>

      {/* header */}
      <header style={{ position: "sticky", top: 0, zIndex: 40, background: "var(--bg-bar)",
        borderBottom: "1px solid var(--line)", backdropFilter: "blur(8px)", WebkitBackdropFilter: "blur(8px)" }}>
        <div style={{ maxWidth: 1120, margin: "0 auto", padding: "9px 14px", display: "flex", alignItems: "center", gap: 10 }}>
          <button onClick={() => blocks.length && setShowPicker(true)} style={{ display: "flex", alignItems: "center", gap: 9,
            marginRight: "auto", background: "transparent", border: "none", padding: 0, cursor: blocks.length ? "pointer" : "default" }}>
            <span className="header-ball"><img src={(window.__resources && window.__resources.yarnBall) || "assets/yarn-ball.png"} alt="" width="28" height="28" style={{ display: "block" }} /></span>
            <span style={{ fontFamily: "var(--font-display)", fontSize: 19, color: "var(--ink)" }}>Skein
              <span style={{ color: "var(--muted)", fontSize: 13, fontStyle: "italic" }}> · {tr("yarn estimator")}</span></span>
          </button>
          <HeaderMenu lang={lang} onLang={changeLang} onSaved={() => setSavedOpen(true)} onStartOver={startOver}
            onUndo={undo} canUndo={histLen > 0} hasBuild={blocks.length > 0} />
        </div>
      </header>

      {/* body */}
      {blocks.length === 0 ? (
        <div style={{ flex: 1 }}>
          <GarmentPicker onPick={loadPreset} onBlank={() => setAddOpen(true)} />
        </div>
      ) : isMobile ? (
        <>
          <main style={{ flex: 1, maxWidth: 640, width: "100%", margin: "0 auto", padding: "14px 14px 24px",
            display: "flex", flexDirection: "column", gap: 12 }}>
            {schematicCard}
            {buildSections}
          </main>
          <div style={{ position: "sticky", bottom: 0, zIndex: 30 }}>
            <ResultBar baseTotal={baseTotal} byYarn={byYarn} buffer={buffer} ballGrams={ballGrams} stash={stash} onExpand={() => setBreakdownOpen(true)} />
          </div>
          <Sheet open={breakdownOpen} onClose={() => setBreakdownOpen(false)} title={tr("Yarn estimate")}>
            <ResultContent {...resultProps} />
          </Sheet>
        </>
      ) : (
        <main style={{ flex: 1, maxWidth: 1120, width: "100%", margin: "0 auto", padding: "18px 18px 28px",
          display: "grid", gridTemplateColumns: "minmax(0,1fr) 380px", gap: 18, alignItems: "start" }}>
          <div style={{ display: "flex", flexDirection: "column", gap: 14 }}>
            {schematicCard}
            {buildSections}
          </div>
          <aside style={{ position: "sticky", top: 78 }}>
            <div className="card" style={{ background: "var(--card)", border: "1px solid var(--line)", borderRadius: 18, padding: 18 }}>
              <ResultContent {...resultProps} />
            </div>
          </aside>
        </main>
      )}

      {/* sheets */}
      <Sheet open={showPicker} onClose={() => setShowPicker(false)} title={tr("Change project")} maxWidth={560}>
        <GarmentPicker embedded onPick={loadPreset} onBlank={() => { setShowPicker(false); setAddOpen(true) }} />
      </Sheet>
      <Sheet open={addOpen} onClose={() => setAddOpen(false)} title={tr("Add a piece")} maxWidth={420}>
        <AddPieceList onAdd={addBlock} />
      </Sheet>
      <Sheet open={pieceOpen && !!selected} onClose={closePiece}
        title={selected ? tr(selected.label || PRIMITIVES[selected.type].label) : ""} maxWidth={440}>
        {selected && (
          <PieceEditor block={selected} blocks={blocks} effectiveDims={selIdx >= 0 ? effDims[selIdx] : {}}
            garmentYarn={garmentYarn} meters={selIdx >= 0 ? blockMeters[selIdx] : 0}
            anchorLabel={anchorBlock ? (anchorBlock.label || PRIMITIVES[anchorBlock.type].label) : "anchor"}
            isSized={hasSize} sizeName={size}
            onUpdate={(f, v) => updateBlock(selectedId, f, v)} onDim={(k, v) => updateDim(selectedId, k, v)}
            onRemove={() => removeBlock(selectedId)} onReset={() => resetBlock(selectedId)}
            onMirrorAll={(tid) => mirrorAll(selectedId, tid)} hasPos={!!(selected && selected.pos)} onResetPos={() => resetPos(selectedId)}
            onCustomYarn={() => { setCustomTarget(selectedId); }} />
        )}
      </Sheet>
      <SavedSheet open={savedOpen} onClose={() => setSavedOpen(false)} projects={projects} onLoad={loadProject} onDelete={deleteProject} onRename={renameProject} />
      <SaveSheet open={saveOpen} onClose={() => setSaveOpen(false)} defaultName={defaultProjectName()} onSave={commitSave} />
      <CustomYarnSheet open={customTarget != null} onClose={() => setCustomTarget(null)} onSave={applyCustomYarn} />
      {(() => { const d = shareOpen ? buildShareText() : null; return (
        <ShareMenu open={shareOpen} onClose={() => setShareOpen(false)}
          url={d ? d.url : ""} text={d ? d.text : ""} subject={d ? d.subject : ""}
          onCopy={shareCopy} onSaveImage={shareSaveImage} onNative={shareNative} />
      ) })()}

      {toastNode}

      <TweaksPanel>
        <TweakSection label={tr("Flow")} />
        <TweakRadio label={tr("Flow")} value={t.flow} options={[{ value: "guided", label: tr("Guided") }, { value: "open", label: tr("Open") }]}
          onChange={(v) => setTweak("flow", v)} />
        <TweakSection label={tr("Style")} />
        <TweakRadio label={tr("Theme")} value={t.theme} options={["oatmeal", "linen", "charcoal"]} onChange={(v) => setTweak("theme", v)} />
        <TweakSection label={tr("Layout")} />
        <TweakRadio label={tr("Density")} value={t.density} options={["compact", "regular", "roomy"]} onChange={(v) => setTweak("density", v)} />
      </TweaksPanel>
    </div>
    </LangCtx.Provider>
  )
}

// merge new translations before first render
Object.assign(LT, LT_EXTRA)
ReactDOM.createRoot(document.getElementById("root")).render(<App />)
