// ctm-cmb.jsx — CMB (Character Manufacturing Blueprint) builder.
// Third intake path on Chop-the-Mock AND a living sheet on the review board (◈ CMB sheet).
// Panels fill from the collection's HOT (approved/active) assets — or upload. Asset cards can
// link their image onto a CMB panel (window.CmbLinkButton). Sheet persists per collection.
// Save/Manufacture → every filled panel flows into the SAME peel/review session as α0 chops.
// Parchment "Character Master Bible" look per the WAYNE_001 / SCARECROW references — NOT AF1 blue.
// Doctrine: Canon Score is DERIVED from filled panels, never typed. No fake data.
const { useState: mS, useRef: mR, useEffect: mE, useMemo: mM } = React;

// --- register CMB layer groups on CTM_DATA so the review board renders these sections (idempotent) ---
(function(){
  var G = window.CTM_DATA && window.CTM_DATA.groups; if (!G) return;
  var add = [
    { id:"cmb_turn",   name:"CMB · Turnaround",   desc:"Front / ¾ / side / back canon views" },
    { id:"cmb_detail", name:"CMB · Details",      desc:"Closeup detail panels" },
    { id:"cmb_expr",   name:"CMB · Expression pack", desc:"Approved, named faces" },
    { id:"cmb_outfit", name:"CMB · Outfits & variants", desc:"Seasonal & alternate looks" },
    { id:"cmb_prop",   name:"CMB · Props & items", desc:"Held & carried objects" },
    { id:"cmb_acc",    name:"CMB · Accessories breakdown", desc:"Exploded parts — peeled recursively" }
  ];
  add.forEach(function(g){ if (!G.some(function(x){ return x.id === g.id; })) G.push(g); });
})();

// --- self-contained spring (motion law) ---
function cmbReducedMotion(){
  try { return !!(window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches); }
  catch(e){ return false; }
}
function cmbSpringStep(s, target, k, d, dt){ var f = -k*(s.x-target) - d*s.v; s.v += f*dt; s.x += s.v*dt; return s; }
function cmbSpringIn(node, dy){
  if(cmbReducedMotion()){
    if(node){ node.dataset.motionMode="reduced"; node.style.opacity=""; node.style.transform=""; }
    return;
  }
  if(!node) return; var s={x:0,v:0}, D=dy==null?12:dy, last=performance.now(), done=false;
  node.dataset.motionMode="standard";
  var clear=function(){ if(done) return; done=true; node.style.opacity=""; node.style.transform=""; node.dataset.motionSettled="true"; };
  node.style.opacity="0"; node.style.transform="translateY("+D+"px)";
  setTimeout(clear, 900); // rAF-throttle safety: never leave content hidden
  (function tick(now){ if(done) return; var n=Math.min(3,Math.round((now-last)/16.6))||1; last=now;
    for(var i=0;i<n;i++) cmbSpringStep(s,1,168,22,1/60);
    node.style.opacity=String(Math.min(1,s.x)); node.style.transform="translateY("+(D*(1-s.x))+"px)";
    if(Math.abs(s.x-1)>0.003||Math.abs(s.v)>0.003) requestAnimationFrame(tick);
    else clear();
  })(last);
}

// --- section blueprint ---
const CMB_SECTIONS = [
  { key:"turn",   g:"cmb_turn",   n:"01", title:"Turnaround", addable:false,
    slots:[["front","FRONT"],["three","3/4 FRONT"],["side","SIDE"],["back","BACK"]] },
  { key:"detail", g:"cmb_detail", n:"02", title:"Details", addable:true,
    slots:[["face","FACE CLOSEUP"],["badge","HAT BADGE"],["buckle","BELT BUCKLE"],["boot","BOOT DETAIL"]] },
  { key:"expr",   g:"cmb_expr",   n:"03", title:"Expression Pack", addable:true,
    slots:[["neutral","NEUTRAL"],["happy","HAPPY"],["confident","CONFIDENT"],["wink","WINK"],["surprised","SURPRISED"],["talking","TALKING"],["laughing","LAUGHING"],["determined","DETERMINED"]] },
  { key:"outfit", g:"cmb_outfit", n:"04", title:"Outfits & Variants", addable:true,
    slots:[["default","DEFAULT"],["winter","WINTER"],["christmas","CHRISTMAS"],["halloween","HALLOWEEN"]] },
  { key:"prop",   g:"cmb_prop",   n:"05", title:"Props & Items", addable:true,
    slots:[["p1","PROP 1"],["p2","PROP 2"],["p3","PROP 3"],["p4","PROP 4"]] },
  { key:"acc",    g:"cmb_acc",    n:"06", title:"Accessories Breakdown", addable:true, recursive:true,
    slots:[["hat","HAT"],["badge","BADGE"],["rope","ROPE"],["belt","BELT"],["boots","BOOTS"]] }
];
const ACC_PARTS = { hat:["BAND","LEATHER","BADGE","STUDS"], badge:["STAR","PLATE","RIVETS","ENGRAVE"], rope:["COIL","WORK","END CAP"], belt:["STRAP","BUCKLE","KEEPER","STITCH"], boots:["UPPER","SOLE","STRAP","SPUR"] };

function blankSheet(){
  const panels = {};
  CMB_SECTIONS.forEach(sec => { panels[sec.key] = sec.slots.map(([id,label]) => ({
    id, label, img:null, ref:null,
    parts: sec.recursive ? (ACC_PARTS[id]||[]).map(p => ({ label:p, img:null, ref:null })) : null
  })); });
  return {
    dna: { character_id:"", name:"", tagline:"", role:"", alignment:"", universe:"", species:"", style:"", scale:"", personality:"", strapline:"", version:"1.0" },
    palette: [], materials: [], features: [], prohibited: [], relationships: [],
    panels
  };
}

// ---- draft persistence (per collection; "intake" when no collection yet) ----
const cmbKey = (pid) => "ctm_cmb_draft_" + (pid || "intake");
function cmbLoadDraft(pid){
  try { const d = JSON.parse(localStorage.getItem(cmbKey(pid))); if (d && d.panels && d.dna) {
    const b = blankSheet(); // merge over blank so new fields/sections appear on old drafts
    d.dna = { ...b.dna, ...d.dna };
    Object.keys(b.panels).forEach(k => { if (!d.panels[k]) d.panels[k] = b.panels[k]; });
    return d;
  } } catch(e){}
  return blankSheet();
}
function cmbSaveDraft(pid, sheet){
  const key = cmbKey(pid);
  try { localStorage.setItem(key, JSON.stringify(sheet)); return true; }
  catch(e){ // quota: strip uploaded dataURLs, keep refs + labels + DNA
    try {
      const slim = JSON.parse(JSON.stringify(sheet));
      Object.keys(slim.panels).forEach(k => slim.panels[k].forEach(p => { if(p.img) p.img = null; (p.parts||[]).forEach(pt => { if(pt.img) pt.img = null; }); }));
      localStorage.setItem(key, JSON.stringify(slim)); return false;
    } catch(e2){ return false; }
  }
}

// pull DNA (text only) from a Character Lab V2 canon record
function canonDb(){
  try { const d = JSON.parse(localStorage.getItem("af1_charlab_v2")); if (d && d.characters) return d; } catch(e){}
  return null;
}
function loadCanonInto(sheet, c, db){
  const s = JSON.parse(JSON.stringify(sheet));
  s.dna.character_id = c.id || ""; s.dna.name = c.name || ""; s.dna.role = c.role || "";
  s.dna.alignment = c.alignment || ""; s.dna.universe = c.universe || ""; s.dna.species = c.species || "";
  s.dna.style = c.visual_style || ""; s.dna.scale = c.scale || "";
  s.dna.personality = (c.personality||[]).join(", ");
  s.dna.tagline = c.role ? ("THE " + c.role.split("·")[0].trim().toUpperCase() + ". THE HEART OF " + (c.universe||"THE UNIVERSE").toUpperCase() + ".") : "";
  const uni = db && db.universes && db.universes.find(u => u.id === c.universe);
  if (uni && uni.strapline) s.dna.strapline = uni.strapline;
  s.palette = (c.palette||[]).map(p => ({ name:p.name, hex:p.hex }));
  s.materials = (c.materials||[]).map(m => ({ part:m.part, finish:m.finish }));
  s.features = (c.signature_features||[]).slice();
  s.prohibited = (c.prohibited_changes||[]).slice();
  s.relationships = (c.relationships||[]).filter(r=>r.name).map(r => ({ name:r.name, type:r.type||"" }));
  return s;
}

