/* Custex — Library / Manufacture / Community pages.
   Loaded after node-editor-parts.jsx. Re-uses CSS tokens + chrome from editor.css. */

const { useState: pS, useMemo: pM, useEffect: pE, useRef: pR } = React;

function cxLibMotifSvgInner(m) {
  const H = typeof window !== 'undefined' ? window.CxMotifHelpers : null;
  if (H && typeof H.svgInnerFromSchema === 'function') return H.svgInnerFromSchema(m);
  return m?.svg || '';
}

/** Community gallery cover — paths under `community/covers/` only; not shared with Library motifs. */
function cxCommunityCoverHref(relPath) {
  if (!relPath) return '';
  if (typeof document === 'undefined') return relPath;
  try {
    return new URL(relPath, document.baseURI || window.location.href).href;
  } catch {
    return relPath;
  }
}

// ─────────── router dispatcher ───────────
function CxPageRouter({ route, setRoute, graph, setGraph, validation, theme }) {
  if (route === 'home')        return <CxHome setRoute={setRoute} />;
  if (route === 'library')     return <CxLibrary setRoute={setRoute} graph={graph} setGraph={setGraph} theme={theme} />;
  if (route === 'manufacture') return <CxManufacture setRoute={setRoute} graph={graph} validation={validation} theme={theme} />;
  if (route === 'community')   return <CxCommunity setRoute={setRoute} setGraph={setGraph} theme={theme} />;
  return null;
}

// ═════════════════════════════════════════════════════════════════
// HOME — landing
// ═════════════════════════════════════════════════════════════════

/** Welcome landing — 3D fabric intro + Begin Custex CTA. */
const CX_HOME_HTML_SRC = '/home';

function CxHome({ setRoute: _setRoute }) {
  return (
    <div className="cx-main cx-page-home cx-page-home-embed">
      <iframe
        className="cx-home-embed-frame"
        title="Custex — Cybertextile"
        src={CX_HOME_HTML_SRC}
      />
    </div>
  );
}

// ═════════════════════════════════════════════════════════════════
// LIBRARY — textile DNA catalog
// ═════════════════════════════════════════════════════════════════

const LIB_TABS = [
  { id:'motifs',    label:'Motifs'    },
  { id:'materials', label:'Materials' },
  { id:'yarns',     label:'Yarns'     },
  { id:'crafts',    label:'Crafts'    },
];

function CxLibrary({ setRoute, graph, setGraph, theme }) {
  const [tab, setTab] = pS('materials');
  const [q, setQ] = pS('');
  const [hover, setHover] = pS(null);

  return (
    <div className="cx-main cx-page-library">
      <aside className="cx-sidebar">
        <div className="cx-side-section">
          <div className="cx-side-h">Library</div>
          <ul className="cx-libnav">
            {LIB_TABS.map(t => (
              <li key={t.id} className={tab===t.id?'sel':''} onClick={() => setTab(t.id)}>
                <span className="cx-libnav-l">{t.label}</span>
                <span className="cx-libnav-c">{libCount(t.id)}</span>
              </li>
            ))}
          </ul>
        </div>
        <div className="cx-side-section">
          <div className="cx-side-h">Filters</div>
          <CxLibraryFilters tab={tab} />
        </div>
        <div className="cx-side-section">
          <div className="cx-side-h">In current project</div>
          <ul className="cx-libcurrent">
            <li><span className="cx-lc-k">Motif</span><span className="cx-lc-v">{window.CxSchema.MOTIFS.find(m=>m.id===graph.nodes.motif?.data.motifId)?.name || '—'}</span></li>
            <li><span className="cx-lc-k">Material</span><span className="cx-lc-v">{window.CxSchema.MATERIALS.find(m=>m.id===graph.nodes.fabric?.data.materialId)?.name || '—'}</span></li>
            <li><span className="cx-lc-k">Craft</span><span className="cx-lc-v">{window.CxSchema.CRAFTS.find(c=>c.id===graph.nodes.craft?.data.type)?.label || '—'}</span></li>
          </ul>
        </div>
      </aside>

      <div className="cx-libroot">
        <div className="cx-libheader">
          <div>
            <div className="cx-pagetag">CUSTEX · LIBRARY · {tab.toUpperCase()}</div>
            <h1 className="cx-pageh1">{libTitle(tab)}</h1>
            <div className="cx-pagesub">{libSub(tab)}</div>
          </div>
          <div className="cx-libsearch">
            <span className="cx-libsearch-i">⌕</span>
            <input className="cx-libsearch-in" placeholder={`search ${tab}…`} value={q} onChange={e=>setQ(e.target.value)} />
          </div>
        </div>
        <div className="cx-libgrid">
          {tab==='motifs'    && <LibMotifs    q={q} graph={graph} setGraph={setGraph} setRoute={setRoute} hover={hover} setHover={setHover} />}
          {tab==='materials' && <LibMaterials q={q} graph={graph} setGraph={setGraph} setRoute={setRoute} />}
          {tab==='yarns'     && <LibYarns     q={q} graph={graph} setGraph={setGraph} setRoute={setRoute} />}
          {tab==='crafts'    && <LibCrafts    q={q} graph={graph} setGraph={setGraph} setRoute={setRoute} />}
        </div>
      </div>
    </div>
  );
}

function libCount(t) {
  if (t==='motifs')    return window.CxSchema.MOTIFS.length;
  if (t==='materials') return window.CxSchema.MATERIALS.length;
  if (t==='yarns')     return window.CxSchema.YARNS.length;
  if (t==='crafts')    return window.CxSchema.CRAFTS.length;
}
function libTitle(t) {
  return ({motifs:'Motifs', materials:'Materials', yarns:'Yarns', crafts:'Crafts'})[t];
}
function libSub(t) {
  return ({
    motifs:'Repeatable patterns ready to drop into the motif node.',
    materials:'Base fabrics — gsm, hand, weight class. Tap to wire into the fabric node.',
    yarns:'Spun threads for tufting, weaving, knitting. Multi-select feeds the yarn node.',
    crafts:'Fabrication processes with their parameter ranges and constraints.'
  })[t];
}

