// ═══════════════════════════════════════════════════════
// MOCK DATA: Canifa Customer Knowledge Graph
// ═══════════════════════════════════════════════════════
const graphData = {
    nodes: [
        // === CUSTOMERS ===
        {id:'c1',name:'Nguyễn Thị Mai',type:'Customer',attrs:{age:'32',gender:'Nữ',segment:'VIP',city:'Hà Nội',totalOrders:'24',ltv:'12.5M'}},
        {id:'c2',name:'Trần Văn Hùng',type:'Customer',attrs:{age:'28',gender:'Nam',segment:'Regular',city:'TP.HCM',totalOrders:'8',ltv:'4.2M'}},
        {id:'c3',name:'Lê Phương Anh',type:'Customer',attrs:{age:'35',gender:'Nữ',segment:'VIP',city:'Đà Nẵng',totalOrders:'31',ltv:'18.7M'}},
        {id:'c4',name:'Phạm Minh Tuấn',type:'Customer',attrs:{age:'41',gender:'Nam',segment:'Premium',city:'Hà Nội',totalOrders:'15',ltv:'9.8M'}},
        {id:'c5',name:'Hoàng Thúy Linh',type:'Customer',attrs:{age:'26',gender:'Nữ',segment:'New',city:'TP.HCM',totalOrders:'3',ltv:'1.5M'}},

        // === PRODUCTS ===
        {id:'p1',name:'Áo Polo Classic',type:'Product',attrs:{sku:'8TS26C005',price:'399K',category:'Áo',stock:'245'}},
        {id:'p2',name:'Váy Midi Floral',type:'Product',attrs:{sku:'6VD24A012',price:'599K',category:'Váy',stock:'120'}},
        {id:'p3',name:'Quần Jeans Slim',type:'Product',attrs:{sku:'7QJ25B003',price:'499K',category:'Quần',stock:'180'}},
        {id:'p4',name:'Áo Khoác Bomber',type:'Product',attrs:{sku:'8AK26D008',price:'799K',category:'Áo khoác',stock:'95'}},
        {id:'p5',name:'Đầm Công Sở',type:'Product',attrs:{sku:'6DC24E015',price:'699K',category:'Đầm',stock:'75'}},
        {id:'p6',name:'Áo Sơ Mi Linen',type:'Product',attrs:{sku:'8SM26F002',price:'459K',category:'Áo',stock:'200'}},

        // === CATEGORIES ===
        {id:'cat1',name:'Thời trang Nữ',type:'Category',attrs:{products:'156',revenue:'2.3B'}},
        {id:'cat2',name:'Thời trang Nam',type:'Category',attrs:{products:'98',revenue:'1.1B'}},
        {id:'cat3',name:'Trẻ em',type:'Category',attrs:{products:'67',revenue:'680M'}},
        {id:'cat4',name:'Phụ kiện',type:'Category',attrs:{products:'45',revenue:'320M'}},

        // === BEHAVIORS ===
        {id:'b1',name:'Mua sắm cuối tuần',type:'Behavior',attrs:{frequency:'Weekly',channel:'Online+Offline'}},
        {id:'b2',name:'Flash Sale Hunter',type:'Behavior',attrs:{frequency:'Every sale',channel:'App'}},
        {id:'b3',name:'Gift Buyer',type:'Behavior',attrs:{frequency:'Seasonal',channel:'Offline'}},
        {id:'b4',name:'Browsing Only',type:'Behavior',attrs:{frequency:'Daily',channel:'Website'}},
        {id:'b5',name:'Loyal Repeater',type:'Behavior',attrs:{frequency:'Monthly',channel:'All'}},

        // === SEGMENTS ===
        {id:'s1',name:'Mom Shopper',type:'Segment',attrs:{size:'2,340',growth:'+12%',avgLTV:'8.5M'}},
        {id:'s2',name:'Young Professional',type:'Segment',attrs:{size:'1,850',growth:'+18%',avgLTV:'5.2M'}},
        {id:'s3',name:'Fashion Enthusiast',type:'Segment',attrs:{size:'980',growth:'+25%',avgLTV:'15.3M'}},
        {id:'s4',name:'Budget Conscious',type:'Segment',attrs:{size:'3,200',growth:'+5%',avgLTV:'2.1M'}},

        // === OCCASIONS ===
        {id:'o1',name:'Đi làm',type:'Occasion',attrs:{peak:'Mon-Fri'}},
        {id:'o2',name:'Đi chơi',type:'Occasion',attrs:{peak:'Weekend'}},
        {id:'o3',name:'Sự kiện',type:'Occasion',attrs:{peak:'Holiday'}},
        {id:'o4',name:'Mặc nhà',type:'Occasion',attrs:{peak:'Everyday'}},

        // === TRENDS ===
        {id:'t1',name:'Minimalism',type:'Trend',attrs:{strength:'Strong',since:'2025'}},
        {id:'t2',name:'Sustainable Fashion',type:'Trend',attrs:{strength:'Growing',since:'2024'}},
        {id:'t3',name:'Oversized Fit',type:'Trend',attrs:{strength:'Peak',since:'2025'}},
    ],
    edges: [
        // Customers → Products
        {source:'c1',target:'p1',label:'mua',weight:3},
        {source:'c1',target:'p2',label:'mua',weight:5},
        {source:'c1',target:'p5',label:'mua',weight:2},
        {source:'c2',target:'p1',label:'mua',weight:2},
        {source:'c2',target:'p3',label:'mua',weight:4},
        {source:'c2',target:'p4',label:'mua',weight:1},
        {source:'c3',target:'p2',label:'mua',weight:6},
        {source:'c3',target:'p5',label:'mua',weight:4},
        {source:'c3',target:'p6',label:'mua',weight:3},
        {source:'c4',target:'p3',label:'mua',weight:3},
        {source:'c4',target:'p4',label:'mua',weight:2},
        {source:'c4',target:'p6',label:'mua',weight:5},
        {source:'c5',target:'p1',label:'xem',weight:1},
        {source:'c5',target:'p2',label:'thích',weight:2},
        // Products → Categories
        {source:'p1',target:'cat2',label:'thuộc',weight:1},
        {source:'p2',target:'cat1',label:'thuộc',weight:1},
        {source:'p3',target:'cat2',label:'thuộc',weight:1},
        {source:'p4',target:'cat2',label:'thuộc',weight:1},
        {source:'p5',target:'cat1',label:'thuộc',weight:1},
        {source:'p6',target:'cat2',label:'thuộc',weight:1},
        // Customers → Behaviors
        {source:'c1',target:'b1',label:'hành vi',weight:2},
        {source:'c1',target:'b5',label:'hành vi',weight:3},
        {source:'c2',target:'b2',label:'hành vi',weight:2},
        {source:'c3',target:'b5',label:'hành vi',weight:3},
        {source:'c3',target:'b3',label:'hành vi',weight:2},
        {source:'c4',target:'b1',label:'hành vi',weight:1},
        {source:'c5',target:'b4',label:'hành vi',weight:2},
        {source:'c5',target:'b2',label:'hành vi',weight:1},
        // Customers → Segments
        {source:'c1',target:'s1',label:'thuộc nhóm',weight:2},
        {source:'c2',target:'s2',label:'thuộc nhóm',weight:2},
        {source:'c3',target:'s3',label:'thuộc nhóm',weight:3},
        {source:'c4',target:'s2',label:'thuộc nhóm',weight:1},
        {source:'c5',target:'s4',label:'thuộc nhóm',weight:1},
        // Products → Occasions
        {source:'p1',target:'o1',label:'phù hợp',weight:1},
        {source:'p1',target:'o2',label:'phù hợp',weight:1},
        {source:'p2',target:'o2',label:'phù hợp',weight:2},
        {source:'p2',target:'o3',label:'phù hợp',weight:1},
        {source:'p3',target:'o1',label:'phù hợp',weight:1},
        {source:'p4',target:'o2',label:'phù hợp',weight:2},
        {source:'p5',target:'o1',label:'phù hợp',weight:3},
        {source:'p5',target:'o3',label:'phù hợp',weight:2},
        {source:'p6',target:'o1',label:'phù hợp',weight:2},
        {source:'p6',target:'o4',label:'phù hợp',weight:1},
        // Categories → Trends
        {source:'cat1',target:'t1',label:'xu hướng',weight:2},
        {source:'cat1',target:'t2',label:'xu hướng',weight:1},
        {source:'cat2',target:'t3',label:'xu hướng',weight:3},
        {source:'cat2',target:'t1',label:'xu hướng',weight:2},
        // Segments → Behaviors
        {source:'s1',target:'b1',label:'đặc trưng',weight:2},
        {source:'s1',target:'b3',label:'đặc trưng',weight:1},
        {source:'s2',target:'b2',label:'đặc trưng',weight:2},
        {source:'s3',target:'b5',label:'đặc trưng',weight:3},
        {source:'s4',target:'b2',label:'đặc trưng',weight:3},
        {source:'s4',target:'b4',label:'đặc trưng',weight:2},
        // Cross
        {source:'c1',target:'c3',label:'tương tự',weight:2},
        {source:'p1',target:'p6',label:'mua kèm',weight:3},
        {source:'p2',target:'p5',label:'mua kèm',weight:2},
        {source:'t1',target:'t2',label:'liên quan',weight:2},
    ]
};

