// ── data.jsx ───────────────────────────────────────────────────────────────
// Yarn weights, construction modifiers, shape primitives, garment presets,
// the meterage calc, and the anatomical layout engine.
// Everything is exported to window at the bottom for cross-file use.
//
// CALIBRATION SOURCES (Narplioju_Calibration_v7.xlsx)
//   All yarn length flows through the FLAT-SWATCH density curve in geometry.jsx
//   (metresPerCm2Flat, fit to weighed swatches). Every garment is summed from its
//   pieces' areas × that density — no whole-garment regression engines remain.
//   Construction modifiers: Hiatt (2012), Melville (2013), EasiestHat YFY,
//     Anthology Hats/Cowls, Aura Top — empirically confirmed values.

// ── Yarn weights ────────────────────────────────────────────────────────────
// stdSts:   standard stitches per 10cm for this weight — default gauge input.
// mPer100g: typical metres per 100g — converts metres → grams → ball count.
// (No gPerCm2: fabric length comes from gauge via metresPerCm2Flat, or from a
//  weighed-swatch mPerCm2 override; weight comes from mPer100g. Audit C3.)
const YARN = {
  lace:       { mPer100g: 625, stdSts: 32, label: "Lace",        sub: "625 m / 100g" },
  fingering:  { mPer100g: 450, stdSts: 28, label: "Fingering",   sub: "450 m / 100g" },
  sport:      { mPer100g: 320, stdSts: 25, label: "Sport",       sub: "320 m / 100g" },
  dk:         { mPer100g: 250, stdSts: 22, label: "DK",          sub: "250 m / 100g" },
  worsted:    { mPer100g: 200, stdSts: 20, label: "Worsted",     sub: "200 m / 100g" },
  bulky:      { mPer100g: 120, stdSts: 15, label: "Bulky",       sub: "120 m / 100g" },
  superbulky: { mPer100g:  80, stdSts: 10, label: "Super Bulky", sub: "80 m / 100g"  },
}

// ── Construction modifiers ──────────────────────────────────────────────────
// Calibrated values — do not edit without updating the calibration workbook.
// Sources documented in Methodology sheet.
//   stockinette  1.00  baseline
//   garter       1.38  DROPS Puna 40×40-st swatch: measured 1.385× stockinette (was 1.10, EasiestHat); vertical row compression
//   rib_elastic  1.25  Hiatt: 30-40% horizontal compression, elastic yarns
//                      (was rib_1x1: 0.97 — wrong direction)
//   rib_relaxed  1.02  same compression, inelastic fibres (cotton, linen)
//                      (was rib_2x2: 0.95 — wrong direction)
//   colorwork    1.55  Anthology hats/cowls imply 1.5–1.9 by intensity; 1.55 is the
//                      MAPE-minimising central value (was 1.35 — too low for stranded)
//   lace_stitch  0.95  Hiatt/Melville: YOs cancel decreases, slight under-gauge
//                      (was 0.90 — too aggressive)
//   cables       1.45  Melville: 26% horizontal compression confirmed
//   brioche      1.35  Hiatt: row-compressed, spans two rows per visual stitch
//   open_knit    0.20  Aura Top (ROWS Knitwear) empirical: 0.189–0.197
//                      OOS validation (Lana Gatto): +46% error without modifier
//   textured     1.08  unchanged — insufficient new data
const CONSTRUCTION = {
  stockinette: { label: "Stockinette",          mod: 1.00 },
  garter:      { label: "Garter",               mod: 1.38, hint: "Ripsinis raštas" },
  rib_elastic: { label: "Elastic Rib",          mod: 1.25, hint: "Merino, vilna" },
  rib_relaxed: { label: "Relaxed Rib",          mod: 1.02, hint: "Medvilnė, linas" },
  textured:    { label: "Textured",             mod: 1.08 },
  cables:      { label: "Cables",               mod: 1.45, hint: "Pynės" },
  colorwork:   { label: "Colorwork",            mod: 1.55, hint: "Žakardinis mezgimas" },
  brioche:     { label: "Brioche",              mod: 1.35, hint: "Patentinis raštas" },
  lace_stitch: { label: "Lace",                 mod: 0.95, hint: "Ažūrinis raštas" },
  open_knit:   { label: "Open Knit (Mohair)",   mod: 0.20, hint: "Mohera ant didelių virbalų" },
}

// ── Stash check ────────────────────────────────────────────────────────────────
// Compares a stash against the single "yarn to buy" figure. `buyTotal` is that
// figure (flat-swatch metres × (1 + spare)); `knitTotal` is the 0-spare amount —
// exactly enough to knit at gauge. Three states:
//   covered — stash ≥ buyTotal (full chosen spare margin in hand)
//   tight   — knitTotal ≤ stash < buyTotal (enough to knit, less than the margin)
//   short   — stash < knitTotal (genuinely not enough)
function stashStatus(buyTotal, knitTotal, stash) {
  const s = parseInt(stash) || 0
  if (!buyTotal || s <= 0) return { state: "none", buy: 0, spare: 0 }
  if (s >= buyTotal)  return { state: "covered", spare: s - buyTotal, buy: 0 }
  if (s >= knitTotal) return { state: "tight",   spare: 0, buy: buyTotal - s }
  return { state: "short", spare: 0, buy: buyTotal - s }
}