function CxLibraryFilters({ tab }) {
  // illustrative pills; not all wired to grid filtering — keeps the page calm
  const sets = {
    motifs:    [['Style','Folk','Geometric','Floral','Stripe'], ['Repeat','Tile','Half-drop','Mirror']],
    materials: [['Weight','Light','Medium','Heavy'], ['Hand','Crisp','Soft','Plush']],
    yarns:     [['Fiber','Cotton','Wool','Linen']],
    crafts:    [['Input','Material','Yarn'], ['Speed','Fast','Standard','Slow']],
  };
  const set = sets[tab] || [];
  return (
    <div className="cx-libfilters">
      {set.map((row, i) => (
        <div key={i} className="cx-filtgroup">
          <div className="cx-filtgroup-h">{row[0]}</div>
          <div className="cx-filtchips">
            {row.slice(1).map(c => <button key={c} className="cx-filtchip">{c}</button>)}
          </div>
        </div>
      ))}
    </div>
  );
}

function LibMotifs({ q, graph, setGraph, setRoute, hover, setHover }) {
  const motifs = window.CxSchema.MOTIFS.filter(m => !q || m.name.toLowerCase().includes(q.toLowerCase()));
  const current = graph.nodes.motif?.data.motifId;
  return motifs.map(m => (
    <div key={m.id}
      className={`cx-libcard cx-motif-card ${current===m.id?'sel':''}`}
      onMouseEnter={() => setHover(m.id)}
      onMouseLeave={() => setHover(null)}>
      <div className={`cx-libcard-art ${m.image ? 'cx-libcard-art--motif-raster' : ''}`} style={{ color:'#3a322a' }}>
        {m.image ? (
          <img
            src={(window.CxMotifHelpers && window.CxMotifHelpers.catalogAbsHref(m.image)) || m.image}
            alt=""
            className="cx-libcard-motif-fill"
          />
        ) : (
          <svg viewBox="0 0 80 80" width="100%" height="100%" style={{ display:'block' }}
               dangerouslySetInnerHTML={{ __html: cxLibMotifSvgInner(m) + (hover===m.id ? motifTilePattern(m) : '') }} />
        )}
      </div>
      <div className="cx-libcard-foot">
        <div className="cx-libcard-name">{m.name}</div>
        <div className="cx-libcard-meta">3-color · half-drop</div>
        <button className="cx-libcard-use" onClick={() => {
          setGraph(g => ({ ...g, nodes: { ...g.nodes, motif: { ...g.nodes.motif, data: { ...g.nodes.motif.data, motifId: m.id } } }}));
          setRoute('studio');
        }}>{current===m.id ? '✓ in use' : 'Use → studio'}</button>
      </div>
    </div>
  ));
}
function motifTilePattern() { return ''; }

function LibMaterials({ q, graph, setGraph, setRoute }) {
  const mats = window.CxSchema.MATERIALS.filter(m => !q || m.name.toLowerCase().includes(q.toLowerCase()));
  const current = graph.nodes.fabric?.data.materialId;
  return mats.map(m => (
    <div key={m.id} className={`cx-libcard cx-mat-card ${current===m.id?'sel':''}`}>
      <div className="cx-mat-swatch" style={{ background:m.swatch }}>
        <div className="cx-mat-weave" />
        <div className="cx-mat-gsm">{m.gsm} <span className="cx-mat-gsmu">gsm</span></div>
      </div>
      <div className="cx-libcard-foot">
        <div className="cx-libcard-name">{m.name}</div>
        <div className="cx-libcard-meta">{m.hand}</div>
        <div className="cx-mat-bar">
          <div className="cx-mat-bar-fill" style={{ width: `${Math.min(100, (m.gsm/450)*100)}%` }} />
        </div>
        <button className="cx-libcard-use" onClick={() => {
          if (!graph.nodes.fabric) { setRoute('studio'); return; }
          setGraph(g => ({ ...g, nodes: { ...g.nodes, fabric: { ...g.nodes.fabric, data: { ...g.nodes.fabric.data, materialId: m.id } } }}));
          setRoute('studio');
        }}>{current===m.id ? '✓ in use' : 'Use → studio'}</button>
      </div>
    </div>
  ));
}

function LibYarns({ q, graph, setGraph, setRoute }) {
  const yarns = window.CxSchema.YARNS.filter(y => !q || y.name.toLowerCase().includes(q.toLowerCase()));
  const selected = graph.nodes.yarn?.data.selected || [];
  return yarns.map(y => (
    <div key={y.id} className={`cx-libcard cx-yarn-card ${selected.includes(y.id)?'sel':''}`}>
      <div className="cx-yarn-swatch" style={{ background:y.swatch }}>
        <div className="cx-yarn-twist" style={{ background: `repeating-linear-gradient(135deg, transparent 0 6px, rgba(58,47,38,0.2) 6px 7px)` }} />
      </div>
      <div className="cx-libcard-foot">
        <div className="cx-libcard-name">{y.name}</div>
        <div className="cx-libcard-meta">{y.spin} · {y.note}</div>
        <button className="cx-libcard-use" onClick={() => {
          if (!graph.nodes.yarn) {
            // hint: switching craft to a yarn-craft is needed first
            setRoute('studio'); return;
          }
          setGraph(g => {
            const cur = g.nodes.yarn.data.selected || [];
            const next = cur.includes(y.id) ? cur.filter(x => x!==y.id) : [...cur, y.id];
            return { ...g, nodes: { ...g.nodes, yarn: { ...g.nodes.yarn, data: { ...g.nodes.yarn.data, selected: next } } } };
          });
        }}>{selected.includes(y.id) ? '✓ added' : 'Add yarn'}</button>
      </div>
    </div>
  ));
}