// ═══════════════════════════════════════════════════════
// COLOR PALETTE — warm tones matching Canifa
// ═══════════════════════════════════════════════════════
const typeColors = {
    'Customer':  '#6D28D9', // diamond/purple
    'Product':   '#DB2777', // pink
    'Category':  '#0891B2', // cyan
    'Behavior':  '#EA580C', // orange
    'Segment':   '#B45309', // gold
    'Occasion':  '#065F46', // green
    'Trend':     '#1D4ED8', // blue
};

// ═══════════════════════════════════════════════════════
// RENDER GRAPH — delayed init for iframe compatibility
// ═══════════════════════════════════════════════════════
const container = document.getElementById('graphContainer');
const svg = d3.select('#graphSvg');
let simulation, link, linkLabel, linkLabelBg, node, g, zoom;
let showLabels = true;
let selectedNode = null;

// Stats
document.getElementById('statNodes').textContent = graphData.nodes.length;
document.getElementById('statEdges').textContent = graphData.edges.length;
const uniqueTypes = [...new Set(graphData.nodes.map(n => n.type))];
document.getElementById('statTypes').textContent = uniqueTypes.length;

function initGraph(){
    const w = container.clientWidth || window.innerWidth;
    const h = container.clientHeight || (window.innerHeight - 54);
    svg.attr('width', w).attr('height', h);

    // Root group
    g = svg.append('g');

    // Defs
    const defs = svg.append('defs');
    defs.append('marker')
        .attr('id','arrowhead')
        .attr('viewBox','0 -5 10 10')
        .attr('refX',24).attr('refY',0)
        .attr('markerWidth',5).attr('markerHeight',5)
        .attr('orient','auto')
        .append('path')
        .attr('d','M0,-5L10,0L0,5')
        .attr('fill','#C4C0B8');

    // Simulation — stronger centering
    simulation = d3.forceSimulation(graphData.nodes)
        .force('link', d3.forceLink(graphData.edges).id(d => d.id).distance(d => 100 + (1/(d.weight||1)) * 25))
        .force('charge', d3.forceManyBody().strength(-280))
        .force('center', d3.forceCenter(w/2, h/2).strength(1))
        .force('collide', d3.forceCollide(30))
        .force('x', d3.forceX(w/2).strength(0.08))
        .force('y', d3.forceY(h/2).strength(0.08));

    // Links
    const linkGroup = g.append('g');
    link = linkGroup.selectAll('line')
        .data(graphData.edges)
        .enter().append('line')
        .attr('stroke','#E2E0D8')
        .attr('stroke-width', d => Math.max(1, (d.weight||1)*0.6))
        .attr('marker-end','url(#arrowhead)')
        .style('cursor','pointer')
        .on('click', onEdgeClick);

    // Link labels
    const linkLabelGroup = g.append('g');
    linkLabelBg = linkLabelGroup.selectAll('rect')
        .data(graphData.edges)
        .enter().append('rect')
        .attr('fill','rgba(245,244,240,0.9)')
        .attr('rx',4).attr('ry',4)
        .style('pointer-events','none');

    linkLabel = linkLabelGroup.selectAll('text')
        .data(graphData.edges)
        .enter().append('text')
        .text(d => d.label)
        .attr('font-size','9px')
        .attr('fill','#78716C')
        .attr('text-anchor','middle')
        .attr('dominant-baseline','middle')
        .attr('font-weight','500')
        .style('pointer-events','none')
        .style('font-family','Outfit,system-ui,sans-serif');

    // Nodes
    const nodeGroup = g.append('g');
    node = nodeGroup.selectAll('g')
        .data(graphData.nodes)
        .enter().append('g')
        .style('cursor','pointer')
        .call(d3.drag()
            .on('start', (e,d) => { if(!e.active) simulation.alphaTarget(0.3).restart(); d.fx=d.x;d.fy=d.y; })
            .on('drag', (e,d) => { d.fx=e.x;d.fy=e.y; })
            .on('end', (e,d) => { if(!e.active) simulation.alphaTarget(0); d.fx=null;d.fy=null; }))
        .on('click', onNodeClick)
        .on('mouseenter', onNodeHover)
        .on('mouseleave', onNodeLeave);

    // Outer ring
    node.append('circle')
        .attr('r',20)
        .attr('fill','none')
        .attr('stroke', d => typeColors[d.type])
        .attr('stroke-width',1.5)
        .attr('stroke-opacity',0.12)
        .attr('class','node-ring');

    // Inner circle
    node.append('circle')
        .attr('r',13)
        .attr('fill', d => typeColors[d.type])
        .attr('stroke','#fff')
        .attr('stroke-width',2.5)
        .attr('class','node-circle');

    // Node label
    node.append('text')
        .text(d => d.name.length > 10 ? d.name.substring(0,10)+'…' : d.name)
        .attr('dy', 28)
        .attr('text-anchor','middle')
        .attr('font-size','11px')
        .attr('fill','#78716C')
        .attr('font-weight','500')
        .style('pointer-events','none')
        .style('font-family','Outfit,system-ui,sans-serif');

    // Tick
    simulation.on('tick', () => {
        link.attr('x1',d=>d.source.x).attr('y1',d=>d.source.y)
            .attr('x2',d=>d.target.x).attr('y2',d=>d.target.y);

        linkLabel
            .attr('x',d=>(d.source.x+d.target.x)/2)
            .attr('y',d=>(d.source.y+d.target.y)/2);

        linkLabelBg.each(function(d, i){
            const tx = (d.source.x+d.target.x)/2;
            const ty = (d.source.y+d.target.y)/2;
            const textEl = linkLabel.nodes()[i];
            if(textEl){
                const bb = textEl.getBBox();
                d3.select(this)
                    .attr('x', tx - bb.width/2 - 4)
                    .attr('y', ty - bb.height/2 - 2)
                    .attr('width', bb.width + 8)
                    .attr('height', bb.height + 4);
            }
        });

        node.attr('transform', d => `translate(${d.x},${d.y})`);
    });

    // Zoom
    zoom = d3.zoom().scaleExtent([0.1,5]).on('zoom', e => g.attr('transform', e.transform));
    svg.call(zoom);

    // Auto-fit after simulation settles
    simulation.on('end', () => {
        fitGraphToView(w, h);
    });

    // Also fit after 2 seconds in case simulation still running
    setTimeout(() => fitGraphToView(w, h), 2000);

    // Click blank to deselect
    svg.on('mousemove', e => {
        const tt = document.getElementById('tooltip');
        if(tt.classList.contains('show')){ tt.style.left=(e.pageX+14)+'px'; tt.style.top=(e.pageY-8)+'px'; }
    });
    svg.on('click', () => { selectedNode=null; resetStyles(); document.getElementById('detailPanel').classList.remove('open'); });

    // Setup controls
    setupControls(w, h);
    setupSearch();
}