// Each primitive is an individual geometric shape. Its area (cm²) comes from the
// AREA table in model.jsx — the v2 engine sums these and multiplies by the
// fabric's metres_per_cm². No primitive snaps to a garment-level estimate.
const PRIMITIVES = {
  rectangle: {
    label: "Panel", color: "#B05C43", symbol: "▭",
    desc: "Body · Scarf · Front/Back",
    dims: [
      { key: "width",  label: "Width",  unit: "cm", default: 20,  min: 5,  max: 250 },
      { key: "length", label: "Length", unit: "cm", default: 180, min: 10, max: 500 },
    ],
    area: d => AREA.rectangle(d),
  },
  tube: {
    label: "Tube", color: "#5E7C8B", symbol: "⬭",
    desc: "Hat body · Sock leg · Cowl",
    dims: [
      { key: "circumference", label: "Circumference", unit: "cm", default: 50, min: 15, max: 140 },
      { key: "depth",         label: "Depth",         unit: "cm", default: 22, min: 2,  max: 80  },
    ],
    area: d => AREA.tube(d),
  },
  // Tapered tube — sleeve / sock-foot. circumference = the WIDE end (upper arm),
  // cuff = the NARROW end (wrist). The taper is the cuff:circumference ratio.
  cone: {
    label: "Sleeve", color: "#5E7C8B", symbol: "◣",
    desc: "Sleeve · Tapered tube",
    dims: [
      { key: "circumference", label: "Upper circumference", unit: "cm", default: 32, min: 14, max: 70 },
      { key: "cuff",          label: "Cuff circumference",  unit: "cm", default: 20, min: 8,  max: 60 },
      { key: "depth",         label: "Length",      unit: "cm", default: 44, min: 10, max: 75 },
    ],
    area: d => AREA.cone(d),
  },
  // Hat crown — spherical-cap dome: π(r² + crown_depth²). Crown depth is a real,
  // adjustable dimension (Build Plan §3.3) — no more fixed crown shape.
  dome: {
    label: "Crown", color: "#C18A3A", symbol: "◠",
    desc: "Hat top — dome",
    dims: [
      { key: "circumference", label: "Circumference", unit: "cm", default: 50, min: 15, max: 80 },
      { key: "depth",         label: "Crown depth",   unit: "cm", default: 8,  min: 2,  max: 18 },
    ],
    area: d => AREA.dome(d),
  },
  triangle: {
    label: "Triangle", color: "#7C8A5E", symbol: "▽",
    desc: "Triangular shawl",
    dims: [
      { key: "base",   label: "Wingspan", unit: "cm", default: 165, min: 30, max: 400 },
      { key: "height", label: "Depth",    unit: "cm", default: 66,  min: 10, max: 200 },
    ],
    area: d => AREA.triangle(d),
  },
  ribband: {
    label: "Rib Band", color: "#9C5468", symbol: "≡",
    desc: "Brim · Cuff · Hem · Waistband",
    defaultConstruction: "rib_elastic",
    dims: [
      { key: "circumference", label: "Circumference", unit: "cm", default: 50, min: 15, max: 160 },
      { key: "depth",         label: "Depth",         unit: "cm", default: 7,  min: 1,  max: 30  },
    ],
    area: d => AREA.ribband(d),
  },
  // Sock heel — flap + heel-turn + gusset wedge, sized from circumference. A
  // dedicated shape, NOT a crown (Build Plan §3.4).
  heel: {
    label: "Heel", color: "#9C5468", symbol: "⊿",
    desc: "Heel flap + gusset",
    dims: [
      { key: "circumference", label: "Circumference", unit: "cm", default: 22, min: 12, max: 34 },
    ],
    area: d => AREA.heel(d),
  },
  // Sock toe — paired decrease wedge, sized from circumference.
  toe: {
    label: "Toe", color: "#C18A3A", symbol: "◢",
    desc: "Decrease wedge",
    dims: [
      { key: "circumference", label: "Circumference", unit: "cm", default: 22, min: 12, max: 34 },
    ],
    area: d => AREA.toe(d),
  },
  overhead: {
    label: "Fixed", color: "#9A8E7C", symbol: "✦",
    desc: "Pompom · Tassel · Fringe",
    dims: [], area: () => 0, isFixed: true,
  },
}