function LibCrafts({ q, graph, setGraph, setRoute }) {
  const crafts = window.CxSchema.CRAFTS.filter(c => !q || c.label.toLowerCase().includes(q.toLowerCase()));
  const current = graph.nodes.craft?.data.type;
  return crafts.map(c => (
    <div key={c.id} className={`cx-libcard cx-craft-card ${current===c.id?'sel':''}`}>
      <div className="cx-craft-tag">
        <span className="cx-craft-short">{c.short}</span>
        <span className="cx-craft-input">in · {c.inputKind}</span>
      </div>
      <div className="cx-libcard-foot">
        <div className="cx-libcard-name">{c.label}</div>
        <div className="cx-libcard-meta">{c.note}</div>
        <div className="cx-craft-params">
          {Object.entries(c.params).slice(0,2).map(([k,p]) => (
            <div key={k} className="cx-craft-param">
              <span className="cx-craft-pk">{p.label || k}</span>
              <span className="cx-craft-pv">{(p.options ? p.options.join(' · ') : (p.min!=null?`${p.min}–${p.max}${p.unit||''}`:'—')).slice(0, 28)}</span>
            </div>
          ))}
        </div>
        <button className="cx-libcard-use" onClick={() => {
          // We can't fully wire craft change here without the editor's reshape logic; just navigate.
          setRoute('studio');
        }}>{current===c.id ? '✓ in use' : 'Open in studio'}</button>
      </div>
    </div>
  ));
}

// ═════════════════════════════════════════════════════════════════
// MANUFACTURE — satellite workshop network
// ═════════════════════════════════════════════════════════════════

const WORKSHOPS = [
  { id:'wuzhen',    name:'Camden Loom Co.',      city:'Camden',      km:4.2, status:'open',  load:0.55, machines:['8-shaft loom','Jacquard 1280'], craft:'weaving',   eta:6,  rating:4.9, runs:213, lat:51.5412, lng:-0.1420 },
  { id:'mei_tuft',  name:'Bankside Tuft Studio', city:'Southwark',   km:6.8, status:'open',  load:0.32, machines:['Tufting gun','Backing press'],  craft:'tufting',   eta:8,  rating:4.7, runs:128, lat:51.5038, lng:-0.0916 },
  { id:'atelier',   name:'Hackney Stitch House', city:'Hackney',     km:9.1, status:'busy',  load:0.91, machines:['Embroidery 12-head','ZSK'],     craft:'embroidery',eta:14, rating:4.8, runs:341, lat:51.5450, lng:-0.0553 },
  { id:'inkstone',  name:'Wharfside Print Lab',  city:'Canary Wharf',km:11.4,status:'open',  load:0.45, machines:['DTF','Reactive printer'],       craft:'printing',  eta:5,  rating:4.6, runs:402, lat:51.5054, lng:-0.0235 },
  { id:'silkbow',   name:'Wandsworth Knit Co.',  city:'Wandsworth',  km:14.0,status:'open',  load:0.40, machines:['Stoll CMS', '12gg'],            craft:'knitting',  eta:7,  rating:4.5, runs:67,  lat:51.4568, lng:-0.1810 },
  { id:'mori',      name:'Greenwich Print House',city:'Greenwich',   km:18.2,status:'queued',load:0.78, machines:['Sublimation','Pigment'],        craft:'printing',  eta:9,  rating:4.4, runs:96,  lat:51.4826, lng:0.0059 },
  { id:'redthread', name:'Red Thread Embroidery',city:'Hammersmith', km:22.5,status:'open',  load:0.20, machines:['Tajima 6-head','Hand-station'], craft:'embroidery',eta:11, rating:4.9, runs:184, lat:51.4927, lng:-0.2230 },
  { id:'riverloom', name:'Riverloom Works',      city:'Lambeth',     km:7.6, status:'open',  load:0.36, machines:['Rapier loom','Finishing line'], craft:'weaving',   eta:6,  rating:4.6, runs:121, lat:51.4898, lng:-0.1128 },
  { id:'shoreline', name:'Shoreline Embroidery', city:'Islington',   km:8.8, status:'busy',  load:0.83, machines:['Tajima 8-head','Laser cutter'], craft:'embroidery',eta:12, rating:4.7, runs:206, lat:51.5358, lng:-0.1021 },
  { id:'threadlab', name:'Threadlab Knit Co.',   city:'Clapham',     km:10.3,status:'open',  load:0.47, machines:['Flatbed knit','Steam set'],     craft:'knitting',  eta:7,  rating:4.5, runs:88,  lat:51.4645, lng:-0.1380 },
  { id:'fleetprint',name:'Fleet Print Mill',     city:'Tower Hamlets',km:12.1,status:'open', load:0.51, machines:['Pigment line','Fixation oven'], craft:'printing',  eta:6,  rating:4.6, runs:173, lat:51.5155, lng:-0.0626 },
  { id:'northpile', name:'Northpile Tuft House', city:'Haringey',    km:13.4,status:'queued',load:0.72, machines:['Cut pile gun','Shear table'],    craft:'tufting',   eta:10, rating:4.4, runs:94,  lat:51.5871, lng:-0.1016 },
  { id:'dockweave', name:'Dock Weave Station',   city:'Deptford',    km:15.0,status:'open',  load:0.42, machines:['Jacquard loom','Warp prep'],    craft:'weaving',   eta:8,  rating:4.5, runs:109, lat:51.4797, lng:-0.0246 },
  { id:'pearlstitch',name:'Pearl Stitch House',  city:'Chelsea',     km:16.8,status:'busy',  load:0.87, machines:['Embroidery 10-head','Bead set'],craft:'embroidery',eta:13, rating:4.8, runs:231, lat:51.4876, lng:-0.1680 },
  { id:'selprint',  name:'Selvedge Print Lab',   city:'Lewisham',    km:18.6,status:'open',  load:0.44, machines:['Reactive print','Dry tunnel'],  craft:'printing',  eta:7,  rating:4.5, runs:132, lat:51.4586, lng:-0.0102 },
  { id:'westloop',  name:'Westloop Knitroom',    city:'Ealing',      km:20.1,status:'open',  load:0.39, machines:['Stoll ADF','Linking'],          craft:'knitting',  eta:8,  rating:4.4, runs:76,  lat:51.5131, lng:-0.3035 },
  { id:'tuftdock',  name:'Tuftdock Studio',      city:'Bermondsey',  km:21.7,status:'queued',load:0.69, machines:['Loop pile gun','Latex coat'],     craft:'tufting',   eta:11, rating:4.3, runs:69,  lat:51.4979, lng:-0.0635 },
];
const LONDON_ACTIVITY_POINTS = [
  { lat:51.516, lng:-0.14, craft:'embroidery' }, { lat:51.511, lng:-0.12, craft:'printing' }, { lat:51.505, lng:-0.10, craft:'tufting' }, { lat:51.499, lng:-0.09, craft:'weaving' }, { lat:51.495, lng:-0.12, craft:'knitting' },
  { lat:51.520, lng:-0.11, craft:'printing' },   { lat:51.526, lng:-0.09, craft:'tufting' },  { lat:51.532, lng:-0.08, craft:'weaving' }, { lat:51.538, lng:-0.06, craft:'embroidery' }, { lat:51.543, lng:-0.10, craft:'knitting' },
  { lat:51.548, lng:-0.13, craft:'embroidery' }, { lat:51.553, lng:-0.16, craft:'printing' }, { lat:51.507, lng:-0.03, craft:'weaving' }, { lat:51.498, lng:-0.02, craft:'tufting' }, { lat:51.488, lng:-0.01, craft:'knitting' },
  { lat:51.477, lng:-0.04, craft:'embroidery' }, { lat:51.468, lng:-0.09, craft:'printing' }, { lat:51.460, lng:-0.14, craft:'tufting' }, { lat:51.454, lng:-0.19, craft:'weaving' }, { lat:51.470, lng:-0.22, craft:'knitting' },
  { lat:51.486, lng:-0.20, craft:'embroidery' }, { lat:51.503, lng:-0.18, craft:'printing' }, { lat:51.514, lng:-0.17, craft:'tufting' }, { lat:51.525, lng:-0.15, craft:'weaving' }, { lat:51.535, lng:-0.12, craft:'knitting' },
  { lat:51.558, lng:-0.20, craft:'embroidery' }, { lat:51.561, lng:-0.11, craft:'printing' }, { lat:51.555, lng:-0.04, craft:'tufting' }, { lat:51.546, lng:-0.00, craft:'weaving' }, { lat:51.533, lng:0.01, craft:'knitting' },
  { lat:51.520, lng:0.00, craft:'embroidery' },  { lat:51.509, lng:0.02, craft:'printing' },  { lat:51.496, lng:0.01, craft:'tufting' },  { lat:51.484, lng:-0.01, craft:'weaving' }, { lat:51.472, lng:-0.02, craft:'knitting' },
  { lat:51.462, lng:-0.05, craft:'embroidery' }, { lat:51.452, lng:-0.08, craft:'printing' }, { lat:51.446, lng:-0.12, craft:'tufting' }, { lat:51.444, lng:-0.16, craft:'weaving' }, { lat:51.448, lng:-0.21, craft:'knitting' },
  { lat:51.456, lng:-0.24, craft:'embroidery' }, { lat:51.468, lng:-0.25, craft:'printing' }, { lat:51.482, lng:-0.24, craft:'tufting' }, { lat:51.497, lng:-0.23, craft:'weaving' }, { lat:51.512, lng:-0.22, craft:'knitting' },
];
const ACTIVITY_CRAFT_COLORS = {
  embroidery: '#944a20',
  printing: '#3f5fa8',
  tufting: '#d9a441',
  weaving: '#7d946d',
  knitting: '#8b5a8c',
};