// Auto-fit: calculate bounding box of all nodes and center the view
function fitGraphToView(w, h){
    if(!graphData.nodes.length) return;
    let minX=Infinity, maxX=-Infinity, minY=Infinity, maxY=-Infinity;
    graphData.nodes.forEach(n => {
        if(n.x < minX) minX = n.x;
        if(n.x > maxX) maxX = n.x;
        if(n.y < minY) minY = n.y;
        if(n.y > maxY) maxY = n.y;
    });
    const padding = 80;
    const gw = maxX - minX + padding*2;
    const gh = maxY - minY + padding*2;
    const scale = Math.min(w/gw, h/gh, 1.2);
    const cx = (minX + maxX)/2;
    const cy = (minY + maxY)/2;
    svg.transition().duration(600).call(
        zoom.transform,
        d3.zoomIdentity.translate(w/2, h/2).scale(scale).translate(-cx, -cy)
    );
}

function resetStyles(){
    if(!node || !link) return;
    node.selectAll('.node-circle').attr('stroke','#fff').attr('stroke-width',2.5);
    node.selectAll('.node-ring').attr('stroke-opacity',0.12).attr('stroke-width',1.5);
    link.attr('stroke','#E2E0D8').attr('stroke-width', d => Math.max(1,(d.weight||1)*0.6));
}