// ── Presets ──────────────────────────────────────────────────────────────────
const PRESETS = {
  hat: {
    label: "Hat", glyph: "hat",
    blurb: "Brim, body & dome crown — worked in the round.",
    blocks: [
      { type: "ribband", label: "Brim", role: "hat-brim",
        dims: { circumference: 50, depth: 5 }, construction: "rib_elastic",
        ranges: { circumference: [30, 62], depth: [2, 12] },
        followsAnchorDim: "circumference" },
      { type: "tube", label: "Body", role: "hat-body",
        dims: { circumference: 50, depth: 11 }, construction: "stockinette",
        ranges: { circumference: [30, 62], depth: [6, 28] },
        isAnchor: true },
      { type: "dome", label: "Crown", role: "hat-crown",
        dims: { circumference: 50, depth: 7 }, construction: "stockinette",
        ranges: { circumference: [30, 62], depth: [3, 16] },
        followsAnchorDim: "circumference" },
    ],
  },
  scarf: {
    label: "Scarf", glyph: "scarf",
    blurb: "One long worked panel.",
    blocks: [
      { type: "rectangle", label: "Body", role: "scarf-body",
        dims: { width: 20, length: 180 }, construction: "stockinette",
        ranges: { width: [10, 60], length: [50, 400] } },
    ],
  },
  socks: {
    label: "Socks", glyph: "socks",
    blurb: "Cuff down — leg, heel, foot & toe. Pair counted automatically.",
    blocks: [
      { type: "ribband", label: "Cuff", role: "sock-cuff",
        dims: { circumference: 22, depth: 5 }, construction: "rib_elastic",
        ranges: { circumference: [14, 32], depth: [2, 8] },
        quantity: 2, isAnchor: true },
      { type: "tube", label: "Leg", role: "sock-leg",
        dims: { circumference: 22, depth: 16 }, construction: "stockinette",
        ranges: { circumference: [14, 32], depth: [5, 40] },
        quantity: 2, followsAnchorDim: "circumference" },
      { type: "heel", label: "Heel", role: "sock-heel",
        dims: { circumference: 22 }, construction: "stockinette",
        ranges: { circumference: [14, 32] },
        quantity: 2, followsAnchorDim: "circumference" },
      { type: "tube", label: "Foot", role: "sock-foot",
        dims: { circumference: 22, depth: 22 }, construction: "stockinette",
        ranges: { circumference: [14, 32], depth: [12, 32] },
        quantity: 2, followsAnchorDim: "circumference" },
      { type: "toe", label: "Toe", role: "sock-toe",
        dims: { circumference: 22 }, construction: "stockinette",
        ranges: { circumference: [14, 32] },
        quantity: 2, followsAnchorDim: "circumference" },
    ],
  },
  sweater: {
    label: "Sweater", glyph: "sweater",
    blurb: "Body, two tapered sleeves & a ribbed hem.",
    blocks: [
      { type: "rectangle", label: "Body##sweater", role: "sweater-body",
        dims: { width: 97, length: 60 }, construction: "stockinette",
        dimLabels: { width: "Circumference", length: "Length (shoulder–hem)" },
        ranges: { width: [60, 180], length: [20, 90] }, isAnchor: true,
        neckType: "crew", neckW: null, neckD: null },
      { type: "cone", label: "Sleeve", role: "sweater-sleeve",
        dims: { circumference: 33, cuff: 19, depth: 44 }, construction: "stockinette",
        dimLabels: { depth: "Sleeve length" },
        ranges: { circumference: [20, 55], cuff: [12, 44], depth: [30, 65] },
        quantity: 2 },
      { type: "ribband", label: "Hem", role: "sweater-hem",
        dims: { circumference: 97, depth: 5 }, construction: "rib_elastic",
        ranges: { circumference: [60, 180], depth: [2, 10] },
        followsAnchorDim: "circumference", followsAnchorKey: "width" },
    ],
  },
  shawl: {
    label: "Shawl", glyph: "shawl",
    blurb: "A single triangular body.",
    blocks: [
      { type: "triangle", label: "Body", role: "shawl-body",
        dims: { base: 165, height: 66 }, construction: "stockinette",
        ranges: { base: [60, 350], height: [20, 150] }, isAnchor: true },
    ],
  },
}

// ── Sizing engine ─────────────────────────────────────────────────────────────
const SIZING = {
  sweater: {
    measureLabel: "Bust", finishedLabel: "finished chest",
    finishedRole: "sweater-body", finishedKey: "width",
    sizes: ["XS", "S", "M", "L", "XL", "2XL", "3XL"],
    base: { XS: 76, S: 81, M: 91, L: 101, XL: 112, "2XL": 122, "3XL": 132 },
    defaultSize: "M",
    fits: { Fitted: 5, Classic: 15, Relaxed: 25 }, defaultFit: "Fitted",
    derive: (C) => ({
      "sweater-body":   { width: Math.round(C), length: Math.round(0.36 * C + 21) },
      "sweater-sleeve": { circumference: Math.round(0.34 * C), cuff: Math.round(0.20 * C), depth: Math.round(44 + (C - 95) * 0.06) },
      "sweater-hem":    { circumference: Math.round(C), depth: 6 },
    }),
  },
  hat: {
    measureLabel: "Head", finishedLabel: "finished circumference",
    finishedRole: "hat-body", finishedKey: "circumference",
    sizes: ["Baby", "Toddler", "Child", "Adult S", "Adult M", "Adult L"],
    base: { Baby: 40, Toddler: 48, Child: 52, "Adult S": 54, "Adult M": 57, "Adult L": 59 },
    defaultSize: "Adult M",
    // ease in cm relative to head. A fitted beanie wants ~7–10% negative ease;
    // c = head + ease (audit A7 — the old ×0.82 baked in ~21% which under-predicted).
    fits: { Snug: -6, Fitted: -4, Slouchy: 2 }, defaultFit: "Fitted",
    derive: (H) => {
      const c = Math.round(H)                       // finished circumference (ease already applied)
      return {
        "hat-brim":  { circumference: c, depth: Math.max(4, Math.round(c * 0.10)) },
        "hat-body":  { circumference: c, depth: Math.round(c * 0.20) },
        "hat-crown": { circumference: c, depth: Math.round(c * 0.14) },
      }
    },
  },
  socks: {
    measureLabel: "Foot", finishedLabel: "finished circumference",
    finishedRole: "sock-leg", finishedKey: "circumference",
    sizes: ["Child", "Women S", "Women M", "Men M", "Men L"],
    base: { Child: 18, "Women S": 20, "Women M": 22, "Men M": 24, "Men L": 26 },
    defaultSize: "Women M",
    fits: { Snug: -3, Fitted: -2 }, defaultFit: "Fitted",
    derive: (F) => ({
      "sock-cuff": { circumference: Math.round(F), depth: 5 },
      "sock-leg":  { circumference: Math.round(F), depth: Math.round(F * 0.80) },
      "sock-heel": { circumference: Math.round(F) },
      "sock-foot": { circumference: Math.round(F), depth: Math.round(F * 1.05) },
      "sock-toe":  { circumference: Math.round(F) },
    }),
  },
}