function workshopMatchesQuery(w, q) {
  if (!q) return true;
  const craft = window.CxSchema.CRAFTS.find(c => c.id === w.craft) || {};
  const blob = [w.name, w.city, w.id, w.craft, craft.label, craft.short, ...(w.machines || [])].join(' ').toLowerCase();
  return blob.includes(q);
}

function CxManufacture({ setRoute, graph, validation, theme }) {
  const [selected, setSelected] = pS('mei_tuft');
  const [searchQ, setSearchQ] = pS('');
  const qNorm = searchQ.trim().toLowerCase();
  const filtered = pM(() => WORKSHOPS.filter(w => workshopMatchesQuery(w, qNorm)), [qNorm]);
  const sel = WORKSHOPS.find(w => w.id === selected);
  const craftId = graph.nodes.craft?.data.type;
  const compatible = WORKSHOPS.filter(w => w.craft === craftId);
  const ready = validation.length === 0;
  const openNow = filtered.filter(w => w.status === 'open').length;

  pE(() => {
    if (!qNorm || !filtered.length) return;
    if (!filtered.some(w => w.id === selected)) setSelected(filtered[0].id);
  }, [qNorm, filtered, selected]);

  return (
    <div className="cx-main cx-page-mfg">
      <aside className="cx-sidebar">
        <div className="cx-side-section">
          <div className="cx-side-h">This run</div>
          <div className="cx-mfg-job">
            <div className="cx-mfg-job-h">
              <span className="cx-mfg-job-tag">UNASSIGNED</span>
              <span className="cx-mfg-job-id">CXR-2046</span>
            </div>
            <div className="cx-mfg-job-name">Studio Holland · Spring</div>
            <div className="cx-mfg-job-meta">
              {window.CxSchema.PRODUCTS.find(p=>p.id===graph.nodes.product?.data.selected)?.name || '—'}
              {' · '}
              {window.CxSchema.CRAFTS.find(c=>c.id===craftId)?.short || '—'}
              {' · qty '}{graph.nodes.output?.data.qty || 0}
            </div>
            <div className={`cx-mfg-job-status ${ready?'ok':'err'}`}>
              {ready ? '● tech-pack ready' : '● fix issues before dispatch'}
            </div>
          </div>
        </div>
        <div className="cx-side-section">
          <div className="cx-side-h">Filter</div>
          <div className="cx-mfg-filt">
            <button className="cx-filtchip active">Within 25km</button>
            <button className="cx-filtchip">All crafts</button>
            <button className="cx-filtchip">Open now</button>
          </div>
        </div>
        <div className="cx-side-section">
          <div className="cx-side-h">Recent dispatch</div>
          <ul className="cx-mfg-recent">
            <li><span className="cx-mfg-r-d ok"/>CXR-2031 · Hackney Stitch House · <span className="cx-mfg-r-t">delivered</span></li>
            <li><span className="cx-mfg-r-d busy"/>CXR-2038 · Bankside Tuft · <span className="cx-mfg-r-t">on loom</span></li>
            <li><span className="cx-mfg-r-d busy"/>CXR-2041 · Camden Loom Co. · <span className="cx-mfg-r-t">queued</span></li>
            <li><span className="cx-mfg-r-d ok"/>CXR-2024 · Wharfside Print Lab · <span className="cx-mfg-r-t">delivered</span></li>
          </ul>
        </div>
      </aside>

      <div className="cx-mfgroot">
        <div className="cx-mfgheader">
          <div>
            <div className="cx-pagetag">CUSTEX · MANUFACTURE · SATELLITE NETWORK</div>
            <h1 className="cx-pageh1">Workshops within reach</h1>
            <div className="cx-pagesub">
              {qNorm ? `${filtered.length} match${filtered.length === 1 ? '' : 'es'} · ` : `${WORKSHOPS.length} workshops · `}
              5 crafts · cluster radius 25 km. Custex routes the tech-pack to whoever&rsquo;s ready first.
            </div>
          </div>
          <div className="cx-mfgheader-r">
            <div className="cx-libsearch cx-mfgsearch">
              <span className="cx-libsearch-i">⌕</span>
              <input
                type="search"
                className="cx-libsearch-in"
                placeholder="Search name, city, craft, equipment…"
                value={searchQ}
                onChange={e => setSearchQ(e.target.value)}
                aria-label="Search workshops"
              />
            </div>
            <div className="cx-mfgkpis">
              <div className="cx-kpi"><span className="cx-kpi-v">{filtered.length}</span><span className="cx-kpi-l">{qNorm ? 'matches' : 'workshops'}</span></div>
              <div className="cx-kpi"><span className="cx-kpi-v">{openNow}</span><span className="cx-kpi-l">open now</span></div>
              <div className="cx-kpi"><span className="cx-kpi-v">~6d</span><span className="cx-kpi-l">est. lead time</span></div>
            </div>
          </div>
        </div>

        <div className="cx-mfgmap-wrap">
          <CxNetworkMap selected={selected} setSelected={setSelected} compatible={compatible.map(w=>w.id)} />
          <div className="cx-mfg-legend">
            <div><span className="cx-leg-dot ok"/>open</div>
            <div><span className="cx-leg-dot busy"/>busy</div>
            <div><span className="cx-leg-dot queued"/>queued</div>
            <div><span className="cx-leg-cap"/>compatible w/ this craft</div>
          </div>
        </div>

        <div className="cx-mfg-bottom">
          <div className="cx-mfg-list">
            <div className="cx-mfg-list-h">
              <span>Workshop</span>
              <span>Craft</span>
              <span>Load</span>
              <span>Lead</span>
              <span>Distance</span>
              <span></span>
            </div>
            {filtered.length === 0 && (
              <div className="cx-mfg-empty">No workshops match &ldquo;{searchQ.trim()}&rdquo;. Try another name, city, or craft.</div>
            )}
            {filtered.map(w => (
              <div key={w.id}
                className={`cx-mfg-row ${selected===w.id?'sel':''} ${compatible.find(c=>c.id===w.id)?'compat':''}`}
                onClick={() => setSelected(w.id)}>
                <div className="cx-mfg-r-name">
                  <span className={`cx-mfg-r-d ${w.status==='busy'?'busy':(w.status==='queued'?'queued':'ok')}`}/>
                  <div>
                    <div className="cx-mfg-r-n">{w.name}</div>
                    <div className="cx-mfg-r-c">{w.city}</div>
                  </div>
                </div>
                <div className="cx-mfg-r-craft"><span className="cx-mfg-tag">{(window.CxSchema.CRAFTS.find(c=>c.id===w.craft)||{}).short}</span></div>
                <div className="cx-mfg-r-load">
                  <div className="cx-mfg-loadbar"><div className="cx-mfg-loadfill" style={{ width:`${w.load*100}%` }}/></div>
                  <span className="cx-mfg-loadn">{Math.round(w.load*100)}%</span>
                </div>
                <div className="cx-mfg-r-eta">{w.eta}d</div>
                <div className="cx-mfg-r-km">{w.km} km</div>
                <div className="cx-mfg-r-act">
                  <button className="cx-mfg-disp" disabled={w.craft!==craftId || !ready}>Dispatch</button>
                </div>
              </div>
            ))}
          </div>

          <div className="cx-mfg-detail">
            <div className="cx-mfg-detail-h">
              <div>
                <div className="cx-pagetag">{(sel.craft||'').toUpperCase()} · WORKSHOP</div>
                <div className="cx-mfg-detail-name">{sel.name}</div>
                <div className="cx-mfg-detail-city">{sel.city} · {sel.km} km</div>
              </div>
              <span className={`cx-mfg-detail-stat ${sel.status}`}>{sel.status}</span>
            </div>
            <div className="cx-mfg-detail-stats">
              <div><span className="cx-kpi-v">{sel.rating}</span><span className="cx-kpi-l">rating</span></div>
              <div><span className="cx-kpi-v">{sel.runs}</span><span className="cx-kpi-l">runs</span></div>
              <div><span className="cx-kpi-v">{sel.eta}d</span><span className="cx-kpi-l">est. lead</span></div>
            </div>
            <div className="cx-mfg-detail-h2">Equipment</div>
            <ul className="cx-mfg-machines">
              {sel.machines.map(m => <li key={m}>{m}</li>)}
            </ul>
            <div className="cx-mfg-detail-h2">Capability fit</div>
            <div className="cx-mfg-fit">
              <span className={`cx-mfg-fitb ${sel.craft===craftId?'ok':'bad'}`}>
                {sel.craft===craftId ? '✓ accepts ' : '✗ does not accept '}{(window.CxSchema.CRAFTS.find(c=>c.id===craftId)||{}).short || '—'}
              </span>
            </div>
            <button className="cx-mfg-cta" disabled={sel.craft!==craftId || !ready}>
              Dispatch tech-pack → {sel.name}
            </button>
            <div className="cx-mfg-foot">
              {sel.craft!==craftId
                ? `Switch craft to ${(window.CxSchema.CRAFTS.find(c=>c.id===sel.craft)||{}).label} to dispatch here.`
                : (ready ? 'Tech-pack will transmit on confirmation.' : 'Resolve graph errors in Studio first.')}
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

function CxNetworkMap({ selected, setSelected, compatible }) {
  const mapRef = pR(null);
  const mapInstRef = pR(null);
  const markerLayerRef = pR(null);
  const activityLayerRef = pR(null);
  const [leafletReady, setLeafletReady] = pS(!!window.L);

  pE(() => {
    if (window.L) return;
    const t = setTimeout(() => setLeafletReady(!!window.L), 120);
    return () => clearTimeout(t);
  }, [leafletReady]);

  pE(() => {
    if (!leafletReady || !mapRef.current || !window.L) return undefined;
    if (mapInstRef.current) {
      try {
        mapInstRef.current.remove();
      } catch (_) { /* ignore */ }
      mapInstRef.current = null;
      markerLayerRef.current = null;
      activityLayerRef.current = null;
    }
    const el = mapRef.current;
    const map = window.L.map(el, {
      zoomControl: false,
      attributionControl: true,
      scrollWheelZoom: true,
      doubleClickZoom: false,
    }).setView([51.5074, -0.1278], 11);
    window.L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
      maxZoom: 19,
      attribution: '&copy; OpenStreetMap contributors',
    }).addTo(map);
    markerLayerRef.current = window.L.layerGroup().addTo(map);
    activityLayerRef.current = window.L.layerGroup().addTo(map);
    mapInstRef.current = map;

    const bump = () => {
      try {
        map.invalidateSize({ animate: false });
      } catch (_) { /* ignore */ }
    };
    bump();
    requestAnimationFrame(bump);
    const onWinResize = () => bump();
    window.addEventListener('resize', onWinResize);
    let ro;
    if (typeof ResizeObserver !== 'undefined') {
      ro = new ResizeObserver(() => bump());
      ro.observe(el);
    }
    return () => {
      window.removeEventListener('resize', onWinResize);
      if (ro) ro.disconnect();
      try {
        map.remove();
      } catch (_) { /* ignore */ }
      mapInstRef.current = null;
      markerLayerRef.current = null;
      activityLayerRef.current = null;
    };
  }, [leafletReady]);

  pE(() => {
    if (!markerLayerRef.current || !activityLayerRef.current || !window.L) return;
    activityLayerRef.current.clearLayers();
    LONDON_ACTIVITY_POINTS.forEach((p, i) => {
      const dot = ACTIVITY_CRAFT_COLORS[p.craft] || '#8a7d68';
      window.L.circleMarker([p.lat, p.lng], {
        radius: i % 4 === 0 ? 2.8 : 2.1,
        color: dot,
        weight: 0.6,
        fillColor: dot,
        fillOpacity: i % 5 === 0 ? 0.7 : 0.48,
        opacity: 0.55,
        className: 'cx-map-activity-dot',
      }).addTo(activityLayerRef.current);
    });

    markerLayerRef.current.clearLayers();
    WORKSHOPS.forEach((w) => {
      const isSel = selected === w.id;
      const isCompat = compatible.includes(w.id);
      const statusClass = w.status === 'busy' ? 'busy' : (w.status === 'queued' ? 'queued' : 'ok');
      const icon = window.L.divIcon({
        className: 'cx-map-pin-wrap',
        html: `<span class="cx-map-pin ${statusClass} ${isCompat ? 'compat' : ''} ${isSel ? 'sel' : ''}"></span>`,
        iconSize: [20, 20],
        iconAnchor: [10, 10],
      });
      const marker = window.L.marker([w.lat, w.lng], { icon }).addTo(markerLayerRef.current);
      marker.bindTooltip(`${w.name} · ${w.km} km · ${w.eta}d`, { direction: 'top', offset: [0, -10] });
      marker.on('click', () => setSelected(w.id));
    });
  }, [selected, compatible, setSelected]);

  if (!leafletReady) {
    return <div className="cx-mfgmap cx-mfgmap-loading">Loading London map…</div>;
  }
  return <div className="cx-mfgmap" ref={mapRef} />;
}