function onNodeClick(event, d){
    event.stopPropagation();
    selectedNode = d;
    resetStyles();
    d3.select(this).select('.node-circle').attr('stroke',typeColors[d.type]).attr('stroke-width',3.5);
    d3.select(this).select('.node-ring').attr('stroke-opacity',0.4).attr('stroke-width',2.5);
    const connectedIds = new Set();
    link.each(function(l){
        if(l.source.id===d.id || l.target.id===d.id){
            d3.select(this).attr('stroke',typeColors[d.type]).attr('stroke-width', Math.max(2,(l.weight||1)));
            if(l.source.id===d.id) connectedIds.add(l.target.id);
            if(l.target.id===d.id) connectedIds.add(l.source.id);
        }
    });
    node.filter(n => connectedIds.has(n.id)).select('.node-ring').attr('stroke-opacity',0.3);
    showDetailPanel(d,'node');
}

function onEdgeClick(event,d){ event.stopPropagation(); showDetailPanel(d,'edge'); }

function onNodeHover(event,d){
    if(selectedNode?.id===d.id) return;
    d3.select(this).select('.node-ring').attr('stroke-opacity',0.35).attr('stroke-width',2);
    const tt = document.getElementById('tooltip');
    tt.innerHTML = `<div class="tooltip-name">${d.name}</div><div class="tooltip-type">${d.type}</div>`;
    tt.classList.add('show');
    tt.style.left = (event.pageX+14)+'px';
    tt.style.top = (event.pageY-8)+'px';
}