// ── Helper: default gauge from yarn weight ────────────────────────────────────
// Returns standard stitches per 10cm for a given yarn weight key.
// Used to pre-fill gauge input when user selects a yarn weight.
function defaultGauge(yarnKey) {
  return (YARN[yarnKey] || YARN.dk).stdSts
}

function finishedMeasure(presetKey, size, fit) {
  const S = SIZING[presetKey]
  if (!S) return null
  const base = S.base[size] ?? S.base[S.defaultSize]
  const ease = S.fits[fit] ?? 0
  return base + ease
}

// The ACTUAL finished body measurement currently shown on the schematic —
// reads the live (possibly hand-overridden) dimension off the anchor piece,
// falling back to the size-derived value when no build/blocks exist yet.
// This is what keeps the "finished chest NNcm" readout honest after a manual edit.
function actualFinishedMeasure(presetKey, size, fit, blocks, effDims) {
  const S = SIZING[presetKey]
  if (!S) return null
  if (S.finishedRole && blocks && effDims) {
    const i = blocks.findIndex(b => b.role === S.finishedRole)
    if (i >= 0) {
      const v = effDims[i] && effDims[i][S.finishedKey]
      if (typeof v === "number" && v > 0) return Math.round(v)
    }
  }
  return finishedMeasure(presetKey, size, fit)
}

function applySizing(blocks, presetKey, size, fit) {
  const S = SIZING[presetKey]
  if (!S) return blocks
  const byRole = S.derive(finishedMeasure(presetKey, size, fit))
  return blocks.map(b => {
    if (!b.role) return b
    const nd = byRole[b.role]
    if (!nd) return b
    const dims = { ...b.dims }
    const pinned = b.pinnedDims || {}
    Object.entries(nd).forEach(([k, v]) => {
      if (!(k in dims) || pinned[k]) return
      const r = b.ranges[k]
      dims[k] = r ? Math.min(r[1], Math.max(r[0], v)) : v
    })
    return { ...b, dims }
  })
}

// ── Core calculation (v2 — geometric primitive engine) ───────────────────────
// calcMeters: metres of yarn for a single block = area × metres_per_cm² × stitch
// modifier × quantity. There is NO regression and NO fibre modifier here — every
// piece is its own shape (Build Plan §0). The sweater body subtracts its neckline
// (a manipulable negative shape).
//   block:        the block object
//   effectiveDims: resolved dimensions (after linking/mirroring)
//   garmentYarn:  project-level yarn weight key (only used to fall back on stdSts)
//   gauge:        stitches per 10cm — drives metres_per_cm²
//   rows:         rows per 10cm — optional row-gauge correction (else standard)
function calcMeters(block, effectiveDims, garmentYarn, gauge, rows) {
  const prim = PRIMITIVES[block.type]
  if (prim.isFixed) return (block.fixedMeters || 25) * (block.quantity || 1)

  const yarn   = YARN[block.yarnOverride || garmentYarn]
  const constr = CONSTRUCTION[block.construction] || CONSTRUCTION.stockinette
  const constrMod = constr.mod
  if (!yarn) return 0

  // area of this shape; the sweater body has its neckline subtracted
  let area = prim.area(effectiveDims)
  if (block.role === "sweater-body" && block.neckType) {
    area = Math.max(0, area - neckArea(block.neckType, effectiveDims.width, block.neckW, block.neckD))
  }

  // A piece with its own yarn override is knit at that yarn's standard gauge
  // unless the project gauge applies; otherwise the effective project gauge drives it.
  const blockGauge = block.yarnOverride ? (yarn.stdSts || gauge || 22) : (gauge || yarn.stdSts || 22)
  const blockRows  = block.yarnOverride ? null : rows
  // precision mode: a weighed swatch gives metres_per_cm² directly (captures the
  // knitter's real tension, row gauge & fibre). Otherwise use the FLAT-SWATCH curve.
  // TODO: validate hat/sock/scarf/shawl against Cleome / Atherley / AnyFoot data
  const mPerCm2 = (yarn.mPerCm2 && yarn.mPerCm2 > 0) ? yarn.mPerCm2 : metresPerCm2Flat(blockGauge, blockRows)
  // held-double knits along ONE path but consumes that length of EACH strand, so
  // report TOTAL single-strand metres (× strands) — matching the sweater engine and
  // the "total single-strand metres" note. Grams stay correct via the per-strand basis.
  const strands = (yarn.strands && yarn.strands > 0) ? yarn.strands : 1
  return Math.round(mPerCm2 * area * constrMod * (block.quantity || 1) * strands)
}