// resolve a panel to its display/manufacture source. ref = asset id from the collection.
function cmbResolve(p, assets, store){
  if (p.img) return { src:p.img, prePeeled:false, from:"upload" };
  if (p.ref && assets) {
    const a = assets.find(x => x.id === p.ref);
    if (a) { const repl = store && store.replacements && store.replacements[a.id]; return { src:(repl && repl.url) || a.src, prePeeled:!!a.peeled, from:a.id }; }
  }
  return null;
}
function hotAssets(assets, store){
  if (!assets || !store) return [];
  return assets.filter(a => ["approved","active"].includes(store.statuses[a.id]));
}

// ---- Canon Score: derived from what is actually filled, never typed ----
function scoreSheet(sheet, assets, store){
  const P = sheet.panels;
  const has = (p) => !!(p.img || (p.ref && (!assets || assets.some(a=>a.id===p.ref))));
  const filled = (arr) => arr.filter(has).length;
  const ratio = (a,b) => b ? Math.round(a/b*100) : 0;
  const turn = ratio(filled(P.turn), P.turn.length);
  const expr = ratio(filled(P.expr), P.expr.length);
  const outfit = ratio(filled(P.outfit), P.outfit.length);
  const prop = ratio(filled(P.prop), P.prop.length);
  let accTot=0, accHit=0;
  P.acc.forEach(a => { accTot++; if(has(a)) accHit++; (a.parts||[]).forEach(pt => { accTot++; if(has(pt)) accHit++; }); });
  const acc = ratio(accHit, accTot);
  const d = sheet.dna;
  const core = ["name","role","alignment","universe","species","style","scale","personality"];
  const identity = ratio(core.filter(k => (d[k]||"").trim()).length + (d.character_id?1:0), core.length+1);
  const materials = Math.min(100, Math.round(sheet.materials.filter(m=>m.part&&m.finish).length / 6 * 100));
  const paletteScore = Math.min(100, Math.round(sheet.palette.filter(p=>p.name&&p.hex).length / 8 * 100));
  const detail = ratio(filled(P.detail), P.detail.length);
  const anim = Math.round(turn*0.5 + expr*0.5);
  const merch = Math.round(outfit*0.5 + prop*0.5);
  const game = Math.round((paletteScore + materials + turn) / 3);
  return [
    { k:"IDENTITY", v:identity }, { k:"VISUAL CONSISTENCY", v:turn }, { k:"MATERIALS", v:materials },
    { k:"ACCESSORIES", v:acc }, { k:"EXPRESSION PACK", v:expr }, { k:"ANIMATION READY", v:anim },
    { k:"MERCH READY", v:merch }, { k:"GAME READY", v:game },
    { _detail:detail, _outfit:outfit, _prop:prop, _palette:paletteScore }
  ];
}

// ---- hot-asset picker (fills a slot from the collection's approved files) ----
function CmbPicker({ hot, onPick, onUpload, close }){
  return (
    <div className="cmb-pick-wrap" onClick={close}>
      <div className="cmb-pick" onClick={(e)=>e.stopPropagation()}>
        <div className="cmb-pick-head"><span className="mono-lab">FILL FROM PROJECT FILES · HOT ASSETS ONLY</span><button className="cmb-sw-x" onClick={close}>✕</button></div>
        {hot.length ? (
          <div className="cmb-pick-grid">
            {hot.map(a => (
              <button className="cmb-pick-cell" key={a.id} onClick={()=>onPick(a)}>
                <span className="cmb-pick-thumb">{a.src ? <img src={a.src} alt={a.name}/> : <i className="mono">{a.glyph||"—"}</i>}</span>
                <span className="cmb-pick-name">{a.name}</span>
                <span className="mono-lab">{a.id.toUpperCase()}{a.peeled?" · α0":""}</span>
              </button>
            ))}
          </div>
        ) : (
          <div className="cmb-empty mono-lab">NO HOT ASSETS IN THIS COLLECTION YET — APPROVE ASSETS ON THE BOARD, OR UPLOAD.</div>
        )}
        <div className="cmb-pick-foot"><button className="cmb-ghost" onClick={onUpload}>⇧ Upload a file instead</button></div>
      </div>
    </div>
  );
}

// ---- one editable image panel (hot-asset pick / drop / upload) ----
function CmbSlot({ p, on, small, assets, store }){
  const [over, setOver] = mS(false);
  const [pick, setPick] = mS(false);
  const inp = mR(null);
  const res = cmbResolve(p, assets, store);
  const hot = hotAssets(assets, store);
  const take = (f) => { if(!f || !f.type.startsWith("image/")) return; const rd = new FileReader(); rd.onload = () => on({ img: rd.result, ref:null }); rd.readAsDataURL(f); };
  const click = () => { if (hot.length) setPick(true); else inp.current.click(); };
  return (
    <div className={"cmb-slot" + (small?" sm":"") + (over?" over":"") + (res?" filled":"")}
      onClick={click}
      onDragOver={(e)=>{e.preventDefault();setOver(true);}} onDragLeave={()=>setOver(false)}
      onDrop={(e)=>{e.preventDefault();setOver(false);take(e.dataTransfer.files[0]);}}>
      {res ? <img src={res.src} alt={p.label} /> : <span className="cmb-slot-glyph mono">✦</span>}
      {res && <button className="cmb-slot-x" onClick={(e)=>{e.stopPropagation();on({img:null, ref:null});}}>✕</button>}
      {res && p.ref && <span className="cmb-slot-ref mono">◈ {p.ref.toUpperCase()}</span>}
      <input ref={inp} type="file" accept="image/*" style={{display:"none"}} onClick={(e)=>e.stopPropagation()} onChange={(e)=>{take(e.target.files[0]);e.target.value="";}} />
      <span className="cmb-slot-cap">{p.label}</span>
      {pick && <CmbPicker hot={hot} close={()=>setPick(false)}
        onPick={(a)=>{ on({ ref:a.id, img:null }); setPick(false); }}
        onUpload={()=>{ setPick(false); inp.current.click(); }} />}
    </div>
  );
}

// ---- editable list row helpers ----
function CmbTagList({ items, ph, onAdd, onDel, onEdit }){
  const [v,setV] = mS("");
  return (
    <div className="cmb-tags">
      {items.map((t,i)=>(
        <span className="cmb-tag" key={i}>
          <input value={t} onChange={(e)=>onEdit(i,e.target.value)} />
          <button onClick={()=>onDel(i)}>✕</button>
        </span>
      ))}
      <span className="cmb-tag add">
        <input value={v} placeholder={ph} onChange={(e)=>setV(e.target.value)} onKeyDown={(e)=>{ if(e.key==="Enter"&&v.trim()){onAdd(v.trim());setV("");} }} />
        <button onClick={()=>{ if(v.trim()){onAdd(v.trim());setV("");} }}>＋</button>
      </span>
    </div>
  );
}

// ---- asset-card → CMB link button (rendered by the review board on every card) ----
function CmbLinkButton({ a, store, pid }){
  const [open, setOpen] = mS(false);
  const [done, setDone] = mS(false);
  const st = store.statuses[a.id] || "pending";
  if (!["approved","active"].includes(st)) return null; // only HOT assets can be placed on the blueprint
  const place = (secKey) => {
    const sheet = cmbLoadDraft(pid);
    const sec = CMB_SECTIONS.find(s => s.key === secKey);
    const arr = sheet.panels[secKey];
    let slot = arr.find(p => !p.img && !p.ref);
    if (!slot && sec.addable) { slot = { id:"x"+Date.now().toString(36), label:(a.name||"PANEL").toUpperCase().slice(0,22), img:null, ref:null, parts: sec.recursive ? [] : null }; arr.push(slot); }
    if (!slot) { setOpen(false); return; }
    slot.ref = a.id; slot.img = null;
    if (/^(PROP \d|NEW PANEL)$/.test(slot.label)) slot.label = (a.name||slot.label).toUpperCase().slice(0,22);
    cmbSaveDraft(pid, sheet);
    setOpen(false); setDone(true); setTimeout(()=>setDone(false), 1600);
  };
  return (
    <span className="cmb-link" onClick={(e)=>e.stopPropagation()}>
      <button className="cmb-link-btn" onClick={()=>setOpen(o=>!o)}>{done ? "◈ Linked ✓" : "◈ CMB"}</button>
      {open && (
        <span className="cmb-link-menu">
          <span className="mono-lab">PLACE ON THE BLUEPRINT →</span>
          {CMB_SECTIONS.map(s => <button key={s.key} onClick={()=>place(s.key)}>{s.n} · {s.title}</button>)}
        </span>
      )}
    </span>
  );
}