function onNodeLeave(event,d){
    if(selectedNode?.id===d.id) return;
    d3.select(this).select('.node-ring').attr('stroke-opacity',0.12).attr('stroke-width',1.5);
    document.getElementById('tooltip').classList.remove('show');
}

// ═══ DETAIL PANEL ═══
function showDetailPanel(d, type){
    const panel = document.getElementById('detailPanel');

    if(type === 'node'){
        const color = typeColors[d.type];
        let attrsHtml = '';
        if(d.attrs) Object.entries(d.attrs).forEach(([k,v]) => {
            attrsHtml += `<div class="detail-row"><span class="detail-key">${k}</span><span class="detail-val">${v}</span></div>`;
        });

        const connections = [];
        graphData.edges.forEach(e => {
            const sId = typeof e.source==='object'?e.source.id:e.source;
            const tId = typeof e.target==='object'?e.target.id:e.target;
            if(sId===d.id){ const t=graphData.nodes.find(n=>n.id===tId); if(t) connections.push({dir:'→',label:e.label,node:t}); }
            if(tId===d.id){ const s=graphData.nodes.find(n=>n.id===sId); if(s) connections.push({dir:'←',label:e.label,node:s}); }
        });

        let connHtml = '';
        if(connections.length){
            connHtml = `<div style="margin-top:16px;padding-top:14px;border-top:1px solid var(--b)">
                <div class="detail-conn-title">Connections (${connections.length})</div>`;
            connections.forEach(c => {
                const cc = typeColors[c.node.type];
                connHtml += `<div class="detail-conn-item">
                    <span class="conn-arrow ${c.dir==='→'?'out':'in'}">${c.dir}</span>
                    <span class="conn-label">${c.label}</span>
                    <span class="conn-dot" style="background:${cc}"></span>
                    <span class="conn-name">${c.node.name}</span>
                </div>`;
            });
            connHtml += '</div>';
        }

        panel.innerHTML = `
            <div class="detail-header">
                <div class="detail-type"><span class="detail-type-dot" style="background:${color}"></span><span class="detail-type-label">${d.type}</span></div>
                <button class="detail-close" onclick="closeDetail()">✕</button>
            </div>
            <div class="detail-body">
                <div class="detail-name">${d.name}</div>
                ${attrsHtml}${connHtml}
            </div>`;
    } else {
        const sN = typeof d.source==='object'?d.source.name:d.source;
        const tN = typeof d.target==='object'?d.target.name:d.target;
        panel.innerHTML = `
            <div class="detail-header">
                <div class="detail-type"><span class="detail-type-dot" style="background:var(--blue)"></span><span class="detail-type-label">Relationship</span></div>
                <button class="detail-close" onclick="closeDetail()">✕</button>
            </div>
            <div class="edge-detail-header">${sN} <span class="edge-arrow">→</span> <strong>${d.label}</strong> <span class="edge-arrow">→</span> ${tN}</div>
            <div class="detail-body">
                <div class="detail-row"><span class="detail-key">Type</span><span class="detail-val">${d.label}</span></div>
                <div class="detail-row"><span class="detail-key">Weight</span><span class="detail-val">${d.weight||1}</span></div>
            </div>`;
    }
    panel.classList.add('open');
}