let uid = 1

function makeBlock(cfg) {
  const prim = PRIMITIVES[cfg.type]
  const dims = {}
  prim.dims.forEach(d => { dims[d.key] = cfg.dims?.[d.key] ?? d.default })
  const ranges = {}
  prim.dims.forEach(d => { ranges[d.key] = cfg.ranges?.[d.key] ?? [d.min, d.max] })
  return {
    id: uid++,
    type: cfg.type,
    label: cfg.label || "",
    role: cfg.role || null,
    dims,
    defaultDims: { ...dims },
    ranges,
    construction: cfg.construction || prim.defaultConstruction || "stockinette",
    yarnOverride: null,
    quantity: cfg.quantity || 1,
    isAnchor: cfg.isAnchor || false,
    followsAnchorDim: cfg.followsAnchorDim || null,
    followsAnchorKey: cfg.followsAnchorKey || null,   // anchor's source dim when its key differs from ours
    links: cfg.links ? { ...cfg.links } : {},
    dimLabels: cfg.dimLabels ? { ...cfg.dimLabels } : {},
    pinnedDims: {},
    pos: cfg.pos || null,
    fixedMeters: 25,
    // sweater neckline (negative shape subtracted from the body). null = no neck.
    neckType: cfg.neckType || null,
    neckW: cfg.neckW != null ? cfg.neckW : null,
    neckD: cfg.neckD != null ? cfg.neckD : null,
  }
}