// ═════════════════════════════════════════════════════════════════
// COMMUNITY — gallery of finished projects
// ═════════════════════════════════════════════════════════════════

/* Gallery covers C1–C16 (Community only). C16 uses `community/covers/16.jpg`. */
const COMMUNITY_COVER_PATHS = [
  'community/covers/c1.jpg',
  'community/covers/c2.jpg',
  'community/covers/c3.png',
  'community/covers/c4.jpg',
  'community/covers/c5.png',
  'community/covers/c6.png',
  'community/covers/c7.png',
  'community/covers/c8.jpg',
  'community/covers/c9.png',
  'community/covers/c10.png',
  'community/covers/c11.png',
  'community/covers/c12.jpg',
  'community/covers/c13.jpg',
  'community/covers/c14.webp',
  'community/covers/c15.jpg',
  'community/covers/16.jpg',
];

/* Titles + crafts per brief. `craft`: printing | knitting | tufting (schema ids). `product` must exist in CxSchema.PRODUCTS. Optional `motif` id for Remix → studio when catalog has entries. */
const COMMUNITY = [
  { id:'p1',  coverImage: COMMUNITY_COVER_PATHS[0],  title:'Pepper table mat',          by:'Custex Community', craft:'printing',  product:'unfinished_material', ground:'#e8e0d4', accent:'#944a20', likes:210, runs:28, comments:11, featured:true,  size:'45×32 cm', tagline:'Digital print · pepper motif on table mat.' },
  { id:'p2',  coverImage: COMMUNITY_COVER_PATHS[1],  title:'Pepper cushion',            by:'Custex Community', craft:'printing',  product:'cushion',           ground:'#d4c8b2', accent:'#3a4f6b', likes:176, runs:19, comments:8  },
  { id:'p3',  coverImage: COMMUNITY_COVER_PATHS[2],  title:'Dotty tablecloth',          by:'Custex Community', craft:'printing',  product:'curtain',           ground:'#e1d8c2', accent:'#944a20', likes:142, runs:15, comments:6  },
  { id:'p4',  coverImage: COMMUNITY_COVER_PATHS[3],  title:'Pink tablet',               by:'Custex Community', craft:'printing',  product:'unfinished_material', ground:'#f0d8dc', accent:'#c45c5c', likes:98,  runs:12, comments:4  },
  { id:'p5',  coverImage: COMMUNITY_COVER_PATHS[4],  title:'Mushroom blanket',          by:'Custex Community', craft:'printing',  product:'blanket',           ground:'#dccfa8', accent:'#7d946d', likes:164, runs:21, comments:9  },
  { id:'p6',  coverImage: COMMUNITY_COVER_PATHS[5],  title:'Spring tablecloth',         by:'Custex Community', craft:'printing',  product:'curtain',           ground:'#efeae0', accent:'#3a4f6b', likes:131, runs:14, comments:5  },
  { id:'p7',  coverImage: COMMUNITY_COVER_PATHS[6],  title:'Flora blanket',             by:'Custex Community', craft:'printing',  product:'blanket',           ground:'#d8e4d4', accent:'#7d946d', likes:189, runs:18, comments:10 },
  { id:'p8',  coverImage: COMMUNITY_COVER_PATHS[7],  title:'Couch case',                by:'Custex Community', craft:'knitting',  product:'cushion',           ground:'#e6e0d8', accent:'#9a8a70', likes:154, runs:16, comments:7  },
  { id:'p9',  coverImage: COMMUNITY_COVER_PATHS[8],  title:'Flora coriander',           by:'Custex Community', craft:'printing',  product:'curtain',           ground:'#e1d8c2', accent:'#944a20', likes:117, runs:11, comments:4  },
  { id:'p10', coverImage: COMMUNITY_COVER_PATHS[9],  title:'Pepper cushion',            by:'Custex Community', craft:'printing',  product:'cushion',           ground:'#d4c8b2', accent:'#3a4f6b', likes:103, runs:10, comments:3  },
  { id:'p11', coverImage: COMMUNITY_COVER_PATHS[10], title:'Cabbage linen tablecloth',  by:'Custex Community', craft:'printing',  product:'curtain',           ground:'#e8ead8', accent:'#7d946d', likes:145, runs:13, comments:6  },
  { id:'p12', coverImage: COMMUNITY_COVER_PATHS[11], title:'Ink cushion',               by:'Custex Community', craft:'printing',  product:'cushion',           ground:'#c8c4bc', accent:'#3a4f6b', likes:128, runs:12, comments:5  },
  { id:'p13', coverImage: COMMUNITY_COVER_PATHS[12], title:'Ink tablecloth',            by:'Custex Community', craft:'printing',  product:'curtain',           ground:'#d0ccc4', accent:'#3a4f6b', likes:112, runs:9,  comments:4  },
  { id:'p14', coverImage: COMMUNITY_COVER_PATHS[13], title:'Moss cushion',              by:'Custex Community', craft:'tufting',   product:'cushion',           ground:'#c9d4c0', accent:'#5a7d8c', likes:201, runs:24, comments:12 },
  { id:'p15', coverImage: COMMUNITY_COVER_PATHS[14], title:'Ink carpet',                by:'Custex Community', craft:'tufting',   product:'rug',               ground:'#b8b4ac', accent:'#3a4f6b', likes:167, runs:17, comments:8  },
  { id:'p16', coverImage: COMMUNITY_COVER_PATHS[15], title:'Ink carpet',                by:'Custex Community', craft:'knitting',  product:'rug',               ground:'#b8b4ac', accent:'#3a4f6b', likes:88,  runs:8,  comments:3  },
];