function closeDetail(){ document.getElementById('detailPanel').classList.remove('open'); selectedNode=null; resetStyles(); }

// ═══ LEGEND ═══
(function renderLegend(){
    const el = document.getElementById('legend');
    const counts = {};
    graphData.nodes.forEach(n => { counts[n.type]=(counts[n.type]||0)+1; });
    let h = '<div class="legend-title">Entity Types</div><div class="legend-items">';
    Object.entries(typeColors).forEach(([type,color]) => {
        if(counts[type]) h += `<div class="legend-item"><span class="legend-dot" style="background:${color}"></span><span class="legend-label">${type}</span><span class="legend-count">${counts[type]}</span></div>`;
    });
    h += '</div>';
    el.innerHTML = h;
})();

// ═══ CONTROLS ═══
function setupControls(w, h){
    document.getElementById('btnZoomIn').onclick = () => svg.transition().duration(300).call(zoom.scaleBy, 1.3);
    document.getElementById('btnZoomOut').onclick = () => svg.transition().duration(300).call(zoom.scaleBy, 0.7);
    document.getElementById('btnZoomFit').onclick = () => fitGraphToView(container.clientWidth, container.clientHeight);
    document.getElementById('btnToggleLabels').onclick = function(){
        showLabels = !showLabels;
        linkLabel.style('display', showLabels?'block':'none');
        linkLabelBg.style('display', showLabels?'block':'none');
        this.classList.toggle('active', showLabels);
    };
    document.getElementById('btnReset').onclick = () => {
        graphData.nodes.forEach(n=>{n.fx=null;n.fy=null});
        const cw = container.clientWidth, ch = container.clientHeight;
        simulation.force('center', d3.forceCenter(cw/2, ch/2).strength(1));
        simulation.force('x', d3.forceX(cw/2).strength(0.08));
        simulation.force('y', d3.forceY(cw/2).strength(0.08));
        simulation.alpha(1).restart();
        setTimeout(() => fitGraphToView(cw, ch), 2000);
    };
}