// ── Anatomical layout engine ──────────────────────────────────────────────────
function shapesForGarment(presetKey, blocks, effDims) {
  const byRole = {}
  blocks.forEach((b, i) => { if (b.role) (byRole[b.role] || (byRole[b.role] = [])).push({ b, d: effDims[i], i }) })
  const first = role => byRole[role]?.[0]
  const S = []

  const put = (entry, kind, box, labelOverride) => {
    if (!entry) return
    S.push({
      blockId: entry.b.id, kind,
      x: box.x, y: box.y, w: box.w, h: box.h,
      taper: box.taper, dir: box.dir, rot: box.rot, pts: box.pts,
      color: PRIMITIVES[entry.b.type].color,
      label: labelOverride || entry.b.label || PRIMITIVES[entry.b.type].label,
    })
  }

  if (presetKey === "hat") {
    const body = first("hat-body"), brim = first("hat-brim"), crown = first("hat-crown")
    const fw = (body?.d.circumference ?? 50) / 2
    // crown drawn at its real dome depth (clamped so it stays readable)
    const crownH = Math.max(fw * 0.22, Math.min(fw * 1.1, crown?.d.depth ?? fw * 0.42))
    const bodyH = body?.d.depth ?? 15
    const brimH = brim?.d.depth ?? 7
    let y = 0
    put(crown, "dome", { x: 0, y, w: fw, h: crownH }); y += crownH
    put(body,  "rect", { x: 0, y, w: fw, h: bodyH });  y += bodyH
    put(brim,  "rect", { x: 0, y, w: fw, h: brimH })
    return S
  }

  if (presetKey === "scarf") {
    const body = first("scarf-body")
    put(body, "rect", { x: 0, y: 0, w: body?.d.width ?? 20, h: body?.d.length ?? 180 })
    return S
  }

  if (presetKey === "socks") {
    // Anatomical sock, edges flush: a vertical cuff+leg column, then the foot angled
    // down-and-forward so its BACK edge starts exactly at the leg's front-bottom
    // corner; the heel is the triangle between the leg bottom and that back edge
    // (its hypotenuse IS the foot's back edge); a rounded dome caps the toe.
    const cuff = first("sock-cuff"), leg = first("sock-leg"),
          heel = first("sock-heel"), foot = first("sock-foot"), toe = first("sock-toe")
    const legW = (leg?.d.circumference ?? 22) / 2    // drawn leg width (flattened half-circ)
    const footH = legW * 0.92                         // foot a touch narrower than the leg
    const cuffH = cuff?.d.depth ?? 5
    const legH = leg?.d.depth ?? 16
    const footL = foot?.d.depth ?? 22
    const toeD = footH * 0.58
    // vertical leg column
    let y = 0
    put(cuff, "rect", { x: 0, y, w: legW, h: cuffH }); y += cuffH
    put(leg,  "rect", { x: 0, y, w: legW, h: legH });  y += legH
    const ankleY = y
    // foot axis (down-and-forward) and its perpendicular (down-the-back-edge)
    const deg = 40, th = deg * Math.PI / 180
    const dx = Math.cos(th), dy = Math.sin(th)        // along the foot
    const nx = -Math.sin(th), ny = Math.cos(th)       // along the foot's back edge
    const Q = { x: legW, y: ankleY }                  // leg front-bottom corner = foot back-edge top
    const backLo = { x: Q.x + footH * nx, y: Q.y + footH * ny }  // foot back-edge bottom
    const backMid = { x: Q.x + (footH / 2) * nx, y: Q.y + (footH / 2) * ny }
    const fc = { x: backMid.x + (footL / 2) * dx, y: backMid.y + (footL / 2) * dy }
    // heel — triangle (leg-bottom-left, leg-bottom-front Q, foot back-edge bottom)
    const hpts = [[0, ankleY], [Q.x, Q.y], [backLo.x, backLo.y]]
    const hxs = hpts.map(p => p[0]), hys = hpts.map(p => p[1])
    put(heel, "poly", { pts: hpts, x: Math.min(...hxs), y: Math.min(...hys),
      w: Math.max(...hxs) - Math.min(...hxs), h: Math.max(...hys) - Math.min(...hys) })
    // foot — rectangle rotated about its centre; back edge runs Q → backLo
    put(foot, "rect", { x: fc.x - footL / 2, y: fc.y - footH / 2, w: footL, h: footH, rot: deg })
    // toe — rounded dome capping the far end, apex pointing along the foot
    const tc = { x: backMid.x + (footL + toeD / 2) * dx, y: backMid.y + (footL + toeD / 2) * dy }
    put(toe, "dome", { x: tc.x - footH / 2, y: tc.y - toeD / 2, w: footH, h: toeD, rot: deg + 90 })
    return S
  }

  if (presetKey === "sweater") {
    const body = first("sweater-body"), hem = first("sweater-hem")
    const sleeves = byRole["sweater-sleeve"] || []
    const left = sleeves[0], right = sleeves[1] || sleeves[0]
    const bw = (body?.d.width ?? 97) / 2
    const bh = body?.d.length ?? 60
    const lenL = left?.d.depth ?? 44,  wL = (left?.d.circumference ?? 31) / 2, cL = (left?.d.cuff ?? 20) / 2
    const lenR = right?.d.depth ?? 44, wR = (right?.d.circumference ?? 31) / 2, cR = (right?.d.cuff ?? 20) / 2
    const hemH = hem?.d.depth ?? 5
    const bodyX = lenL + 6
    put(body,  "rect", { x: bodyX, y: 0, w: bw, h: bh })
    // tapered sleeves: wide end (upper arm) meets the body, narrows to the cuff
    put(left,  "trap", { x: bodyX - lenL - 4, y: 0, w: lenL, h: wL, taper: cL / Math.max(1, wL), dir: "left" })
    put(right, "trap", { x: bodyX + bw + 4,   y: 0, w: lenR, h: wR, taper: cR / Math.max(1, wR), dir: "right" })
    put(hem,   "rect", { x: bodyX, y: bh, w: bw, h: hemH })
    return S
  }

  if (presetKey === "shawl") {
    const body = first("shawl-body"), border = first("shawl-border")
    const base = body?.d.base ?? 165
    const h = body?.d.height ?? 66
    const bd = border?.d.depth ?? 0
    if (border) put(border, "rect", { x: 0, y: 0, w: base, h: bd })
    put(body, "tri", { x: 0, y: bd, w: base, h })
    return S
  }

  return null
}