function CxCommunity({ setRoute, setGraph, theme }) {
  const [filter, setFilter] = pS('all');
  const [sort, setSort]     = pS('hot');
  const items = COMMUNITY.filter(p => filter==='all' || p.craft===filter);
  const sorted = [...items].sort((a,b) => sort==='hot' ? b.likes - a.likes : (sort==='runs' ? b.runs - a.runs : 0));
  const featured = COMMUNITY.find(p => p.featured);

  return (
    <div className="cx-main cx-page-com">
      <aside className="cx-sidebar">
        <div className="cx-side-section">
          <div className="cx-side-h">Sort</div>
          <div className="cx-com-sort">
            {[['hot','Most loved'],['runs','Most remixed'],['new','New']].map(([id,l]) => (
              <button key={id} className={`cx-com-sortb ${sort===id?'sel':''}`} onClick={()=>setSort(id)}>{l}</button>
            ))}
          </div>
        </div>
        <div className="cx-side-section">
          <div className="cx-side-h">Craft</div>
          <ul className="cx-com-fset">
            <li className={filter==='all'?'sel':''} onClick={()=>setFilter('all')}>
              <span>All</span><span className="cx-com-c">{COMMUNITY.length}</span>
            </li>
            {window.CxSchema.CRAFTS.map(c => {
              const n = COMMUNITY.filter(p=>p.craft===c.id).length;
              return (
                <li key={c.id} className={filter===c.id?'sel':''} onClick={()=>setFilter(c.id)}>
                  <span>{c.label}</span><span className="cx-com-c">{n}</span>
                </li>
              );
            })}
          </ul>
        </div>
        <div className="cx-side-section">
          <div className="cx-side-h">Featured studios</div>
          <ul className="cx-com-studios">
            <li><span className="cx-com-av" style={{background:'#944a20'}}>SH</span>Studio Holland</li>
            <li><span className="cx-com-av" style={{background:'#7d946d'}}>CL</span>Camden Loom Co.</li>
            <li><span className="cx-com-av" style={{background:'#3a4f6b'}}>WP</span>Wharfside Print Lab</li>
            <li><span className="cx-com-av" style={{background:'#9a8a70'}}>BT</span>Bankside Tuft</li>
          </ul>
        </div>
      </aside>

      <div className="cx-comroot">
        <div className="cx-comheader">
          <div>
            <div className="cx-pagetag">CUSTEX · COMMUNITY · GALLERY</div>
            <h1 className="cx-pageh1">Made on Custex</h1>
            <div className="cx-pagesub">Finished work from studios, e-commerce shops, and weekend designers. Tap remix to fork the graph into your studio.</div>
          </div>
          <div className="cx-comkpis">
            <div className="cx-kpi"><span className="cx-kpi-v">{COMMUNITY.length}</span><span className="cx-kpi-l">projects</span></div>
            <div className="cx-kpi"><span className="cx-kpi-v">{COMMUNITY.reduce((s,p)=>s+p.runs,0)}</span><span className="cx-kpi-l">remixes</span></div>
            <div className="cx-kpi"><span className="cx-kpi-v">{COMMUNITY.reduce((s,p)=>s+p.likes,0)}</span><span className="cx-kpi-l">loves</span></div>
          </div>
        </div>

        <div className="cx-com-feature">
          <ComFeatured p={featured} setRoute={setRoute} setGraph={setGraph} />
        </div>

        <div className="cx-comgrid">
          {sorted.filter(p=>!p.featured).map(p => <ComCard key={p.id} p={p} setRoute={setRoute} setGraph={setGraph}/>)}
        </div>
      </div>
    </div>
  );
}