// ═══ SEARCH ═══
function setupSearch(){
    const searchBox = document.getElementById('searchBox');
    const searchInput = document.getElementById('searchInput');
    document.getElementById('btnSearch').onclick = () => { searchBox.classList.toggle('open'); if(searchBox.classList.contains('open')) searchInput.focus(); };
    document.addEventListener('keydown', e => {
        if(e.ctrlKey && e.key==='f'){ e.preventDefault(); searchBox.classList.add('open'); searchInput.focus(); }
        if(e.key==='Escape'){ searchBox.classList.remove('open'); searchInput.value=''; node.selectAll('.node-circle').attr('opacity',1); node.selectAll('text').attr('opacity',1); link.attr('opacity',1); linkLabelBg.attr('opacity',1); linkLabel.attr('opacity',1); }
    });
    searchInput.addEventListener('input', () => {
        const q = searchInput.value.toLowerCase().trim();
        if(!q){ node.selectAll('.node-circle').attr('opacity',1); node.selectAll('text').attr('opacity',1); link.attr('opacity',1); linkLabelBg.attr('opacity',1); linkLabel.attr('opacity',1); return; }
        const matchIds = new Set();
        graphData.nodes.forEach(n => { if(n.name.toLowerCase().includes(q)||n.type.toLowerCase().includes(q)) matchIds.add(n.id); });
        node.selectAll('.node-circle').attr('opacity', d => matchIds.has(d.id)?1:0.12);
        node.selectAll('text').attr('opacity', d => matchIds.has(d.id)?1:0.12);
        link.attr('opacity', d => { const s=d.source.id||d.source, t=d.target.id||d.target; return (matchIds.has(s)||matchIds.has(t))?0.8:0.05; });
        linkLabelBg.attr('opacity', d => { const s=d.source.id||d.source, t=d.target.id||d.target; return (matchIds.has(s)||matchIds.has(t))?1:0.05; });
        linkLabel.attr('opacity', d => { const s=d.source.id||d.source, t=d.target.id||d.target; return (matchIds.has(s)||matchIds.has(t))?1:0.05; });
    });
}

// ═══ RESIZE ═══
window.addEventListener('resize', () => {
    const w=container.clientWidth, h=container.clientHeight;
    svg.attr('width',w).attr('height',h);
    simulation.force('center',d3.forceCenter(w/2,h/2).strength(1));
    simulation.force('x', d3.forceX(w/2).strength(0.08));
    simulation.force('y', d3.forceY(h/2).strength(0.08));
    simulation.alpha(0.3).restart();
});

// ═══ DELAYED INIT — wait for iframe to provide correct dimensions ═══
requestAnimationFrame(() => {
    setTimeout(initGraph, 100);
});