// ── Localization ──────────────────────────────────────────────────────────────
const LT = {
  // header / chrome
  "yarn estimator": "siūlų skaičiuoklė",
  "Yarn": "Siūlai",
  "Stash": "Atsargos",
  "need {n}m more": "trūksta {n}m",
  "{n}m to spare": "lieka {n}m",
  // onboarding
  "Yarn estimator": "Siūlų skaičiuoklė",
  "How much yarn|will it take?": "Kiek siūlų|prireiks?",
  "onboarding_blurb": "Pradėkite nuo mezginio ir formuokite kiekvieną dalį. Skaičiuosime, kiek metrų natūralių siūlų prireiks.",
  "or build from scratch": "arba kurkite nuo nulio",
  "Add piece": "Pridėti dalį",
  "Start from a garment, then shape each piece. We'll estimate the meterage for your natural-fibre yarn as you go.":
    "Pasirinkite mezginį ir formuokite kiekvieną dalį. Skaičiuosime, kiek metrų natūralių siūlų prireiks.",
  "Tap a project to start — adjust the details after.": "Spustelėkite mezginį ir pradėkite — detales suderinsite vėliau.",
  "Back to your build": "Grįžti į mezginį",
  "Designs": "Mezginiai",
  // stage toolbar
  "Your build": "Jūsų mezginys",
  "Undo": "Atšaukti",
  "Undo (⌘Z)": "Atšaukti (⌘Z)",
  "Reset all sizes": "Atstatyti visus dydžius",
  "Clear": "Išvalyti",
  "tap any piece to shape it": "spustelėkite dalį, kad ją keistumėte",
  "shown flat — tap any piece to shape it": "išklotinė — spustelėkite dalį, kad ją keistumėte",
  "drag to arrange · tap to shape": "tempkite norėdami pertvarkyti · spustelėkite norėdami keisti",
  "Reset position": "Atstatyti vietą",
  "Moved": "Perkelta",
  // size / fit bar
  "Size": "Dydis",
  "Fit": "Laisvumas",
  "Gauge": "Tankumas",
  "sts / 10cm": "akys / 10cm",
  "Fibre": "Pluoštas",
  "Shoulder": "Pečiai",
  "Set-in": "Įstatytas",
  "Drop": "Nuleistas",
  "Your gauge": "Jūsų tankumas",
  "we estimate": "įvertiname",
  "we estimate {n}": "įvertiname {n}",
  "Knit tighter or looser than the label? Enter your swatch gauge — it overrides the default and drives the estimate.":
    "Mezgate tampriau ar laisviau nei nurodyta? Įveskite savo mėginio tankumą — jis pakeis numatytąjį ir nulems skaičiavimą.",
  "Gauge drives this estimate. The default comes from the yarn weight — set a custom gauge in the yarn dialog to override it.":
    "Tankumas lemia šį skaičiavimą. Numatytasis imamas pagal siūlų storį — savo tankumą galite nurodyti siūlų lange.",
  "finished chest": "krūtinės apimtis",
  "finished circumference": "gatava apimtis",
  "+{n}cm ease": "laisvumas +{n}cm",
  "{n}cm ease": "laisvumas {n}cm",
  // size-up helper
  "Size up adds ~{n}m": "Didesnis dydis +~{n}m",
  "Next size (~+6cm) needs ~{n}m more": "Kitas dydis (+6cm) reikia ~{n}m daugiau",
  // inspector
  "Tap a piece on the schematic to shape it.": "Spustelėkite dalį schemoje, kad ją keistumėte.",
  "Rename piece": "Pervadinti dalį",
  "Type": "Tipas",
  "Stitch pattern": "Raštas",
  "· override": "· pakeista",
  "· inherits": "· paveldima",
  "Garment yarn ({x})": "Mezginio siūlai ({x})",
  "Quantity": "Kiekis", "Quantity / Layer": "Kiekis / sluoksniai",
  "split": "skaidyti",
  "Split into individual pieces": "Išskaidyti į atskiras dalis",
  "Reset size": "Atstatyti dydį",
  "Re-sync to {x}": "Grąžinti į {x}",
  "Reset to default size": "Atstatyti numatytą dydį",
  "Pull this piece back to size {x}": "Grąžinti šią dalį į dydį {x}",
  "Remove": "Šalinti",
  "linked": "susieta",
  "Linked to another piece": "Susieta su kita dalimi",
  "custom": "keista",
  "size {x}": "dydis {x}",
  "Following size {x}": "Seka dydį {x}",
  "Custom — edited dimensions won't change when you switch size. Reset to follow size again.":
    "Keista — pakeisti matmenys nesikeis keičiant dydį. Atstatykite, kad vėl sektų dydį.",
  "Mirror another piece": "Atkartoti kitą dalį",
  "Not linked — size independently": "Nesusieta — keiskite atskirai",
  "Match {name}'s size": "Kaip \u201E{name}\u201C",
  "Every dimension follows that piece. Edit individual dims by unlinking above.":
    "Visi matmenys seka tą dalį. Norėdami keisti atskirai, atsiekite viršuje.",
  "Mirror {dim} of\u2026": "Atkartoti \u201E{dim}\u201C i\u0161\u2026",
  "Link to another piece": "Susieti su kita dalimi",
  "Link sizes: keep this measurement equal to another piece. Change one and both update together.":
    "Susieti matmenis: ši reikšmė bus lygi kitos dalies. Pakeitus vieną, pasikeičia abi.",
  "Linked pieces share a measurement — change one and the others follow.":
    "Susietos dalys turi bendrą matmenį — pakeitus vieną, kitos prisitaiko.",
  "Unlink": "Atsieti",
  // custom yarn
  "Other… (custom)": "Kita… (sava)",
  "Your own yarn": "Savi siūlai",
  "Length": "Ilgis", "Weight": "Svoris",
  "e.g. you have a ball that is 20 m and 25 g": "pvz., turite kamuoliuką: 20 m ir 25 g",
  "Custom": "Sava",
  "Save": "Išsaugoti", "Cancel": "Atšaukti", "Use this yarn": "Naudoti šiuos siūlus",
  // fibre types
  "Wool / Alpaca": "Vilna / Alpaka",
  "Cashmere": "Kašmyras",
  "Cotton / Linen": "Medvilnė / Linas",
  "Silk blend": "Šilko mišinys",
  "Synthetic": "Sintetinis",
  // save / load
  "Saved": "Išsaugoti", "Save project": "Išsaugoti projektą", "Saved projects": "Išsaugoti projektai",
  "Name your project": "Pavadinkite projektą", "No saved projects yet": "Išsaugotų projektų dar nėra",
  "Load": "Atverti", "Delete": "Ištrinti", "Saved ✓": "Išsaugota ✓",
  "Follows {x}": "Seka {x}",
  "Mirrored from another piece": "Atkartota iš kitos dalies",
  "Breakdown": "Suvestinė",
  "Total": "Iš viso",
  "By yarn": "Pagal siūlus",
  "buy": "pirkti",
  // fixed-overhead options
  "Pompom small — 15m": "Bumbulas mažas — 15m",
  "Pompom medium — 25m": "Bumbulas vidutinis — 25m",
  "Pompom large — 40m": "Bumbulas didelis — 40m",
  "Tassel — 35m": "Kutas — 35m",
  "Fringe strand — 8m": "Kuto siūlas — 8m",
  // garment names
  "Hat": "Kepurė", "Scarf": "Šalikas", "Socks": "Kojinės", "Sweater": "Megztinis", "Shawl": "Skara",
  // blurbs
  "Brim, body & crown — worked in the round.": "Kraštas, korpusas ir viršus — mezgama ratu.",
  "One long worked panel.": "Viena ilga numegzta juosta.",
  "Cuff down — pair counted automatically.": "Nuo aukšto žemyn — pora skaičiuojama automatiškai.",
  "Body, two sleeves & a ribbed hem.": "Korpusas, dvi rankovės ir gumytės kraštas.",
  "A single triangular body.": "Vientisas trikampis.",
  // piece labels
  "Brim": "Kraštas", "Body": "Pagrindinė dalis", "Body##sweater": "Liemuo", "Crown": "Viršus", "Cuff": "Atnara", "Leg": "Blauzda",
  "Heel / Toe": "Kulnas / Pirštai", "Foot": "Pėda", "Sleeve": "Rankovė", "Hem": "Kraštas", "Border": "Krašelis",
  "Toe": "Pirštai", "Heel": "Kulnas",
  // primitive labels
  "Panel": "Plokštė", "Tube": "Vamzdis", "Triangle": "Trikampis", "Rib Band": "Gumytės juosta", "Fixed": "Fiksuota",
  // primitive descriptions
  "Body · Scarf · Front/Back": "Pagrindinė dalis · Šalikas · Priekis+nugara",
  "Hat body · Sleeve · Sock leg": "Kepurės korpusas · Rankovė · Kojinės blauzda",
  "Hat top · Sock toe/heel": "Kepurės viršus · Kojinės pirštai/kulnas",
  "Triangular shawl": "Trikampė skara",
  "Brim · Cuff · Hem · Waistband": "Kraštas · Atnara · Apačia · Juosmuo",
  "Pompom · Tassel · Fringe": "Bumbulas · Kutas · Kutai",
  // dimension labels
  "Width": "Plotis", "Length": "Ilgis", "Circumference": "Apimtis", "Depth": "Aukštis",
  // stitch patterns
  "Stockinette": "Lygusis",
  "Garter": "Ripso",
  "Elastic Rib": "Gumytė (elastinga)",
  "Relaxed Rib": "Gumytė (atsipalaidavusi)",
  "Textured": "Reljefinis",
  "Cables": "Pynės",
  "Colorwork": "Spalvotas",
  "Brioche": "Brioche (angliški stulpeliai)",
  "Lace": "Ažūrinis",
  "Open Knit (Mohair)": "Atviras mezginys (voratinklis)",
  // stitch short names (chips)
  "St st": "Lyg.", "Texture": "Relj.", "Colour": "Spalv.",
  // fit names
  "Fitted": "Priglundantis", "Classic": "Klasikinis", "Relaxed": "Laisvas",
  "Snug": "Aptemptas", "Slouchy": "Krintantis",
  // sizes
  "Baby": "Kūdikis", "Toddler": "Mažylis", "Child": "Vaikas",
  "Adult S": "Suaug. S", "Adult M": "Suaug. M", "Adult L": "Suaug. L",
  "Women S": "Moterų S", "Women M": "Moterų M", "Men M": "Vyrų M", "Men L": "Vyrų L",
  // measurement words
  "Bust": "Krūtinė", "Head": "Galva", "Foot": "Pėda",
  // tweaks panel
  "Style": "Stilius", "Theme": "Tema", "Layout": "Išdėstymas", "Density": "Tankis",
}