function comTile(p) {
  const href = p.coverImage ? cxCommunityCoverHref(p.coverImage) : '';
  return (
    <div className="cx-com-canvas" style={{ background: p.ground }}>
      {href ? (
        <img src={href} alt="" className="cx-com-cover-img" decoding="async" />
      ) : (
        <div className="cx-com-cover-placeholder" aria-hidden />
      )}
      <div className="cx-com-selvedge" />
    </div>
  );
}

function ComFeatured({ p, setRoute, setGraph }) {
  if (!p) return null;
  return (
    <div className="cx-com-feat">
      <div className="cx-com-feat-art">
        {comTile(p)}
      </div>
      <div className="cx-com-feat-meta">
        <div className="cx-pagetag">FEATURED · {p.craft.toUpperCase()}</div>
        <h2 className="cx-com-feat-title">{p.title}</h2>
        <div className="cx-com-feat-tag">by <span>{p.by}</span> · {p.size}</div>
        <p className="cx-com-feat-line">{p.tagline}</p>
        <div className="cx-com-feat-stats">
          <span><b>{p.likes}</b> loves</span>
          <span><b>{p.runs}</b> remixes</span>
          <span><b>{p.comments}</b> notes</span>
        </div>
        <div className="cx-com-feat-actions">
          <button className="cx-com-remix" onClick={() => {
            // fork: set motif into current graph, navigate
            setGraph(g => (!g.nodes.motif || !p.motif) ? g : ({ ...g, nodes: { ...g.nodes, motif: { ...g.nodes.motif, data: { ...g.nodes.motif.data, motifId: p.motif, motifIds: [p.motif] } } } }));
            setRoute('studio');
          }}>Remix → studio</button>
          <button className="cx-com-view">View tech-pack</button>
        </div>
      </div>
    </div>
  );
}