// ---- in-review CMB placement (section-scoped): open a card → assign it to a slot in ITS section ----
function CmbPlaceControl({ a, pid }){
  const key = (a && a.g && a.g.indexOf("cmb_")===0) ? a.g.slice(4) : null;
  const sec = key ? CMB_SECTIONS.find(s=>s.key===key) : null;
  const readSlots = () => { const d = cmbLoadDraft(pid); return (d.panels[key]||[]).map(p=>({ id:p.id, label:p.label, filled:!!(p.img||p.ref), mine:p.ref===a.id })); };
  const [slots, setSlots] = mS(readSlots);
  const [placed, setPlaced] = mS(() => { const m = slots.find(s=>s.mine); return m ? m.id : ""; });
  mE(() => { const s = readSlots(); setSlots(s); const m = s.find(x=>x.mine); setPlaced(m?m.id:""); }, [a && a.id]);
  if (!sec) return null;
  const apply = (slotId) => {
    const d = cmbLoadDraft(pid);
    d.panels[key] = (d.panels[key]||[]).map(p => {
      if (slotId && p.id===slotId) return { ...p, ref:a.id, img:null };
      if (p.ref===a.id) return { ...p, ref:null }; // move, never duplicate within a section
      return p;
    });
    cmbSaveDraft(pid, d);
    const s = (d.panels[key]||[]).map(p=>({ id:p.id, label:p.label, filled:!!(p.img||p.ref), mine:p.ref===a.id }));
    setSlots(s); setPlaced(slotId||"");
  };
  const cur = slots.find(s=>s.id===placed);
  return (
    <div className="cmb-place">
      <div className="cmb-place-row">
        <select data-testid={`cmb-place-${a.id}`} value={placed} onChange={(e)=>apply(e.target.value)}>
          <option value="">— choose {sec.title} slot —</option>
          {slots.map(s => <option key={s.id} value={s.id}>{s.label}{s.filled && !s.mine ? " · occupied" : ""}</option>)}
        </select>
      </div>
      <span className={"cmb-place-ok mono-lab" + (placed?" on":"")}>{placed ? "◈ placed · "+sec.n+" "+sec.title+" → "+(cur?cur.label:"") : "not on the CMB sheet yet"}</span>
    </div>
  );
}
function CmbBuilder({ onCommit, back, assets, store, pid, mode }){
  const [sheet, setSheet] = mS(() => cmbLoadDraft(pid));
  const [saved, setSaved] = mS(null);
  const sheetRef = mR(null);
  const db = mM(() => canonDb(), []);
  const canon = db ? db.characters : null;
  const scores = mM(() => scoreSheet(sheet, assets, store), [sheet, assets, store && store.statuses]);
  const overall = mM(() => { const s = scores.slice(0,8); return Math.round(s.reduce((a,b)=>a+b.v,0)/s.length); }, [scores]);
  const meta = scores[8];
  const hot = hotAssets(assets, store);

  mE(() => { if(sheetRef.current) cmbSpringIn(sheetRef.current, 14); }, []);
  mE(() => { cmbSaveDraft(pid, sheet); }, [sheet]);

  const setDna = (k,v) => setSheet(s => ({ ...s, dna: { ...s.dna, [k]:v } }));
  const setPanel = (secKey, idx, patch) => setSheet(s => {
    const arr = s.panels[secKey].map((p,i) => i===idx ? { ...p, ...patch } : p);
    return { ...s, panels: { ...s.panels, [secKey]: arr } };
  });
  const setAccPart = (idx, pIdx, patch) => setSheet(s => {
    const arr = s.panels.acc.map((p,i) => i!==idx ? p : { ...p, parts: p.parts.map((pt,j)=> j===pIdx ? { ...pt, ...patch } : pt) });
    return { ...s, panels: { ...s.panels, acc: arr } };
  });
  const addSlot = (secKey) => setSheet(s => {
    const sec = CMB_SECTIONS.find(x=>x.key===secKey);
    const np = { id:"x"+Date.now().toString(36), label:"NEW PANEL", img:null, ref:null, parts: sec.recursive ? [] : null };
    return { ...s, panels: { ...s.panels, [secKey]: [...s.panels[secKey], np] } };
  });
  const relabel = (secKey, idx, label) => setPanel(secKey, idx, { label });
  const listOps = (field) => ({
    onAdd:(v)=>setSheet(s=>({ ...s, [field]:[...s[field], v] })),
    onDel:(i)=>setSheet(s=>({ ...s, [field]:s[field].filter((_,j)=>j!==i) })),
    onEdit:(i,v)=>setSheet(s=>({ ...s, [field]:s[field].map((x,j)=>j===i?v:x) }))
  });
  const idAuto = () => (sheet.dna.character_id || (sheet.dna.name||"CHAR").toUpperCase().replace(/[^A-Z0-9]+/g,"_").replace(/^_|_$/g,"").slice(0,16) + "_001");

  // ASSEMBLE — the CMB builder is the OUTPUT sheet: it collects DEVELOPED assets (assigned from the
  // collection board or uploaded) and hands the character to Asset Flix. It does NOT peel — peeling
  // belongs to intake (a delivered CMB sheet → chops). Nothing here re-processes finished art.
  const assemble = () => {
    const cid = idAuto();
    const panels = [];
    CMB_SECTIONS.forEach(sec => {
      sheet.panels[sec.key].forEach((p) => {
        const r = cmbResolve(p, assets, store);
        if (r) panels.push({ section:sec.key, sectionTitle:sec.title, label:p.label, src:r.src, ref:p.ref||null });
        (p.parts||[]).forEach((pt) => {
          const rp = cmbResolve(pt, assets, store);
          if (rp) panels.push({ section:sec.key, sectionTitle:sec.title+" (part)", label:(p.label||"")+" · "+pt.label, src:rp.src, ref:pt.ref||null });
        });
      });
    });
    if (!panels.length) { setSaved({ err:"Assign at least one developed asset to a panel first." }); return; }
    writeCmbRecord(cid);
    const front = sheet.panels.turn.find(p => p.id==="front" && cmbResolve(p, assets, store));
    const frontSrc = front ? cmbResolve(front, assets, store).src : panels[0].src;
    const exprFilled = sheet.panels.expr.filter(p => cmbResolve(p, assets, store)).map(p => (p.label||"").toLowerCase());
    registerCanonFull(cid, panels, frontSrc, exprFilled);
    if (window.AF1_OBSERVABILITY) {
      window.AF1_OBSERVABILITY.emit("af1.cmb.assembled", { character_id: cid, panel_count: panels.length });
      window.AF1_OBSERVABILITY.emit("af1.product.completed", {
        flow: "cmb-assembly",
        character_id: cid,
        panel_count: panels.length,
        destination: "asset-flix-handoff",
      });
    }
    setSaved({ ok:true, count:panels.length, cid });
  };
  const openAssetFlix = () => { try { window.open("../asset-forge/Asset Flix.html", "_blank"); } catch(e) { try { window.location.href = "../asset-forge/Asset Flix.html"; } catch(e2){} } };


  const writeCmbRecord = (cid) => {
    const rec = {
      character_id: cid, name: sheet.dna.name, tagline: sheet.dna.tagline,
      version: sheet.dna.version, universe: sheet.dna.universe, dna: sheet.dna,
      palette: sheet.palette, materials: sheet.materials, signature_features: sheet.features,
      prohibited_changes: sheet.prohibited, relationships: sheet.relationships,
      canon_score: { overall, breakdown: scores.slice(0,8) },
      panels: Object.keys(sheet.panels).reduce((o,k)=>{ o[k] = sheet.panels[k].map(p=>({ label:p.label, filled:!!(p.img||p.ref), ref:p.ref||undefined, parts: p.parts?p.parts.map(pt=>({label:pt.label,filled:!!(pt.img||pt.ref),ref:pt.ref||undefined})):undefined })); return o; }, {}),
      locked: true, ts: Date.now()
    };
    try { const all = JSON.parse(localStorage.getItem("af1_cmb")||"{}"); all[cid] = rec; localStorage.setItem("af1_cmb", JSON.stringify(all)); } catch(e){}
  };
  // upsert the character into the Character Lab store WITH its developed panel assets, pose image and
  // expressions — Asset Flix imports its cast from af1_charlab_v2, so this is the hand-off.
  const registerCanonFull = (cid, panels, frontSrc, exprFilled) => {
    try {
      const d2 = JSON.parse(localStorage.getItem("af1_charlab_v2")||"null");
      if (!d2 || !d2.characters) return;
      const devAssets = panels.map((p, i) => ({
        id: cid.toLowerCase()+"_"+p.section+"_"+(i+1),
        name: p.label || p.sectionTitle, slot: "render", socket: "",
        style: "CMB · "+p.sectionTitle, materials: "", states: "",
        canon_status: "approved", img: p.src, from_cmb: true
      }));
      const base = {
        id:cid, name:sheet.dna.name||cid, universe:sheet.dna.universe||"", species:sheet.dna.species||"human",
        role:sheet.dna.role||"", alignment:sheet.dna.alignment||"", visual_style:sheet.dna.style||"stylised 3D",
        scale:sheet.dna.scale||"", head_ratio:"", body_proportions:"", face_language:"", stage_scale:1,
        personality:(sheet.dna.personality||"").split(",").map(s=>s.trim()).filter(Boolean),
        materials:sheet.materials.slice(), palette:sheet.palette.slice(),
        signature_features:sheet.features.slice(), prohibited_changes:sheet.prohibited.slice(),
        relationships:sheet.relationships.map(r=>({ with:"", name:r.name, type:r.type, note:"" })),
        pose_img:frontSrc||"", stage_img:frontSrc||"", status:"canon", version:1,
        assets:devAssets, reviews:[], rig:{ pose:{}, notes:"" }, expressions:exprFilled, from_cmb:true
      };
      const idx = d2.characters.findIndex(c => c.id === cid);
      if (idx >= 0) { // existing canon: attach the developed assets, don't fork identity
        const cur = d2.characters[idx];
        const nonCmb = (cur.assets||[]).filter(a => !a.from_cmb);
        d2.characters[idx] = { ...cur, pose_img: cur.pose_img || frontSrc || "", stage_img: frontSrc || cur.stage_img || "", assets: [...nonCmb, ...devAssets], expressions: exprFilled.length ? exprFilled : cur.expressions, cmb_ready: true };
      } else {
        d2.characters.push(base);
      }
      localStorage.setItem("af1_charlab_v2", JSON.stringify(d2));
    } catch(e){}
  };

  const dna = sheet.dna;
  const dnaRows = [["CHARACTER ID", idAuto(), "character_id"],["ROLE", dna.role, "role"],["ALIGNMENT", dna.alignment, "alignment"],["UNIVERSE", dna.universe, "universe"],["SPECIES", dna.species, "species"],["STYLE", dna.style, "style"],["SCALE", dna.scale, "scale"],["PERSONALITY", dna.personality, "personality"]];
  const filledCount = CMB_SECTIONS.reduce((n,sec)=>n+sheet.panels[sec.key].filter(p=>p.img||p.ref).length + (sec.recursive?sheet.panels[sec.key].reduce((m,p)=>m+(p.parts||[]).filter(pt=>pt.img||pt.ref).length,0):0),0);
  const motionMode = cmbReducedMotion() ? "reduced" : "standard";

  return (
    <div className="cmb-root" data-motion-mode={motionMode}>
      <CmbStyle />
      <header className="cmb-topbar">
        <div className="cmb-topbar-l"><span className="cmb-wm">AF1</span><span className="mono-lab">{mode==="board" ? "REVIEW BOARD · CMB SHEET" : "CTM INTAKE · CHARACTER MANUFACTURING BLUEPRINT"}</span></div>
        <div className="cmb-topbar-r">
          {hot.length > 0 && <span className="mono-lab cmb-hot-note">◈ {hot.length} HOT ASSET{hot.length===1?"":"S"} AVAILABLE — CLICK ANY PANEL</span>}
          {canon && canon.length > 0 && (
            <label className="cmb-load mono-lab">↧ Prefill DNA from canon
              <select defaultValue="" onChange={(e)=>{ const c=canon.find(x=>x.id===e.target.value); if(c) setSheet(s=>loadCanonInto(s,c,db)); e.target.value=""; }}>
                <option value="" disabled>character…</option>
                {canon.map(c => <option key={c.id} value={c.id}>{c.name} · {c.id}</option>)}
              </select>
            </label>
          )}
          {back && <button className="cmb-ghost" onClick={back}>{mode==="board" ? "✕ Close" : "⌂ Back to intake"}</button>}
        </div>
      </header>

      <main className="cmb-scroll">
        <div className="cmb-sheet" ref={sheetRef}>
          <div className="cmb-sheet-inner">

          {/* top strip: leather bible patch · kicker · official ticket */}
          <div className="cmb-strip">
            <div className="cmb-patch">
              <span className="cmb-patch-uni">{(dna.universe||"UNIVERSE").toUpperCase()}</span>
              <span className="cmb-patch-lab">CHARACTER<br/>MASTER BIBLE</span>
            </div>
            <div className="cmb-strip-mid">
              <div className="cmb-kicker">CHARACTER MANUFACTURING BLUEPRINT</div>
              <input className="cmb-name" value={dna.name} placeholder="NAME" onChange={(e)=>setDna("name", e.target.value)} />
              <div className="cmb-tagrow">
                <span className="cmb-ver">CMB v<input className="cmb-ver-in" value={dna.version} onChange={(e)=>setDna("version", e.target.value)} /></span>
                <input className="cmb-tag-in" value={dna.tagline} placeholder="THE ROLE. THE ONE-LINE STORY." onChange={(e)=>setDna("tagline", e.target.value)} />
              </div>
            </div>
            <div className="cmb-ticket">
              <span className="mono-lab">OFFICIAL</span>
              <b>{(dna.universe||"UNIVERSE").toUpperCase()}</b>
              <span className="mono-lab">CHARACTER</span>
              <i>★</i>
            </div>
          </div>

          {/* masthead: DNA table left · turnaround right */}
          <div className="cmb-mast">
            <div className="cmb-mast-l">
              <table className="cmb-dna"><tbody>
                {dnaRows.map(([label,val,key]) => (
                  <tr key={key}>
                    <td className="cmb-dna-k mono-lab">{label}</td>
                    <td className="cmb-dna-v">{key==="character_id"
                      ? <input value={dna.character_id} placeholder={val} onChange={(e)=>setDna("character_id", e.target.value.toUpperCase())} />
                      : <input value={val} placeholder="—" onChange={(e)=>setDna(key, e.target.value)} />}</td>
                  </tr>
                ))}
              </tbody></table>
              {/* canon score */}
              <div className="cmb-score">
                <div className="cmb-score-head"><span className="cmb-plaque-mini">CANON SCORE</span><span className="cmb-score-big">{overall}<em>%</em></span></div>
                {scores.slice(0,8).map(s=>(
                  <div className="cmb-score-row" key={s.k}>
                    <span className="cmb-score-k mono-lab">{s.k}</span>
                    <div className="cmb-score-bar"><span style={{ width:s.v+"%" }}></span></div>
                    <span className="cmb-score-v mono">{s.v}%</span>
                  </div>
                ))}
                <div className="mono-lab cmb-derived">✦ DERIVED FROM FILLED PANELS — NEVER TYPED</div>
              </div>
            </div>
            <div className="cmb-mast-r">
              <div className="cmb-plaque"><i>◆</i>01 · TURNAROUND<i>◆</i></div>
              <div className="cmb-turn-grid">
                {sheet.panels.turn.map((p,i)=>(
                  <div className="cmb-turn-cell" key={p.id}>
                    <CmbSlot p={p} on={(patch)=>setPanel("turn", i, patch)} assets={assets} store={store} />
                  </div>
                ))}
              </div>
            </div>
          </div>

          {/* image sections 02–06 */}
          {CMB_SECTIONS.filter(s=>s.key!=="turn").map(sec => (
            <section className="cmb-sec" key={sec.key}>
              <div className="cmb-sec-head">
                <div className="cmb-plaque"><i>◆</i>{sec.n} · {sec.title.toUpperCase()}<i>◆</i></div>
                {sec.addable && <button className="cmb-add mono-lab" onClick={()=>addSlot(sec.key)}>＋ panel</button>}
              </div>
              <div className={"cmb-sec-grid " + sec.key}>
                {sheet.panels[sec.key].map((p,i)=>(
                  sec.recursive ? (
                    <div className="cmb-acc" key={p.id}>
                      <CmbSlot p={p} on={(patch)=>setPanel(sec.key, i, patch)} assets={assets} store={store} />
                      <input className="cmb-acc-name" value={p.label} onChange={(e)=>relabel(sec.key,i,e.target.value)} />
                      <div className="cmb-acc-parts">
                        {(p.parts||[]).map((pt,j)=>(
                          <div className="cmb-acc-part" key={j}>
                            <CmbSlot p={pt} small on={(patch)=>setAccPart(i, j, patch)} assets={assets} store={store} />
                            <span className="cmb-acc-part-lab">{pt.label}</span>
                          </div>
                        ))}
                      </div>
                    </div>
                  ) : (
                    <div className="cmb-panel" key={p.id}>
                      <CmbSlot p={p} on={(patch)=>setPanel(sec.key, i, patch)} assets={assets} store={store} />
                      <input className="cmb-panel-name" value={p.label} onChange={(e)=>relabel(sec.key,i,e.target.value)} />
                    </div>
                  )
                ))}
              </div>
            </section>
          ))}

          {/* palette + materials — full width */}
          <div className="cmb-stack">
            <section className="cmb-sec">
              <div className="cmb-sec-head"><div className="cmb-plaque"><i>◆</i>07 · COLOUR PALETTE (DNA)<i>◆</i></div>
                <button className="cmb-add mono-lab" onClick={()=>setSheet(s=>({...s,palette:[...s.palette,{name:"NEW",hex:"#8a5a2c"}]}))}>＋ swatch</button></div>
              <div className="cmb-pal">
                {sheet.palette.map((c,i)=>(
                  <div className="cmb-sw" key={i}>
                    <button className="cmb-sw-x" onClick={()=>setSheet(s=>({...s,palette:s.palette.filter((_,j)=>j!==i)}))}>✕</button>
                    <label className="cmb-sw-chip" style={{ background:c.hex }}>
                      <input type="color" value={/^#[0-9a-f]{6}$/i.test(c.hex)?c.hex:"#8a5a2c"} onChange={(e)=>setSheet(s=>({...s,palette:s.palette.map((x,j)=>j===i?{...x,hex:e.target.value}:x)}))} />
                    </label>
                    <input className="cmb-sw-name" value={c.name} onChange={(e)=>setSheet(s=>({...s,palette:s.palette.map((x,j)=>j===i?{...x,name:e.target.value}:x)}))} />
                    <input className="cmb-sw-hex mono" value={c.hex} onChange={(e)=>setSheet(s=>({...s,palette:s.palette.map((x,j)=>j===i?{...x,hex:e.target.value}:x)}))} />
                  </div>
                ))}
                {!sheet.palette.length && <div className="cmb-empty mono-lab">NO SWATCHES YET — ADD THE CHARACTER'S NAMED COLOURS</div>}
              </div>
            </section>
            <section className="cmb-sec">
              <div className="cmb-sec-head"><div className="cmb-plaque"><i>◆</i>08 · MATERIALS (DNA)<i>◆</i></div>
                <button className="cmb-add mono-lab" onClick={()=>setSheet(s=>({...s,materials:[...s.materials,{part:"",finish:""}]}))}>＋ material</button></div>
              <div className="cmb-mat">
                {sheet.materials.map((m,i)=>(
                  <div className="cmb-mat-tile" key={i}>
                    <button className="cmb-sw-x" onClick={()=>setSheet(s=>({...s,materials:s.materials.filter((_,j)=>j!==i)}))}>✕</button>
                    <span className="cmb-mat-chip"></span>
                    <input className="cmb-mat-part" value={m.part} placeholder="part" onChange={(e)=>setSheet(s=>({...s,materials:s.materials.map((x,j)=>j===i?{...x,part:e.target.value}:x)}))} />
                    <input className="cmb-mat-fin" value={m.finish} placeholder="finish / material" onChange={(e)=>setSheet(s=>({...s,materials:s.materials.map((x,j)=>j===i?{...x,finish:e.target.value}:x)}))} />
                  </div>
                ))}
                {!sheet.materials.length && <div className="cmb-empty mono-lab">NO MATERIALS YET — MAP EACH PART TO ITS FINISH</div>}
              </div>
            </section>
          </div>

          {/* signature + prohibited */}
          <div className="cmb-two">
            <section className="cmb-sec">
              <div className="cmb-sec-head"><div className="cmb-plaque"><i>◆</i>SIGNATURE FEATURES<i>◆</i></div></div>
              <CmbTagList items={sheet.features} ph="add a signature feature…" {...listOps("features")} />
            </section>
            <section className="cmb-sec">
              <div className="cmb-sec-head"><div className="cmb-plaque red"><i>◆</i>PROHIBITED CHANGES · ANTI-DRIFT<i>◆</i></div></div>
              <CmbTagList items={sheet.prohibited} ph="what must never change…" {...listOps("prohibited")} />
            </section>
          </div>

          <section className="cmb-sec">
            <div className="cmb-sec-head"><div className="cmb-plaque"><i>◆</i>09 · RELATIONSHIPS<i>◆</i></div>
              <button className="cmb-add mono-lab" onClick={()=>setSheet(s=>({...s,relationships:[...s.relationships,{name:"",type:""}]}))}>＋ relation</button></div>
            <div className="cmb-rel">
              {sheet.relationships.map((r,i)=>(
                <div className="cmb-rel-card" key={i}>
                  <input className="cmb-rel-name" value={r.name} placeholder="name" onChange={(e)=>setSheet(s=>({...s,relationships:s.relationships.map((x,j)=>j===i?{...x,name:e.target.value}:x)}))} />
                  <input className="cmb-rel-type mono-lab" value={r.type} placeholder="type" onChange={(e)=>setSheet(s=>({...s,relationships:s.relationships.map((x,j)=>j===i?{...x,type:e.target.value}:x)}))} />
                  <button className="cmb-sw-x" onClick={()=>setSheet(s=>({...s,relationships:s.relationships.filter((_,j)=>j!==i)}))}>✕</button>
                </div>
              ))}
              {!sheet.relationships.length && <div className="cmb-empty mono-lab">NO RELATIONSHIPS YET</div>}
            </div>
          </section>

          <section className="cmb-sec">
            <div className="cmb-sec-head"><div className="cmb-plaque"><i>◆</i>10 · EXPORT READY<i>◆</i></div></div>
            <div className="cmb-export">
              {[["GAME ENGINE","FBX / GLB", scores[7].v>=70],["ANIMATION", "Rig ready", scores[5].v>=70],["PRINT / MERCH","High res", meta._detail>=60],["TOY PRODUCTION","Turnaround + spec", scores[1].v>=75]].map((r,i)=>(
                <div className={"cmb-ex "+(r[2]?"on":"")} key={i}>
                  <span className="cmb-ex-dot">{r[2]?"◉":"○"}</span>
                  <div><b>{r[0]}</b><span className="mono-lab">{r[1]}</span></div>
                  <span className="cmb-ex-st mono-lab">{r[2]?"READY":"PENDING"}</span>
                </div>
              ))}
            </div>
          </section>

          {/* footer */}
          <div className="cmb-foot">
            <span className="cmb-foot-wm">★ {(dna.universe||"UNIVERSE").toUpperCase()}</span>
            <input className="cmb-foot-strap" value={dna.strapline} placeholder="WE GUIDE. WE DELIVER. WE WIN." onChange={(e)=>setDna("strapline", e.target.value)} />
            <span className="cmb-foot-lock mono-lab">LOCKED &amp; CANON · {idAuto()}</span>
          </div>
          </div>
        </div>
      </main>

      <footer className="cmb-actionbar">
        <span className="mono-lab">{overall}% CANON · {filledCount} DEVELOPED PANEL{filledCount===1?"":"S"} ASSIGNED{hot.length?` · ${hot.length} HOT ON THE BOARD`:""}</span>
        <div className="cmb-actionbar-r">
          {saved && saved.err && <span className="cmb-msg err">{saved.err}</span>}
          {saved && saved.ok && (
            <span className="cmb-msg ok">✓ {saved.count} panel{saved.count===1?"":"s"} → {saved.cid} handed to Asset Flix
              <button className="cmb-flix" onClick={openAssetFlix}>Open Asset Flix →</button>
            </span>
          )}
          <button className="cmb-manu" data-testid="assemble-cmb" onClick={assemble}>⚙ Assemble CMB → send to Asset Flix</button>
        </div>
      </footer>
    </div>
  );
}

// ---- injected stylesheet + display faces (self-contained; scoped to .cmb-*) ----
function CmbStyle(){
  mE(() => {
    if (!document.getElementById("cmb-fonts")) {
      const l = document.createElement("link"); l.id="cmb-fonts"; l.rel="stylesheet";
      l.href = "https://fonts.googleapis.com/css2?family=Anton&family=Oswald:wght@500;600&display=swap";
      document.head.appendChild(l);
    }
    if (!document.getElementById("cmb-styles")) {
      const el = document.createElement("style"); el.id = "cmb-styles"; el.textContent = CMB_CSS;
      document.head.appendChild(el);
    }
  }, []);
  return null;
}
const CMB_NOISE = "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/%3E%3CfeColorMatrix values='0 0 0 0 0.42 0 0 0 0 0.30 0 0 0 0 0.18 0 0 0 0.05 0'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E\")";
const CMB_CSS = `
.cmb-root{position:fixed;inset:0;z-index:60;display:flex;flex-direction:column;background:#241c16;font-family:"Oswald",-apple-system,"Segoe UI",Helvetica,sans-serif;color:#3a2a1c}
.cmb-root .mono,.cmb-root .mono-lab{font-family:ui-monospace,"SF Mono",Menlo,Consolas,monospace}
.cmb-root .mono-lab{font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:#8a6a48}
.cmb-topbar{display:flex;align-items:center;justify-content:space-between;padding:10px 18px;background:#171210;border-bottom:1px solid #443426;flex:0 0 auto;gap:14px}
.cmb-topbar .mono-lab{color:#b89468}
.cmb-topbar-l{display:flex;align-items:center;gap:12px}
.cmb-wm{font-weight:800;letter-spacing:.04em;color:#e8c8a0;font-size:15px}
.cmb-topbar-r{display:flex;align-items:center;gap:14px;flex-wrap:wrap;justify-content:flex-end}
.cmb-hot-note{color:#d8a860!important}
.cmb-load{display:flex;align-items:center;gap:8px;color:#b89468}
.cmb-load select{background:#241c16;color:#e8d8c4;border:1px solid #443426;border-radius:5px;padding:5px 8px;font-family:ui-monospace,monospace;font-size:11px}
.cmb-ghost{background:transparent;border:1px solid #443426;color:#c8a878;border-radius:6px;padding:6px 12px;font-size:12px;cursor:pointer;font-family:inherit}
.cmb-ghost:hover{border-color:#8a6a48;color:#e8c8a0}
.cmb-scroll{flex:1;overflow:auto;padding:26px;display:flex;justify-content:center;background:radial-gradient(circle at 50% -10%,#38291d,#1c1512 70%)}
.cmb-sheet{width:1220px;max-width:100%;height:max-content;background:linear-gradient(178deg,#f4ead8,#ecdfc6 60%,#e7d7ba);border:10px solid #593a22;border-radius:4px;box-shadow:0 34px 90px rgba(0,0,0,.55),inset 0 0 90px rgba(122,84,48,.16);position:relative}
.cmb-sheet::before{content:"";position:absolute;inset:0;background-image:${CMB_NOISE};pointer-events:none;border-radius:2px}
.cmb-sheet-inner{position:relative;border:1px solid #c9ae86;outline:1px solid #e6d5b4;outline-offset:-5px;margin:7px;padding:26px 30px 30px}
.cmb-sheet input,.cmb-sheet select{font-family:inherit}
.cmb-sheet input{background:transparent;border:none;color:#33230f;font-size:13px;width:100%;padding:2px 0;border-bottom:1px dotted transparent}
.cmb-sheet input:focus{outline:none;border-bottom:1px dotted #b08a5c}
.cmb-sheet input::placeholder{color:#bfa480}
/* top strip */
.cmb-strip{display:grid;grid-template-columns:170px 1fr 150px;gap:22px;align-items:start;padding-bottom:18px;border-bottom:3px double #8a5a2c;margin-bottom:18px}
.cmb-patch{background:linear-gradient(150deg,#6a4225,#4a2b15);border-radius:10px;padding:14px 12px;box-shadow:inset 0 0 0 1px #3a2110,inset 0 0 22px rgba(0,0,0,.35),0 3px 8px rgba(58,33,16,.4);position:relative;text-align:center}
.cmb-patch::before{content:"";position:absolute;inset:5px;border:1.5px dashed #c9a26a;border-radius:7px;opacity:.65}
.cmb-patch-uni{display:block;font-family:"Anton",sans-serif;font-size:26px;letter-spacing:.04em;color:#e8c48c;text-shadow:0 2px 0 rgba(0,0,0,.35)}
.cmb-patch-lab{display:block;margin-top:6px;font-size:9px;letter-spacing:.22em;color:#d8b380;font-weight:600;line-height:1.5}
.cmb-strip-mid{text-align:center;min-width:0}
.cmb-kicker{font-size:10px;letter-spacing:.34em;color:#9a7448;font-weight:600;margin-bottom:2px}
.cmb-name{font-family:"Anton",sans-serif!important;font-size:64px!important;letter-spacing:.015em;color:#2a1a0c!important;line-height:1.02;text-align:center;text-transform:uppercase;border-bottom:none!important;padding:0!important;text-shadow:0 1px 0 #fff6e6}
.cmb-tagrow{display:flex;align-items:center;justify-content:center;gap:10px;margin-top:4px}
.cmb-ver{display:inline-flex;align-items:center;background:#593a22;color:#f3e9d8;padding:3px 8px;border-radius:3px;font-size:10px;letter-spacing:.08em;font-weight:600;flex:0 0 auto}
.cmb-ver-in{width:30px!important;color:#f3e9d8!important;font-size:10px!important;text-align:center}
.cmb-tag-in{width:auto!important;max-width:520px;flex:1;font-weight:600!important;font-size:14px!important;color:#a3402a!important;letter-spacing:.1em;text-transform:uppercase;text-align:center}
.cmb-tag-in::placeholder{color:#c98a72!important}
.cmb-ticket{justify-self:end;background:#efe3c8;border:1.5px dashed #a98a5a;border-radius:6px;padding:12px 14px;text-align:center;display:flex;flex-direction:column;gap:2px;box-shadow:2px 3px 0 rgba(90,58,34,.25);transform:rotate(2deg)}
.cmb-ticket b{font-family:"Anton",sans-serif;font-size:19px;color:#4a2b15;letter-spacing:.03em}
.cmb-ticket i{font-style:normal;color:#a3402a;font-size:13px}
/* plaques */
.cmb-plaque{display:inline-flex;align-items:center;gap:10px;background:linear-gradient(#6a4526,#4a2c18);color:#f0dfc0;font-weight:600;font-size:13px;letter-spacing:.14em;padding:6px 18px;border-radius:3px;box-shadow:inset 0 1px 0 #8a6240,inset 0 -2px 0 #35200f,0 2px 4px rgba(53,32,15,.35)}
.cmb-plaque i{font-style:normal;font-size:7px;color:#c9a26a}
.cmb-plaque.red{background:linear-gradient(#8a3520,#6a2614);color:#f4dcc8}
.cmb-plaque.red i{color:#d88a6a}
.cmb-plaque-mini{font-weight:600;font-size:12px;letter-spacing:.2em;color:#5a3a22}
/* masthead */
.cmb-mast{display:grid;grid-template-columns:360px 1fr;gap:26px;padding-bottom:6px}
.cmb-dna{width:100%;border-collapse:collapse}
.cmb-dna td{border:1px solid #cdb89a;padding:6.5px 9px;vertical-align:middle}
.cmb-dna-k{width:112px;background:#e6d6b8;color:#7a5836!important;white-space:nowrap}
.cmb-dna-v{background:rgba(255,250,238,.5)}
.cmb-dna-v input{font-size:12.5px}
.cmb-mast-r{display:flex;flex-direction:column;gap:12px;align-items:center}
.cmb-turn-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:12px;width:100%}
.cmb-turn-cell{aspect-ratio:3/4}
/* slots */
.cmb-slot{position:relative;width:100%;height:100%;min-height:70px;background:repeating-linear-gradient(45deg,#eadec2,#eadec2 7px,#e3d3b2 7px,#e3d3b2 14px);border:1.5px dashed #b8946a;border-radius:4px;display:flex;align-items:center;justify-content:center;cursor:pointer;overflow:hidden}
.cmb-slot.sm{min-height:44px}
.cmb-slot.over{border-color:#6f4a2c;background:#f2e8d2}
.cmb-slot.filled{border:1px solid #a98a5a;background:#fffaf0;box-shadow:inset 0 0 0 3px #f4ead8,0 2px 6px rgba(90,58,34,.18)}
.cmb-slot img{width:100%;height:100%;object-fit:cover;display:block}
.cmb-slot-glyph{font-size:16px;color:#c2a67e}
.cmb-slot-cap{position:absolute;left:0;right:0;bottom:0;background:rgba(46,30,16,.85);color:#ead9bf;padding:3px 5px;font-size:8.5px;font-weight:600;letter-spacing:.12em;text-align:center;pointer-events:none;text-transform:uppercase}
.cmb-slot-ref{position:absolute;top:3px;left:3px;background:rgba(163,64,42,.9);color:#f8e8d8;font-size:7.5px;letter-spacing:.06em;padding:2px 5px;border-radius:3px;pointer-events:none}
.cmb-slot-x{position:absolute;top:3px;right:3px;width:18px;height:18px;border-radius:50%;border:none;background:rgba(42,28,18,.8);color:#f3e9d8;cursor:pointer;font-size:10px;line-height:1;display:flex;align-items:center;justify-content:center}
/* hot-asset picker */
.cmb-pick-wrap{position:fixed;inset:0;z-index:80;background:rgba(20,14,10,.6);display:flex;align-items:center;justify-content:center;cursor:default}
.cmb-pick{width:640px;max-width:92vw;max-height:76vh;overflow:auto;background:#f2e7d2;border:8px solid #593a22;border-radius:4px;box-shadow:0 30px 80px rgba(0,0,0,.6);padding:16px}
.cmb-pick-head{display:flex;align-items:center;justify-content:space-between;border-bottom:2px solid #8a5a2c;padding-bottom:8px;margin-bottom:12px}
.cmb-pick-head .mono-lab{color:#5a3a22!important;font-size:10.5px}
.cmb-pick-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:10px}
.cmb-pick-cell{background:#fffaf0;border:1px solid #cdb89a;border-radius:5px;padding:8px;cursor:pointer;display:flex;flex-direction:column;gap:4px;text-align:left;font-family:inherit}
.cmb-pick-cell:hover{border-color:#8a5a2c;box-shadow:0 2px 8px rgba(90,58,34,.25)}
.cmb-pick-thumb{display:block;aspect-ratio:1/1;border-radius:3px;overflow:hidden;background:repeating-conic-gradient(#e8ddc8 0 25%,#f4ecd8 0 50%) 0 0/14px 14px;display:flex;align-items:center;justify-content:center}
.cmb-pick-thumb img{width:100%;height:100%;object-fit:contain}
.cmb-pick-thumb i{font-style:normal;color:#a98a5a}
.cmb-pick-name{font-size:12px;font-weight:600;color:#33230f;line-height:1.2}
.cmb-pick-foot{margin-top:12px;display:flex;justify-content:flex-end}
.cmb-pick .cmb-ghost{border-color:#a98a5a;color:#6a4526}
/* sections */
.cmb-sec{margin:22px 0 0}
.cmb-sec-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:12px}
.cmb-add{background:transparent;color:#6a4526!important;border:1px dashed #a98a5a;border-radius:4px;padding:4px 10px;cursor:pointer;font-size:9px}
.cmb-add:hover{background:#e6d6b8}
.cmb-sec-grid{display:flex;gap:16px;overflow-x:auto;overflow-y:visible;padding:2px 2px 12px;scroll-snap-type:x proximity}
.cmb-sec-grid.detail .cmb-panel,.cmb-sec-grid.outfit .cmb-panel,.cmb-sec-grid.prop .cmb-panel{flex:0 0 168px;scroll-snap-align:start}
.cmb-sec-grid.expr .cmb-panel{flex:0 0 130px;scroll-snap-align:start}
.cmb-sec-grid.acc .cmb-acc{flex:0 0 162px;scroll-snap-align:start}
.cmb-panel{display:flex;flex-direction:column;gap:6px}
.cmb-panel>.cmb-slot{aspect-ratio:1/1}
.cmb-sec-grid::-webkit-scrollbar,.cmb-pal::-webkit-scrollbar,.cmb-mat::-webkit-scrollbar{height:9px}
.cmb-sec-grid::-webkit-scrollbar-track,.cmb-pal::-webkit-scrollbar-track,.cmb-mat::-webkit-scrollbar-track{background:rgba(122,88,54,.12);border-radius:5px}
.cmb-sec-grid::-webkit-scrollbar-thumb,.cmb-pal::-webkit-scrollbar-thumb,.cmb-mat::-webkit-scrollbar-thumb{background:#b8946a;border-radius:5px;border:2px solid transparent;background-clip:padding-box}
.cmb-sec-grid::-webkit-scrollbar-thumb:hover,.cmb-pal::-webkit-scrollbar-thumb:hover,.cmb-mat::-webkit-scrollbar-thumb:hover{background:#8a5a2c;background-clip:padding-box}
.cmb-panel-name{text-align:center;font-size:10.5px!important;color:#7a5836!important;letter-spacing:.1em;text-transform:uppercase;font-weight:600}
.cmb-acc{display:flex;flex-direction:column;align-items:center;gap:5px;background:rgba(255,250,238,.4);border:1px solid #ddccac;border-radius:5px;padding:10px 8px}
.cmb-acc>.cmb-slot{aspect-ratio:1/1;width:100%}
.cmb-acc-name{text-align:center;font-weight:600;font-size:12px!important;color:#4a2b15!important;text-transform:uppercase;letter-spacing:.08em}
.cmb-acc-parts{display:flex;flex-direction:column;gap:0;width:100%;margin-top:2px;position:relative}
.cmb-acc-parts::before{content:"";position:absolute;left:50%;top:-4px;bottom:14px;width:1px;background:#b8946a;opacity:.5}
.cmb-acc-part{display:flex;align-items:center;gap:7px;padding:3px 0;position:relative}
.cmb-acc-part::before{content:"";width:9px;height:1px;background:#b8946a;opacity:.6;flex:0 0 auto;margin-left:calc(50% - 9px)}
.cmb-acc-part .cmb-slot{height:30px;width:30px;flex:0 0 auto}
.cmb-acc-part .cmb-slot .cmb-slot-cap{display:none}
.cmb-acc-part-lab{font-size:9px;letter-spacing:.1em;color:#7a5836;text-transform:uppercase;font-weight:600}
.cmb-two{display:grid;grid-template-columns:1fr 1fr;gap:28px}
.cmb-stack{display:flex;flex-direction:column;gap:20px}
/* score */
.cmb-score{margin-top:16px;padding:14px 16px;background:rgba(255,250,238,.55);border:1px solid #cdb89a;border-radius:4px}
.cmb-score-head{display:flex;align-items:baseline;justify-content:space-between;border-bottom:2px solid #8a5a2c;padding-bottom:6px;margin-bottom:10px}
.cmb-score-big{font-family:"Anton",sans-serif;font-size:30px;color:#6f4a2c}
.cmb-score-big em{font-size:15px;font-style:normal;color:#9a7448}
.cmb-score-row{display:grid;grid-template-columns:132px 1fr 38px;align-items:center;gap:10px;margin:5px 0}
.cmb-score-k{color:#7a5836!important;font-size:8.5px}
.cmb-score-bar{height:6px;background:#d8c4a0;border-radius:3px;overflow:hidden}
.cmb-score-bar span{display:block;height:100%;background:linear-gradient(90deg,#8a5a2c,#c49a2c);border-radius:3px}
.cmb-score-v{font-size:11px;color:#5a3a22;text-align:right;font-weight:700}
.cmb-derived{margin-top:10px;color:#a07c50!important;font-size:8px}
/* palette / materials / tags / relationships / export */
.cmb-pal{display:flex;gap:14px;overflow-x:auto;padding:2px 2px 12px}
.cmb-sw{flex:0 0 152px;position:relative;background:rgba(255,250,238,.55);border:1px solid #ddccac;border-radius:5px;padding:9px;text-align:center}
.cmb-sw-chip{display:block;width:100%;height:52px;border-radius:3px;border:1px solid #b89468;cursor:pointer;position:relative;overflow:hidden;box-shadow:inset 0 -8px 14px rgba(0,0,0,.14),inset 0 0 0 2px rgba(255,250,238,.35);margin-bottom:6px}
.cmb-sw-chip input{position:absolute;inset:-4px;opacity:0;cursor:pointer;width:150%;height:150%}
.cmb-sw-name{font-size:11px!important;font-weight:600;text-align:center!important;text-transform:uppercase;letter-spacing:.04em;color:#4a2b15!important}
.cmb-sw-hex{width:100%!important;font-size:10px!important;color:#7a5836!important;text-align:center!important;letter-spacing:.06em}
.cmb-sw-x{position:absolute;top:2px;right:2px;width:18px;height:18px;border:none;background:rgba(74,43,21,.14);border-radius:50%;color:#8a5a2c;cursor:pointer;font-size:10px;line-height:1;z-index:2}
.cmb-sw-x:hover{background:rgba(160,58,42,.85);color:#fff}
.cmb-mat{display:flex;gap:14px;overflow-x:auto;padding:2px 2px 12px}
.cmb-mat-tile{flex:0 0 138px;position:relative;background:rgba(255,250,238,.55);border:1px solid #ddccac;border-radius:5px;padding:9px;text-align:center}
.cmb-mat-chip{display:block;width:100%;height:46px;border-radius:3px;border:1px solid #b89468;margin-bottom:6px;background:repeating-linear-gradient(45deg,#c9a877,#c9a877 4px,#b8935f 4px,#b8935f 8px);box-shadow:inset 0 0 0 2px rgba(255,250,238,.3)}
.cmb-mat-part{font-weight:600;font-size:11px!important;text-align:center!important;text-transform:uppercase;letter-spacing:.04em;color:#4a2b15!important}
.cmb-mat-fin{font-size:9.5px!important;color:#7a5836!important;text-align:center!important;line-height:1.25}
.cmb-empty{padding:10px;text-align:center;color:#b09468!important;background:rgba(230,214,184,.6);border-radius:4px}
.cmb-tags{display:flex;flex-wrap:wrap;gap:6px}
.cmb-tag{display:inline-flex;align-items:center;gap:4px;background:rgba(230,214,184,.7);border:1px solid #cdb89a;border-radius:4px;padding:2px 4px 2px 8px}
.cmb-tag input{width:auto!important;min-width:60px;font-size:11.5px!important}
.cmb-tag.add{background:rgba(244,236,216,.7);border-style:dashed}
.cmb-tag button{border:none;background:transparent;color:#b8946a;cursor:pointer;font-size:11px;padding:0 4px}
.cmb-rel{display:grid;grid-template-columns:repeat(4,1fr);gap:12px}
.cmb-rel-card{background:rgba(255,250,238,.55);border:1px solid #cdb89a;border-radius:5px;padding:10px;display:flex;flex-direction:column;gap:4px;position:relative}
.cmb-rel-name{font-weight:700;font-size:14px!important;color:#33230f!important}
.cmb-rel-card .cmb-sw-x{position:absolute;top:4px;right:4px}
.cmb-export{display:grid;grid-template-columns:repeat(2,1fr);gap:12px}
.cmb-ex{display:flex;align-items:center;gap:12px;background:rgba(255,250,238,.55);border:1px solid #cdb89a;border-radius:5px;padding:12px 14px}
.cmb-ex.on{background:#e9dfc2;border-color:#a98a5a}
.cmb-ex-dot{font-size:18px;color:#b8946a}
.cmb-ex.on .cmb-ex-dot{color:#6f4a2c}
.cmb-ex b{display:block;font-size:13px;color:#33230f;font-weight:600;letter-spacing:.04em}
.cmb-ex .mono-lab{color:#9a7448!important}
.cmb-ex-st{margin-left:auto;color:#a07c50!important}
.cmb-ex.on .cmb-ex-st{color:#5a7a3a!important}
/* footer */
.cmb-foot{display:grid;grid-template-columns:auto 1fr auto;align-items:center;gap:18px;margin-top:26px;padding:13px 18px;background:linear-gradient(#5f3d24,#4a2c18);border-radius:4px;box-shadow:inset 0 1px 0 #8a6240}
.cmb-foot-wm{font-family:"Anton",sans-serif;color:#e8c48c;font-size:16px;letter-spacing:.04em}
.cmb-foot-strap{color:#e0c093!important;letter-spacing:.3em!important;font-size:11px!important;font-weight:600!important;text-align:center;text-transform:uppercase}
.cmb-foot-strap::placeholder{color:#a98a5a!important}
.cmb-foot-lock{color:#c8a878!important;border:1px solid #7a5836;padding:4px 9px;border-radius:3px}
/* action bar + card link */
.cmb-actionbar{flex:0 0 auto;display:flex;align-items:center;justify-content:space-between;padding:12px 22px;background:#171210;border-top:1px solid #443426}
.cmb-actionbar .mono-lab{color:#b89468}
.cmb-actionbar-r{display:flex;align-items:center;gap:14px}
.cmb-place{display:flex;flex-direction:column;gap:6px}
.cmb-place-row select{width:100%;background:#1c2536;color:#dbe6f5;border:1px solid #33415c;border-radius:6px;padding:8px 10px;font-family:ui-monospace,monospace;font-size:12px;cursor:pointer}
.cmb-place-ok{color:#7f8ea8!important}
.cmb-place-ok.on{color:#5fb0ff!important}
.cmb-msg.err{color:#e88a6a;font-size:12px}
.cmb-msg.ok{display:inline-flex;align-items:center;gap:10px;color:#7bbf6a;font-size:12px}
.cmb-flix{background:linear-gradient(#3a6f4a,#2c5a3a);color:#eafbe2;border:none;border-radius:6px;padding:6px 12px;font-size:12px;font-weight:600;cursor:pointer;font-family:inherit}
.cmb-flix:hover{filter:brightness(1.1)}
.cmb-manu{background:linear-gradient(#c49a2c,#a5772a);color:#241c10;border:none;border-radius:7px;padding:11px 22px;font-size:14px;font-weight:700;cursor:pointer;box-shadow:0 4px 14px rgba(0,0,0,.35);font-family:inherit;letter-spacing:.03em}
.cmb-manu:hover{filter:brightness(1.08)}
.cmb-link{position:relative;display:inline-flex}
.cmb-link-btn{background:transparent;border:1px solid #8a5a2c;color:#c49a2c;border-radius:5px;padding:3px 9px;font-size:10.5px;cursor:pointer;font-family:inherit;letter-spacing:.04em}
.cmb-link-btn:hover{background:rgba(196,154,44,.12)}
.cmb-link-menu{position:absolute;bottom:calc(100% + 6px);right:0;z-index:40;background:#f2e7d2;border:4px solid #593a22;border-radius:5px;padding:8px;display:flex;flex-direction:column;gap:4px;min-width:210px;box-shadow:0 12px 34px rgba(0,0,0,.45)}
.cmb-link-menu .mono-lab{color:#5a3a22!important;padding:2px 4px}
.cmb-link-menu button{background:#fffaf0;border:1px solid #cdb89a;border-radius:4px;padding:5px 9px;text-align:left;font-size:12px;color:#33230f;cursor:pointer;font-family:"Oswald",sans-serif}
.cmb-link-menu button:hover{border-color:#8a5a2c;background:#f4ecd8}
@media(prefers-reduced-motion:reduce){.cmb-root,.cmb-root *,.cmb-root *::before,.cmb-root *::after{animation-duration:.001ms!important;animation-iteration-count:1!important;transition-duration:.001ms!important;scroll-behavior:auto!important}}
@media(max-width:1180px){.cmb-name{font-size:52px!important}}
@media(max-width:820px){.cmb-strip{grid-template-columns:120px 1fr 120px;gap:12px}.cmb-patch{padding:10px 8px}.cmb-patch-uni{font-size:20px}.cmb-ticket{padding:8px}.cmb-ticket b{font-size:15px}.cmb-mast{grid-template-columns:1fr}.cmb-two,.cmb-rel,.cmb-export{grid-template-columns:1fr}}
`;
Object.assign(window, { CmbBuilder, CmbLinkButton, CmbPlaceControl, CMB_SECTIONS, cmbLoadDraft, cmbSaveDraft });