const LangCtx = React.createContext("en")

function fmt(s, vars) {
  return vars ? String(s).replace(/\{(\w+)\}/g, (_, k) => (vars[k] != null ? vars[k] : "{" + k + "}")) : s
}
function translate(lang, en, vars) {
  // Context-keyed labels: an English string may carry a "##context" suffix so the
  // SAME word can take different translations by context (e.g. "Body##sweater" →
  // "Liemuo" vs generic "Body" → "Pagrindinė dalis"). The suffix is looked up in LT
  // but always stripped from the English fallback so it never shows to the user.
  let s = (lang === "lt" && LT[en] != null) ? LT[en] : en
  if (s === en) { const i = s.indexOf("##"); if (i >= 0) s = s.slice(0, i) }
  return fmt(s, vars)
}
function useT() {
  const lang = React.useContext(LangCtx)
  return React.useCallback((en, vars) => translate(lang, en, vars), [lang])
}

Object.assign(window, {
  YARN, CONSTRUCTION, PRIMITIVES, PRESETS, SIZING,
  stashStatus,
  calcMeters, makeBlock, shapesForGarment,
  finishedMeasure, actualFinishedMeasure, applySizing,
  defaultGauge,
  LT, LangCtx, translate, useT,
  nextUid: () => uid++,
  setUidFloor: (n) => { if (n >= uid) uid = n + 1 },
})