function ComCard({ p, setRoute, setGraph }) {
  return (
    <article className="cx-com-card">
      <div className="cx-com-card-art">{comTile(p)}</div>
      <div className="cx-com-card-foot">
        <div className="cx-com-card-h">
          <span className="cx-com-card-t">{p.title}</span>
          <span className="cx-com-card-cr">{(window.CxSchema.CRAFTS.find(c=>c.id===p.craft)||{}).short}</span>
        </div>
        <div className="cx-com-card-by">by {p.by}</div>
        <div className="cx-com-card-stats">
          <span>♡ {p.likes}</span>
          <span>↻ {p.runs}</span>
          <span>· {p.comments}</span>
          <button className="cx-com-card-remix" onClick={() => {
            setGraph(g => (!g.nodes.motif || !p.motif) ? g : ({ ...g, nodes: { ...g.nodes, motif: { ...g.nodes.motif, data: { ...g.nodes.motif.data, motifId: p.motif, motifIds: [p.motif] } } } }));
            setRoute('studio');
          }}>Remix</button>
        </div>
      </div>
    </article>
  );
}

// ─────────── expose ───────────
window.CxPageRouter = CxPageRouter;
window.CxLibrary = CxLibrary;
window.CxManufacture = CxManufacture;
window.CxCommunity = CxCommunity;
