/**
 * Defog admin dashboard.
 *
 * Builds the dashboard shell into #defog-app and renders it from the
 * authenticated REST API. The visual language mirrors the approved prototype;
 * all data here is real (rollups + live today + realtime).
 */
( function () {
	var cfg = window.DefogAdmin || {};
	var API = cfg.rest;
	var NONCE = cfg.nonce;
	var M = cfg.i18n || {};

	var state = { range: '7d', metric: 'visitors', ecomMetric: 'revenue', stats: null, journey: null, lastUpdated: 0 };

	/* ---------- meta ---------- */
	var CH = {
		organic:     { name: 'Organic search', color: '#0C7FF2' },
		direct:      { name: 'Direct',         color: '#08B4A6' },
		paid:        { name: 'Paid ads',       color: '#DB2777' },
		paid_social: { name: 'Paid social',    color: '#EC4899' },
		social:      { name: 'Social',         color: '#7C5CFC' },
		referral:    { name: 'Referral',       color: '#F2A60C' },
		ai:          { name: 'AI assistants',  color: '#E0604C' }
	};
	var DEV = {
		mobile:  { name: 'Mobile',  color: '#0C7FF2' },
		desktop: { name: 'Desktop', color: '#08B4A6' },
		tablet:  { name: 'Tablet',  color: '#C7D4E2' }
	};

	function chMeta( key ) { key = ( key || '' ).toString().trim().toLowerCase(); return CH[ key ] || { name: key, color: '#9FB0C2' }; }
	function devMeta( key ) { key = ( key || '' ).toString().trim().toLowerCase(); return DEV[ key ] || { name: key, color: '#C7D4E2' }; }

	function flag( cc ) {
		if ( ! cc || cc === 'XX' ) { return '🌍'; }
		try {
			return cc.toUpperCase().replace( /./g, function ( c ) {
				return String.fromCodePoint( 0x1F1E6 + c.charCodeAt( 0 ) - 65 );
			} );
		} catch ( e ) { return '🌍'; }
	}
	function countryName( cc ) {
		if ( ! cc || cc === 'XX' ) { return 'Unknown'; }
		try {
			var dn = new Intl.DisplayNames( [ 'en' ], { type: 'region' } );
			return dn.of( cc.toUpperCase() ) || cc;
		} catch ( e ) { return cc; }
	}

	/* ---------- helpers ---------- */
	var REDUCED = window.matchMedia && window.matchMedia( '(prefers-reduced-motion: reduce)' ).matches;

	// Animate a numeric element from → to over ~600ms (ease-out cubic).
	// Skipped entirely when the user prefers reduced motion.
	function animateVal( el, from, to, format ) {
		if ( REDUCED || from === to || ! el ) {
			if ( el ) { el.textContent = format( to ); }
			return;
		}
		var start = null;
		function step( ts ) {
			if ( ! start ) { start = ts; }
			var p = Math.min( 1, ( ts - start ) / 600 );
			p = 1 - Math.pow( 1 - p, 3 );
			el.textContent = format( from + ( to - from ) * p );
			if ( p < 1 ) { requestAnimationFrame( step ); }
		}
		requestAnimationFrame( step );
	}

	// Re-trigger the CSS transition on freshly rendered bars (width) and funnel
	// columns (height) so they grow from zero. Called on load and range change
	// only — not on silent refreshes.
	function growBars( scope ) {
		if ( REDUCED ) { return; }
		( scope || document ).querySelectorAll( '.bar > i, .minibar > i, .pbar > i, .fcol > i' ).forEach( function ( b ) {
			var w = b.style.width, h = b.style.height;
			b.style.transition = 'none';
			if ( w ) { b.style.width = '0%'; }
			if ( h ) { b.style.height = '0%'; }
			void b.offsetWidth;
			b.style.transition = '';
			if ( w ) { b.style.width = w; }
			if ( h ) { b.style.height = h; }
		} );
	}

	function fmt( n ) {
		n = Number( n ) || 0;
		return n >= 1000 ? ( n / 1000 ).toFixed( n >= 10000 ? 0 : 1 ).replace( /\.0$/, '' ) + 'k' : String( Math.round( n ) );
	}
	function fmtFull( n ) { return ( Number( n ) || 0 ).toLocaleString( 'en-US' ); }

	function api( path ) {
		return fetch( API + path, {
			headers: { 'X-WP-Nonce': NONCE },
			credentials: 'same-origin'
		} ).then( function ( r ) {
			// fetch() resolves on 404 too, so turn HTTP errors into rejections —
			// callers rely on .catch() to stop retrying a missing route.
			if ( ! r.ok ) { throw new Error( 'HTTP ' + r.status ); }
			return r.json();
		} );
	}

	/* ---------- shell ---------- */
	function shell() {
		return ''
		+ '<div class="defog-topbar">'
		+   '<div class="brand"><div class="mark">'
		+     '<svg class="fog" width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M2 14l4-5 3 3 4-6 5 7" stroke="#fff" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/></svg>'
		+     '<svg width="20" height="20" viewBox="0 0 20 20" fill="none"><path d="M2 14l4-5 3 3 4-6 5 7" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>'
		+   '</div><div class="wordmark">Fog<b>less</b></div></div>'
		+   '<nav class="dnav" id="dnav" aria-label="Dashboard sections">'
		+     '<button data-nav="overview" aria-current="true">' + esc( M.navOverview ) + '</button>'
		+     '<button data-nav="insights" data-target="defog-insights" hidden>' + esc( M.navInsights ) + '</button>'
		+     '<button data-nav="journey" data-target="defog-journey" hidden>' + esc( M.navJourney ) + '</button>'
		+     '<button data-nav="store" data-target="defog-ecom" hidden>' + esc( M.navStore ) + '</button>'
		+   '</nav>'
		+   '<span class="spacer"></span>'
		+   '<div class="seg" role="group">'
		+     '<button data-range="today" aria-pressed="false">' + esc( M.today ) + '</button>'
		+     '<button data-range="7d" aria-pressed="true">' + esc( M.d7 ) + '</button>'
		+     '<button data-range="30d" aria-pressed="false">' + esc( M.d30 ) + '</button>'
		+   '</div>'
		+   '<button class="guide-btn" id="guideToggle" type="button" aria-pressed="false"><svg viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="2"/><path d="M12 11v5M12 8h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>' + esc( M.guideShow ) + '</button>'
		+   '<button class="guide-btn" id="fbOpen" type="button" title="' + esc( M.fbTrigger ) + '"><svg viewBox="0 0 24 24" fill="none"><path d="M9.5 9a2.5 2.5 0 1 1 3.4 2.3c-.7.3-1.4.9-1.4 1.7v.5M12 17h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="2"/></svg>' + esc( M.fbShort ) + '</button>'
		+   '<span class="gdpr"><svg class="lock" viewBox="0 0 24 24" fill="none"><rect x="5" y="11" width="14" height="9" rx="2" stroke="currentColor" stroke-width="2"/><path d="M8 11V8a4 4 0 0 1 8 0v3" stroke="currentColor" stroke-width="2"/></svg> ' + esc( M.gdpr ) + '</span>'
		+ '</div>'
		+ '<div class="defog-grid">'
		+   '<section class="card live" id="liveCard">'
		+     '<div class="live-head"><span class="eyebrow"><span class="pulse"></span> ' + esc( M.realTime ) + '</span><span class="live-note">' + esc( M.liveNote ) + '</span></div>'
		+     '<div class="live-cols">'
		+       '<div class="now"><div class="big" id="liveNow">0</div><div class="sub">' + esc( M.rightNow ) + '</div>'
		+         '<div class="rt"><div class="rt-label"><span>' + esc( M.last30 ) + '</span><span id="rtPeak"></span></div>'
		+         '<svg id="rtChart" viewBox="0 0 260 46" preserveAspectRatio="none" style="width:100%;height:46px"></svg></div></div>'
		+       '<div class="live-list"><div class="sep-title">' + esc( M.activePages ) + '</div><div id="livePages"></div></div>'
		+       '<div class="live-list"><div class="sep-title">' + esc( M.liveSources ) + '</div><div id="liveSources"></div></div>'
		+     '</div>'
		+   '</section>'
		+   guideCard()
		+ '</div>'
		+ '<div class="scope-hint"><span class="arrow">↓</span> ' + esc( M.scope ) + ' <strong id="scopeLabel"></strong></div>'
		+ '<div class="defog-grid">'
		+   kpiCard( 'visitors', M.visitors ) + kpiCard( 'pageviews', M.pageviews ) + kpiCard( 'sessions', M.sessions ) + kpiCard( 'bounce', M.bounce )
		+ '</div>'
		+ '<div id="defog-insights" class="defog-insights" hidden></div>'
		+ '<div class="defog-grid mt">'
		+   '<section class="card chart-card">'
		+     '<div class="card-head"><div><div class="eyebrow">' + esc( M.traffic ) + help( 'traffic' ) + '</div><div class="card-title" id="chartSub" style="margin-top:3px;color:var(--ink-soft)">' + esc( M.visitors ) + '</div></div>'
		+       '<div class="metric-toggle" role="group">'
		+         '<button data-metric="visitors" aria-pressed="true">' + esc( M.visitors ) + '</button>'
		+         '<button data-metric="pageviews" aria-pressed="false">' + esc( M.pageviews ) + '</button>'
		+         '<button data-metric="sessions" aria-pressed="false">' + esc( M.sessions ) + '</button>'
		+       '</div></div>'
		+     '<div class="chart-host"><svg id="mainChart" viewBox="0 0 760 240" preserveAspectRatio="none"></svg><div class="tooltip" id="tip"></div></div>'
		+     '<div class="legend"><span><i style="background:var(--accent)"></i>' + esc( M.thisPeriod ) + '</span><span><i style="background:#C7D4E2"></i>' + esc( M.prevPeriod ) + '</span></div>'
		+   '</section>'
		+   '<section class="card chan-card"><div class="card-head"><div><div class="eyebrow">' + esc( M.acquisition ) + '</div><div class="card-title" style="margin-top:3px;color:var(--ink-soft)">' + esc( M.channels ) + help( 'channels' ) + '</div></div></div><div class="chan" id="channels"></div></section>'
		+   '<section class="card third"><div class="card-head"><div class="card-title">' + esc( M.topPages ) + help( 'topPages' ) + '</div></div><div class="plist" id="topPages"></div></section>'
		+   '<section class="card third"><div class="card-head"><div class="card-title">' + esc( M.topCountries ) + help( 'countries' ) + '</div></div><div class="plist" id="countries"></div></section>'
		+   '<section class="card third"><div class="card-head"><div class="card-title">' + esc( M.devices ) + help( 'devices' ) + '</div></div><div class="donut-wrap"><svg id="donut" width="118" height="118" viewBox="0 0 118 118"></svg><div class="donut-legend" id="deviceLegend"></div></div></section>'
		+ '</div>'
		+ '<div id="defog-events" class="defog-events" hidden></div>'
		+ '<div id="defog-journey" class="defog-journey" hidden></div>'
		+ '<div id="defog-ecom" class="defog-ecom" hidden></div>'
		+ '<div class="defog-foot"><span class="fresh"><span class="pulse"></span> <span id="freshTxt">' + esc( M.updated ) + '</span></span>'
		+   '<span class="defog-ver">Fogless v' + esc( cfg.version || '' ) + '</span></div>'
		+ '<div class="dg-backdrop" id="fbBackdrop"></div>'
		+ '<div class="dg-modal" id="fbModal" role="dialog" aria-modal="true" aria-hidden="true" aria-label="' + esc( M.fbTitle ) + '">'
		+   '<div class="fb-head"><div class="fb-t">' + esc( M.fbTitle ) + '</div>'
		+     '<button class="dr-close" id="fbClose" type="button" aria-label="' + esc( M.close ) + '"><svg width="14" height="14" viewBox="0 0 24 24" fill="none"><path d="M6 6l12 12M18 6L6 18" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/></svg></button></div>'
		+   '<div class="fb-body">'
		+     '<p class="fb-lead">' + esc( M.fbLead ) + '</p>'
		+     '<textarea id="fbText" class="fb-textarea" rows="5" placeholder="' + esc( M.fbPlaceholder ) + '"></textarea>'
		+     '<div class="fb-actions"><button class="fb-send" id="fbSend" type="button">' + esc( M.fbSend ) + '</button></div>'
		+     '<p class="fb-note">' + esc( M.fbNote ) + '</p>'
		+   '</div>'
		+ '</div>'
		+ '<div class="dg-backdrop" id="pdBackdrop"></div>'
		+ '<aside class="dg-drawer" id="pdDrawer" aria-hidden="true">'
		+   '<div class="dr-head"><div class="dr-eyebrow"><span>' + esc( M.pageDetail ) + '</span>'
		+     '<button class="dr-close" id="pdClose" aria-label="' + esc( M.close ) + '"><svg width="14" height="14" viewBox="0 0 24 24" fill="none"><path d="M6 6l12 12M18 6L6 18" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"/></svg></button></div>'
		+     '<div class="dr-path"><span class="p" id="pdPath"></span></div></div>'
		+   '<div class="dr-body" id="pdBody"></div>'
		+ '</aside>';
	}
	// The "How to read this" guide shown beside the live panel (collapsible).
	function guideCard() {
		var ic = {
			cal:   '<svg viewBox="0 0 24 24" fill="none"><rect x="4" y="5" width="16" height="16" rx="2" stroke="currentColor" stroke-width="2"/><path d="M4 9h16M8 3v4M16 3v4" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>',
			click: '<svg viewBox="0 0 24 24" fill="none"><path d="M5 3l6 15 2.2-6.3L19.5 10 5 3z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/></svg>',
			help:  '<svg viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="2"/><path d="M9.5 9.2a2.5 2.5 0 1 1 3.3 2.3c-.7.3-1.3.9-1.3 1.7v.4M12 16.5h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>',
			store: '<svg viewBox="0 0 24 24" fill="none"><path d="M4 7h16l-1.4 9.2a2 2 0 0 1-2 1.8H7.4a2 2 0 0 1-2-1.8L4 7zM4 7L3.2 3H1" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>',
			route: '<svg viewBox="0 0 24 24" fill="none"><circle cx="6" cy="6" r="2.4" stroke="currentColor" stroke-width="2"/><circle cx="18" cy="18" r="2.4" stroke="currentColor" stroke-width="2"/><path d="M8.4 6H14a4 4 0 0 1 0 8h-4a4 4 0 0 0 0 8" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>',
			lock:  '<svg viewBox="0 0 24 24" fill="none"><rect x="5" y="11" width="14" height="9" rx="2" stroke="currentColor" stroke-width="2"/><path d="M8 11V8a4 4 0 0 1 8 0v3" stroke="currentColor" stroke-width="2"/></svg>'
		};
		function row( i, t ) { return '<div class="g-row"><span class="g-ic">' + i + '</span><span class="g-tx">' + esc( t ) + '</span></div>'; }
		return '<section class="card guide-card" id="guideCard">'
			+ '<div class="g-head"><span class="g-title">' + esc( M.guideTitle ) + '</span>'
			+ '<button class="g-hide" id="guideHide" type="button">' + esc( M.guideHide ) + '</button></div>'
			+ '<div class="g-list">'
			+ row( ic.cal, M.gRange ) + row( ic.click, M.gClick ) + row( ic.help, M.gHelp )
			+ row( ic.store, M.gEcom ) + row( ic.lock, M.gPrivacy )
			+ '</div></section>';
	}
	function kpiCard( key, label ) {
		return '<article class="card kpi" data-kpi="' + key + '"><div class="card-title">' + esc( label ) + help( key ) + '</div><div class="val" data-val>—</div><div data-delta></div><svg class="spark" data-spark viewBox="0 0 78 34" preserveAspectRatio="none"></svg></article>';
	}
	function esc( s ) { s = s == null ? '' : String( s ); return s.replace( /[&<>"]/g, function ( c ) { return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[ c ]; } ); }

	/* ---------- contextual help (? tooltips) ---------- */
	// Maps a metric key to its plain-language explanation (from localized strings).
	var HELP = {
		visitors:  M.hVisitors,  pageviews: M.hPageviews, sessions: M.hSessions, bounce: M.hBounce,
		channels:  M.hChannels,  topPages:  M.hTopPages,  countries: M.hCountries, devices: M.hDevices,
		traffic:   M.hTraffic,    events:    M.hEvents,    journey:   M.hJourney,
		insights:  M.hInsights
	};
	// Renders a small "?" badge with a hover/focus tooltip. Returns '' when no text exists.
	function help( key ) {
		var t = HELP[ key ];
		if ( ! t ) { return ''; }
		return '<span class="dg-help" tabindex="0" role="img" aria-label="' + esc( t ) + '">'
			+ '<span class="qm">?</span><span class="dg-tip">' + esc( t ) + '</span></span>';
	}

	/* ---------- KPI ---------- */
	var upSvg = '<svg viewBox="0 0 24 24" fill="none"><path d="M7 14l5-5 5 5" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/></svg>';
	var dnSvg = '<svg viewBox="0 0 24 24" fill="none"><path d="M7 10l5 5 5-5" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/></svg>';
	function sparkPath( vals, w, h ) {
		w = w || 78; h = h || 34;
		if ( ! vals.length ) { return ''; }
		var max = Math.max.apply( null, vals ), min = Math.min.apply( null, vals ), rng = ( max - min ) || 1, pad = 3;
		return vals.map( function ( v, i ) {
			var x = pad + ( i / ( vals.length - 1 || 1 ) ) * ( w - pad * 2 );
			var y = h - pad - ( ( v - min ) / rng ) * ( h - pad * 2 );
			return ( i ? 'L' : 'M' ) + x.toFixed( 1 ) + ' ' + y.toFixed( 1 );
		} ).join( ' ' );
	}
	var kpiPrev = {};
	function renderKPIs() {
		var d = state.stats, k = d.kpis;
		// Scope to core KPI cards only — e-commerce cards share .kpi but carry data-ekpi.
		document.querySelectorAll( '[data-kpi]' ).forEach( function ( card ) {
			var key = card.dataset.kpi, m = k[ key ], isBounce = key === 'bounce';
			var fmtVal = isBounce
				? function ( v ) { return v.toFixed( 1 ) + '%'; }
				: function ( v ) { return fmtFull( Math.round( v ) ); };
			animateVal( card.querySelector( '[data-val]' ), ( key in kpiPrev ) ? kpiPrev[ key ] : 0, m.val, fmtVal );
			kpiPrev[ key ] = m.val;
			var good = isBounce ? m.d < 0 : m.d >= 0;
			var arrow = m.d >= 0 ? upSvg : dnSvg;
			card.querySelector( '[data-delta]' ).innerHTML =
				'<span class="delta ' + ( good ? 'good' : 'bad' ) + '">' + arrow + Math.abs( m.d ).toFixed( 1 ) + '%<span class="vs">' + esc( M.vs ) + ' ' + esc( k.cmp ) + '</span></span>';
			var sKey = isBounce ? 'visitors' : key;
			var vals = ( d.series[ sKey ] && d.series[ sKey ].current ) || [];
			card.querySelector( '[data-spark]' ).innerHTML =
				'<path d="' + sparkPath( vals ) + '" fill="none" stroke="' + ( good ? 'var(--up)' : 'var(--down)' ) + '" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" opacity=".85"/>';
		} );
	}

	/* ---------- chart ---------- */
	var CW = 760, CH = 240, PAD = { t: 14, r: 8, b: 24, l: 34 };
	function renderChart() {
		var s = state.stats.series[ state.metric ];
		var cur = s.current, prev = s.previous, labels = s.labels;
		var svg = document.getElementById( 'mainChart' );
		var all = cur.concat( prev ), max = ( Math.max.apply( null, all ) || 1 ) * 1.12, min = 0;
		var innerW = CW - PAD.l - PAD.r, innerH = CH - PAD.t - PAD.b;
		var X = function ( i ) { return PAD.l + ( i / ( cur.length - 1 || 1 ) ) * innerW; };
		var Y = function ( v ) { return PAD.t + innerH - ( ( v - min ) / ( max - min ) ) * innerH; };
		var line = function ( vals ) { return vals.map( function ( v, i ) { return ( i ? 'L' : 'M' ) + X( i ).toFixed( 1 ) + ' ' + Y( v ).toFixed( 1 ); } ).join( ' ' ); };
		var area = function ( vals ) { return line( vals ) + ' L ' + X( vals.length - 1 ).toFixed( 1 ) + ' ' + ( PAD.t + innerH ) + ' L ' + PAD.l + ' ' + ( PAD.t + innerH ) + ' Z'; };
		var grid = '';
		for ( var g = 0; g <= 3; g++ ) {
			var y = PAD.t + ( innerH / 3 ) * g, val = max - ( max / 3 ) * g;
			grid += '<line x1="' + PAD.l + '" y1="' + y + '" x2="' + ( CW - PAD.r ) + '" y2="' + y + '" stroke="var(--line-soft)" stroke-width="1"/>';
			grid += '<text x="' + ( PAD.l - 8 ) + '" y="' + ( y + 4 ) + '" text-anchor="end" font-size="10" fill="var(--faint)" font-family="Space Grotesk">' + fmt( val ) + '</text>';
		}
		svg.innerHTML =
			'<defs><linearGradient id="dfill" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="rgba(12,127,242,.22)"/><stop offset="100%" stop-color="rgba(12,127,242,0)"/></linearGradient></defs>'
			+ grid
			+ '<path d="' + area( cur ) + '" fill="url(#dfill)"/>'
			+ '<path d="' + line( prev ) + '" fill="none" stroke="#C7D4E2" stroke-width="2" stroke-dasharray="5 4" stroke-linecap="round"/>'
			+ '<path d="' + line( cur ) + '" fill="none" stroke="var(--accent)" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/>'
			+ '<line class="crosshair" id="cross" x1="0" y1="' + PAD.t + '" x2="0" y2="' + ( PAD.t + innerH ) + '" style="opacity:0"/>'
			+ '<circle class="cursor-dot" id="cdot" r="4.5" style="opacity:0"/>';
		var tip = document.getElementById( 'tip' ), cross = svg.querySelector( '#cross' ), cdot = svg.querySelector( '#cdot' );
		svg.onmousemove = function ( e ) {
			var rect = svg.getBoundingClientRect(), px = ( e.clientX - rect.left ) / rect.width * CW;
			var i = Math.round( ( px - PAD.l ) / innerW * ( cur.length - 1 ) ); i = Math.max( 0, Math.min( cur.length - 1, i ) );
			var x = X( i ), yy = Y( cur[ i ] );
			cross.setAttribute( 'x1', x ); cross.setAttribute( 'x2', x ); cross.style.opacity = 1;
			cdot.setAttribute( 'cx', x ); cdot.setAttribute( 'cy', yy ); cdot.style.opacity = 1;
			tip.style.opacity = 1; tip.style.left = ( x / CW * rect.width ) + 'px'; tip.style.top = ( yy / CH * rect.height ) + 'px';
			tip.innerHTML = '<b>' + fmtFull( cur[ i ] ) + '</b> ' + esc( state.metric ) + '<br><span style="color:#9fb0c2">' + esc( labels[ i ] || '' ) + '</span>';
		};
		svg.onmouseleave = function () { tip.style.opacity = 0; cross.style.opacity = 0; cdot.style.opacity = 0; };
		document.getElementById( 'chartSub' ).textContent = M[ state.metric ] || state.metric;
	}

	/* ---------- dimensions ---------- */
	function renderChannels() {
		var rows = state.stats.channels || [];
		var total = rows.reduce( function ( a, r ) { return a + r.visitors; }, 0 ) || 1;
		document.getElementById( 'channels' ).innerHTML = rows.map( function ( r ) {
			var meta = chMeta( r.label ), pct = Math.round( r.visitors / total * 100 );
			return '<div class="c-row"><div class="c-top"><span class="c-name"><span class="dot" style="background:' + meta.color + ';display:inline-block;margin-right:7px"></span>' + esc( meta.name ) + '</span>'
				+ '<span class="c-val"><b>' + fmtFull( r.visitors ) + '</b> · ' + pct + '%</span></div>'
				+ '<div class="bar"><i style="width:' + pct + '%;background:' + meta.color + '"></i></div></div>';
		} ).join( '' ) || empty();
	}
	function renderPages() {
		var rows = state.stats.pages || [];
		var max = Math.max.apply( null, rows.map( function ( r ) { return r.pageviews; } ).concat( [ 1 ] ) );
		var host = document.getElementById( 'topPages' );
		host.innerHTML = rows.map( function ( r ) {
			return '<div class="row prow" data-path="' + esc( r.label ) + '"><span class="path" title="' + esc( r.label ) + '">' + esc( r.label ) + '</span>'
				+ '<span class="minibar"><i style="width:' + ( r.pageviews / max * 100 ) + '%"></i></span>'
				+ '<span class="num">' + fmtFull( r.pageviews ) + '</span>'
				+ '<span class="chev"><svg width="15" height="15" viewBox="0 0 24 24" fill="none"><path d="M9 6l6 6-6 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg></span></div>';
		} ).join( '' ) || empty();
		host.querySelectorAll( '.prow' ).forEach( function ( row ) {
			row.onclick = function () { openPageDetail( row.dataset.path ); };
		} );
	}
	function renderCountries() {
		var rows = state.stats.countries || [];
		var max = Math.max.apply( null, rows.map( function ( r ) { return r.visitors; } ).concat( [ 1 ] ) );
		document.getElementById( 'countries' ).innerHTML = rows.map( function ( r ) {
			return '<div class="row"><span class="flag">' + flag( r.label ) + '</span><span class="path">' + esc( countryName( r.label ) ) + '</span>'
				+ '<span class="minibar"><i style="width:' + ( r.visitors / max * 100 ) + '%"></i></span>'
				+ '<span class="num">' + fmtFull( r.visitors ) + '</span></div>';
		} ).join( '' ) || empty();
	}
	function renderDevices() {
		var rows = state.stats.devices || [];
		var total = rows.reduce( function ( a, r ) { return a + r.visitors; }, 0 ) || 1;
		var svg = document.getElementById( 'donut' ), cx = 59, cy = 59, rr = 44, sw = 15, C = 2 * Math.PI * rr, a = -90, arcs = '';
		var leg = '';
		rows.forEach( function ( r ) {
			var meta = devMeta( r.label ), frac = r.visitors / total, len = C * frac;
			arcs += '<circle cx="' + cx + '" cy="' + cy + '" r="' + rr + '" fill="none" stroke="' + meta.color + '" stroke-width="' + sw + '" stroke-dasharray="' + len + ' ' + ( C - len ) + '" stroke-dashoffset="' + ( -C * ( ( a + 90 ) / 360 ) ) + '" transform="rotate(-90 ' + cx + ' ' + cy + ')"/>';
			a += frac * 360;
			leg += '<div class="dl"><span class="left"><span class="dot" style="background:' + meta.color + '"></span>' + esc( meta.name ) + '</span><b>' + Math.round( frac * 100 ) + '%</b></div>';
		} );
		var topPct = rows.length ? Math.round( rows[ 0 ].visitors / total * 100 ) : 0;
		var topName = rows.length ? devMeta( rows[ 0 ].label ).name.toLowerCase() : esc( M.mobile );
		svg.innerHTML = arcs + '<text x="' + cx + '" y="' + ( cy - 2 ) + '" text-anchor="middle" font-size="20" font-weight="700" fill="var(--ink)" font-family="Space Grotesk">' + topPct + '%</text><text x="' + cx + '" y="' + ( cy + 13 ) + '" text-anchor="middle" font-size="9.5" fill="var(--muted)">' + esc( topName ) + '</text>';
		document.getElementById( 'deviceLegend' ).innerHTML = leg || empty();
	}
	// Friendly empty state: a faint fog mark + an actionable one-liner.
	function empty( msg ) {
		return '<div class="defog-empty"><svg viewBox="0 0 20 20" fill="none" aria-hidden="true"><path d="M2 14l4-5 3 3 4-6 5 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg><span>' + esc( msg || M.noData ) + '</span></div>';
	}

	/* ---------- realtime ---------- */
	function renderRealtime( rt ) {
		var big = document.getElementById( 'liveNow' );
		if ( big ) {
			if ( big.textContent !== String( rt.now ) ) { big.classList.add( 'bump' ); setTimeout( function () { big.classList.remove( 'bump' ); }, 250 ); }
			big.textContent = rt.now;
		}
		var arr = rt.last30 || [], max = Math.max.apply( null, arr.concat( [ 1 ] ) ), w = 260, h = 46, bw = w / ( arr.length || 30 );
		var svg = document.getElementById( 'rtChart' );
		if ( svg ) {
			svg.innerHTML = arr.map( function ( v, i ) {
				var bh = Math.max( 2, ( v / max ) * ( h - 6 ) );
				return '<rect x="' + ( i * bw + 1 ).toFixed( 1 ) + '" y="' + ( h - bh ).toFixed( 1 ) + '" width="' + ( bw - 2 ).toFixed( 1 ) + '" height="' + bh.toFixed( 1 ) + '" rx="1.5" fill="' + ( i >= arr.length - 3 ? 'var(--teal)' : 'rgba(8,180,166,.32)' ) + '"/>';
			} ).join( '' );
			document.getElementById( 'rtPeak' ).textContent = M.peak + ' ' + max;
		}
		var lp = document.getElementById( 'livePages' );
		if ( lp ) {
			lp.innerHTML = ( rt.pages && rt.pages.length ) ? rt.pages.map( function ( p ) {
				return '<div class="row"><span class="lbl" title="' + esc( p.label ) + '">' + esc( p.label ) + '</span><span class="cnt">' + p.n + '</span></div>';
			} ).join( '' ) : '<div class="defog-empty">—</div>';
		}
		var ls = document.getElementById( 'liveSources' );
		if ( ls ) {
			ls.innerHTML = ( rt.sources && rt.sources.length ) ? rt.sources.map( function ( s ) {
				var meta = chMeta( s.label );
				return '<div class="row"><span class="lbl"><span class="src"><span class="dot" style="background:' + meta.color + '"></span>' + esc( meta.name ) + '</span></span><span class="cnt">' + s.n + '</span></div>';
			} ).join( '' ) : '<div class="defog-empty">—</div>';
		}
	}

	/* ---------- e-commerce (WooCommerce) ---------- */
	function money( n, cur ) {
		n = Number( n ) || 0;
		try {
			return new Intl.NumberFormat( undefined, { style: 'currency', currency: cur || 'USD', maximumFractionDigits: 0 } ).format( n );
		} catch ( e ) { return Math.round( n ).toLocaleString() + ' ' + ( cur || '' ); }
	}
	var icCart = '<svg viewBox="0 0 24 24" fill="none"><path d="M4 7h16M4 7l1.5 11a2 2 0 0 0 2 1.7h9a2 2 0 0 0 2-1.7L20 7M9 11v4M15 11v4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
	var icBag = '<svg viewBox="0 0 24 24" fill="none"><path d="M6 6h15l-1.5 9h-12L6 6zM6 6L5 3H2m4 17a1 1 0 1 0 0 .01M17 20a1 1 0 1 0 0 .01" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
	var icTrend = '<svg viewBox="0 0 24 24" fill="none"><path d="M3 17l5-5 4 4 8-8M16 4h5v5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
	var icCoin = '<svg viewBox="0 0 24 24" fill="none"><path d="M12 3v18M16 7a3 3 0 0 0-3-2h-2a2.5 2.5 0 0 0 0 5h2a2.5 2.5 0 0 1 0 5h-2a3 3 0 0 1-3-2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>';
	var icBox = '<svg viewBox="0 0 24 24" fill="none" width="15" height="15"><path d="M3 8l9-5 9 5v8l-9 5-9-5V8zM3 8l9 5 9-5M12 13v8" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg>';

	function ecomKpiCard( key, label, tone, icon ) {
		var bg = { teal: 'var(--teal-wash)', accent: 'var(--accent-wash)', up: 'rgba(19,160,107,.10)', paid: 'var(--paid-wash)' }[ tone ];
		var fg = { teal: 'var(--teal)', accent: 'var(--accent)', up: 'var(--up)', paid: 'var(--paid)' }[ tone ];
		return '<article class="card kpi" data-ekpi="' + key + '"><div class="ic" style="background:' + bg + ';color:' + fg + '">' + icon + '</div><div class="card-title">' + esc( label ) + '</div><div class="val" data-eval>—</div><div data-edelta></div><svg class="spark" data-espark viewBox="0 0 78 34" preserveAspectRatio="none"></svg></article>';
	}

	function renderEcom( ec ) {
		var host = document.getElementById( 'defog-ecom' );
		if ( ! host ) { return; }
		if ( ! ec ) { host.hidden = true; host.innerHTML = ''; return; }
		host.hidden = false;
		host.innerHTML =
			'<div class="ecom-eyebrow">' + esc( M.ecommerce ) + '<span class="ln"></span></div>'
			+ '<div class="defog-grid">'
			+   ecomKpiCard( 'revenue', M.revenue, 'teal', icCart )
			+   ecomKpiCard( 'orders', M.orders, 'accent', icBag )
			+   ecomKpiCard( 'cr', M.convRate, 'up', icTrend )
			+   ecomKpiCard( 'aov', M.aov, 'paid', icCoin )
			+   '<section class="card chart-card"><div class="card-head"><div><div class="eyebrow">' + esc( M.sales ) + '</div><div class="card-title" id="ecomChartSub" style="margin-top:3px;color:var(--ink-soft)">' + esc( M.revenue ) + '</div></div>'
			+     '<div class="metric-toggle" id="ecomMetricToggle" role="group">'
			+       '<button data-emetric="revenue" aria-pressed="' + ( state.ecomMetric === 'revenue' ) + '">' + esc( M.revenue ) + '</button>'
			+       '<button data-emetric="orders" aria-pressed="' + ( state.ecomMetric === 'orders' ) + '">' + esc( M.orders ) + '</button>'
			+     '</div></div>'
			+     '<div class="chart-host"><svg id="ecomChart" viewBox="0 0 760 230" preserveAspectRatio="none"></svg><div class="tooltip" id="ecomTip"></div></div>'
			+     '<div class="legend"><span><i style="background:var(--teal)"></i>' + esc( M.thisPeriod ) + '</span><span><i style="background:#C7D4E2"></i>' + esc( M.prevPeriod ) + '</span></div>'
			+   '</section>'
			+   '<section class="card chan-card"><div class="card-head"><div><div class="eyebrow">' + esc( M.attribution ) + '</div><div class="card-title" style="margin-top:3px;color:var(--ink-soft)">' + esc( M.revByChannel ) + '</div></div></div><div class="chan" id="ecomChannels"></div><div class="attrib-note">' + esc( M.attribNote ) + '</div></section>'
			+   '<section class="card funnel-card"><div class="card-head"><div class="card-title-wrap"><div class="ct">' + esc( M.funnelTitle ) + '</div><div class="cs">' + esc( M.funnelSub ) + '</div></div><div class="overall"><div class="big" id="funnelOverall">—</div><div class="lbl">' + esc( M.overallConv ) + '</div></div></div><div class="funnel" id="ecomFunnel"></div></section>'
			+   '<section class="card prod-card"><div class="card-head"><div class="card-title">' + esc( M.topProducts ) + '</div></div><div class="plist eprod" id="ecomProducts"></div></section>'
			+ '</div>';
		renderEcomKPIs( ec );
		renderEcomChart( ec );
		renderEcomChannels( ec );
		renderEcomProducts( ec );
		renderEcomFunnel( ec.funnel );
		wireEcom( ec );
	}

	var ekpiPrev = {};
	function renderEcomKPIs( ec ) {
		var k = ec.kpis, cur = ec.currency, cmp = ( state.stats && state.stats.kpis ) ? state.stats.kpis.cmp : '';
		document.querySelectorAll( '[data-ekpi]' ).forEach( function ( card ) {
			var key = card.dataset.ekpi, m = k[ key ];
			var fmtV = ( key === 'revenue' || key === 'aov' )
				? function ( v ) { return money( v, cur ); }
				: ( key === 'cr'
					? function ( v ) { return v.toFixed( 2 ) + ' %'; }
					: function ( v ) { return fmtFull( Math.round( v ) ); } );
			animateVal( card.querySelector( '[data-eval]' ), ( key in ekpiPrev ) ? ekpiPrev[ key ] : 0, m.val, fmtV );
			ekpiPrev[ key ] = m.val;
			var good = m.d >= 0, arrow = m.d >= 0 ? upSvg : dnSvg;
			var deltaTxt = ( key === 'cr' ) ? ( ( m.d >= 0 ? '+' : '' ) + m.d.toFixed( 2 ) + ' ' + M.points ) : ( Math.abs( m.d ).toFixed( 1 ) + ' %' );
			card.querySelector( '[data-edelta]' ).innerHTML = '<span class="delta ' + ( good ? 'good' : 'bad' ) + '">' + arrow + deltaTxt + '<span class="vs">' + esc( M.vs ) + ' ' + esc( cmp ) + '</span></span>';
			var sv = ( key === 'orders' || key === 'cr' ) ? ec.series.orders : ec.series.revenue;
			card.querySelector( '[data-espark]' ).innerHTML = '<path d="' + sparkPath( sv ) + '" fill="none" stroke="' + ( good ? 'var(--up)' : 'var(--down)' ) + '" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" opacity=".85"/>';
		} );
	}

	function renderEcomChart( ec ) {
		var s = ec.series;
		var cur = state.ecomMetric === 'orders' ? s.orders : s.revenue;
		var prev = state.ecomMetric === 'orders' ? s.prevOrders : s.prevRevenue;
		var labels = s.labels;
		var svg = document.getElementById( 'ecomChart' );
		if ( ! svg ) { return; }
		var P = { t: 14, r: 8, b: 22, l: 48 }, W = 760, H = 230;
		var all = cur.concat( prev ), max = ( Math.max.apply( null, all ) || 1 ) * 1.14, min = 0;
		var iW = W - P.l - P.r, iH = H - P.t - P.b;
		var X = function ( i ) { return P.l + ( i / ( cur.length - 1 || 1 ) ) * iW; };
		var Y = function ( v ) { return P.t + iH - ( ( v - min ) / ( max - min ) ) * iH; };
		var line = function ( vv ) { return vv.map( function ( v, i ) { return ( i ? 'L' : 'M' ) + X( i ).toFixed( 1 ) + ' ' + Y( v ).toFixed( 1 ); } ).join( ' ' ); };
		var area = function ( vv ) { return line( vv ) + ' L ' + X( vv.length - 1 ).toFixed( 1 ) + ' ' + ( P.t + iH ) + ' L ' + P.l + ' ' + ( P.t + iH ) + ' Z'; };
		var grid = '';
		for ( var g = 0; g <= 3; g++ ) {
			var y = P.t + ( iH / 3 ) * g, val = max - ( max / 3 ) * g;
			grid += '<line x1="' + P.l + '" y1="' + y + '" x2="' + ( W - P.r ) + '" y2="' + y + '" stroke="var(--line-soft)" stroke-width="1"/>';
			var lbl = state.ecomMetric === 'orders' ? Math.round( val ) : fmt( val );
			grid += '<text x="' + ( P.l - 8 ) + '" y="' + ( y + 4 ) + '" text-anchor="end" font-size="10" fill="var(--faint)" font-family="Space Grotesk">' + lbl + '</text>';
		}
		svg.innerHTML = '<defs><linearGradient id="egf" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="rgba(8,180,166,.22)"/><stop offset="100%" stop-color="rgba(8,180,166,0)"/></linearGradient></defs>'
			+ grid + '<path d="' + area( cur ) + '" fill="url(#egf)"/>'
			+ '<path d="' + line( prev ) + '" fill="none" stroke="#C7D4E2" stroke-width="2" stroke-dasharray="5 4" stroke-linecap="round"/>'
			+ '<path d="' + line( cur ) + '" fill="none" stroke="var(--teal)" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/>'
			+ '<line class="crosshair" id="ecross" x1="0" y1="' + P.t + '" x2="0" y2="' + ( P.t + iH ) + '" style="opacity:0"/>'
			+ '<circle id="ecdot" r="4.5" fill="var(--teal)" stroke="#fff" stroke-width="2" style="opacity:0"/>';
		document.getElementById( 'ecomChartSub' ).textContent = state.ecomMetric === 'orders' ? M.orders : M.revenue;

		var tip = document.getElementById( 'ecomTip' ), cross = svg.querySelector( '#ecross' ), dot = svg.querySelector( '#ecdot' );
		svg.onmousemove = function ( e ) {
			var rect = svg.getBoundingClientRect(), px = ( e.clientX - rect.left ) / rect.width * W;
			var i = Math.round( ( px - P.l ) / iW * ( cur.length - 1 ) ); i = Math.max( 0, Math.min( cur.length - 1, i ) );
			var x = X( i ), yy = Y( cur[ i ] );
			cross.setAttribute( 'x1', x ); cross.setAttribute( 'x2', x ); cross.style.opacity = 1;
			dot.setAttribute( 'cx', x ); dot.setAttribute( 'cy', yy ); dot.style.opacity = 1;
			var val = state.ecomMetric === 'orders' ? fmtFull( cur[ i ] ) + ' ' + esc( M.ordersShort ) : money( cur[ i ], ec.currency );
			tip.style.opacity = 1; tip.style.left = ( x / W * rect.width ) + 'px'; tip.style.top = ( yy / H * rect.height ) + 'px';
			tip.innerHTML = '<b>' + val + '</b><br><span style="color:#9fb0c2">' + esc( labels[ i ] || '' ) + '</span>';
		};
		svg.onmouseleave = function () { tip.style.opacity = 0; cross.style.opacity = 0; dot.style.opacity = 0; };
	}

	function renderEcomChannels( ec ) {
		var rows = ( ec.channels || [] ).filter( function ( c ) { return c.revenue > 0; } );
		var total = rows.reduce( function ( a, c ) { return a + c.revenue; }, 0 ) || 1;
		var max = Math.max.apply( null, rows.map( function ( c ) { return c.revenue; } ).concat( [ 1 ] ) );
		document.getElementById( 'ecomChannels' ).innerHTML = rows.map( function ( c ) {
			var meta = chMeta( c.label ), isPaid = c.label === 'paid' || c.label === 'paid_social';
			return '<div class="c-row"><div class="c-top"><span class="c-name"><span class="dot" style="background:' + meta.color + '"></span>' + esc( meta.name ) + ( isPaid ? '<span class="ads-tag">ADS</span>' : '' ) + '</span>'
				+ '<span class="c-val"><b>' + money( c.revenue, ec.currency ) + '</b> · ' + c.orders + ' ' + esc( M.ordersShort ) + '</span></div>'
				+ '<div class="bar"><i style="width:' + ( c.revenue / max * 100 ) + '%;background:' + meta.color + '"></i></div></div>';
		} ).join( '' ) || empty( M.emptyOrders );
	}

	function renderEcomProducts( ec ) {
		var rows = ec.products || [];
		var max = Math.max.apply( null, rows.map( function ( p ) { return p.revenue; } ).concat( [ 1 ] ) );
		document.getElementById( 'ecomProducts' ).innerHTML = rows.map( function ( p, i ) {
			return '<div class="row"><span class="rank">' + ( i + 1 ) + '</span><span class="thumb">' + icBox + '</span>'
				+ '<span class="pname"><div class="t" title="' + esc( p.name ) + '">' + esc( p.name ) + '</div><div class="u">' + fmtFull( p.units ) + ' ' + esc( M.unitsSold ) + '</div></span>'
				+ '<span class="pbar"><i style="width:' + ( p.revenue / max * 100 ) + '%"></i></span>'
				+ '<span class="prev">' + money( p.revenue, ec.currency ) + '</span></div>';
		} ).join( '' ) || empty( M.emptyOrders );
	}

	var FN_NAMES = { visitors: 'fnVisitors', product: 'fnProduct', cart: 'fnCart', checkout: 'fnCheckout', purchase: 'fnPurchase' };
	var FN_IC = {
		visitors: '<svg viewBox="0 0 24 24" fill="none"><path d="M16 19a4 4 0 0 0-8 0M12 11a3 3 0 1 0 0-6 3 3 0 0 0 0 6z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>',
		product: '<svg viewBox="0 0 24 24" fill="none"><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7z" stroke="currentColor" stroke-width="2"/><circle cx="12" cy="12" r="2.5" stroke="currentColor" stroke-width="2"/></svg>',
		cart: icBag,
		checkout: '<svg viewBox="0 0 24 24" fill="none"><path d="M4 7h16v12H4zM4 11h16M8 15h3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>',
		purchase: '<svg viewBox="0 0 24 24" fill="none"><path d="M5 12l4 4 10-10" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/></svg>'
	};
	function fnNum( n ) { return ( Number( n ) || 0 ).toLocaleString( undefined ).replace( /,/g, '\u00A0' ); }
	// Horizontal "path to purchase": five columns side by side, bar height = share
	// of visitors reaching the stage, with the kept-% from the previous stage below.
	function renderEcomFunnel( fn ) {
		var host = document.getElementById( 'ecomFunnel' );
		if ( ! host ) { return; }
		fn = fn || [];
		var top = ( fn[ 0 ] && fn[ 0 ].count ) || 0;
		var last = ( fn[ fn.length - 1 ] && fn[ fn.length - 1 ].count ) || 0;
		var ov = document.getElementById( 'funnelOverall' );
		if ( ov ) { ov.textContent = ( top ? ( last / top * 100 ).toFixed( 2 ) : '0.00' ) + ' %'; }
		host.innerHTML = fn.map( function ( s, i ) {
			var name = M[ FN_NAMES[ s.key ] ] || s.key;
			var pctTop = top ? ( s.count / top * 100 ) : 0;
			var keep = i > 0 && fn[ i - 1 ].count ? ( s.count / fn[ i - 1 ].count * 100 ) : null;
			return '<div class="fstage s' + ( i + 1 ) + '">'
				+ '<div class="fcol"><i style="height:' + Math.max( 3, pctTop ) + '%" title="' + esc( name ) + ' · ' + fnNum( s.count ) + '"></i></div>'
				+ '<div class="fmeta">'
				+   '<span class="st-ic">' + ( FN_IC[ s.key ] || '' ) + '</span>'
				+   '<div class="fname">' + esc( name ) + '</div>'
				+   '<div class="fnum">' + fnNum( s.count ) + '</div>'
				+   '<div class="fpct">' + pctTop.toFixed( 1 ) + ' % ' + esc( M.ofVisits ) + '</div>'
				+   ( null !== keep
					? '<div class="fkeep">↓ ' + keep.toFixed( 1 ) + ' % ' + esc( M.continued ) + '</div>'
					: '<div class="fkeep fkeep-blank">&nbsp;</div>' )
				+ '</div></div>';
		} ).join( '' );
	}

	function wireEcom( ec ) {
		var t = document.getElementById( 'ecomMetricToggle' );
		if ( ! t ) { return; }
		t.querySelectorAll( 'button' ).forEach( function ( b ) {
			b.onclick = function () {
				t.querySelectorAll( 'button' ).forEach( function ( x ) { x.setAttribute( 'aria-pressed', 'false' ); } );
				b.setAttribute( 'aria-pressed', 'true' );
				state.ecomMetric = b.dataset.emetric;
				renderEcomChart( ec );
			};
		} );
	}

	/* ---------- events / actions ---------- */
	var EVCAT = { outbound: 'evOutbound', download: 'evDownloads', form: 'evForms' };
	var EVUNIT = { form: 'evSubmits' }; // Per-category unit word; default is "clicks".
	function evCatName( key ) { return M[ EVCAT[ key ] ] || key; }
	function renderEvents( ac ) {
		var host = document.getElementById( 'defog-events' );
		if ( ! host ) { return; }
		if ( ! ac || ! ac.categories || ! ac.categories.length ) { host.hidden = true; host.innerHTML = ''; return; }
		host.hidden = false;
		// Category cards share the row: 1 → full width, 2 → halves, 3 → thirds.
		var n = ac.categories.length;
		var wcls = ( 1 === n ) ? 'w12' : ( ( 2 === n ) ? 'w6' : 'w4' );
		var cards = ac.categories.map( function ( cat ) {
			var maxc = Math.max.apply( null, cat.items.map( function ( it ) { return it.count; } ).concat( [ 1 ] ) );
			var rows = cat.items.map( function ( it ) {
				return '<div class="row"><span class="path" title="' + esc( it.label ) + '">' + esc( it.label ) + '</span>'
					+ '<span class="minibar"><i style="width:' + ( it.count / maxc * 100 ) + '%"></i></span>'
					+ '<span class="num">' + fmtFull( it.count ) + '</span></div>';
			} ).join( '' ) || empty();
			var unit = M[ EVUNIT[ cat.key ] ] || M.evClicks;
			return '<section class="card ev-card ' + wcls + '">'
				+ '<div class="card-head"><div><div class="eyebrow">' + esc( M.evEyebrow ) + '</div>'
				+ '<div class="card-title" style="margin-top:3px;color:var(--ink-soft)">' + esc( evCatName( cat.key ) ) + help( 'events' ) + '</div></div>'
				+ '<div class="ev-sum"><b>' + fmtFull( cat.count ) + '</b> ' + esc( unit ) + ' · ' + fmtFull( cat.visitors ) + ' ' + esc( M.evPeople ) + '</div></div>'
				+ '<div class="plist ev-list">' + rows + '</div></section>';
		} ).join( '' );
		host.innerHTML = '<div class="ecom-eyebrow">' + esc( M.evTitle ) + '<span class="ln"></span></div><div class="defog-grid">' + cards + '</div>';
	}

	/* ---------- customer journey (Pro) ---------- */
	var JC = [ '#0C7FF2', '#22A6C3', '#08B4A6', '#13A06B' ];
	function jcolor( i ) { return JC[ i % JC.length ]; }
	function trunc( s, n ) { s = String( s ); return s.length > n ? s.slice( 0, n - 1 ) + '…' : s; }

	// Build the Sankey-style flow SVG from the journey payload.
	function journeySVG( j ) {
		var W = 760, H = 340, pad = { t: 24, b: 16, l: 8, r: 8 };
		var steps = j.steps, nSteps = steps.length, nodeW = 13, gap = 6;
		var colGap = ( W - pad.l - pad.r - nodeW ) / ( nSteps - 1 || 1 );
		var colX = function ( i ) { return pad.l + i * colGap; };
		var colTotals = steps.map( function ( col ) { return col.reduce( function ( a, n ) { return a + n.value; }, 0 ); } );
		var maxTotal = Math.max.apply( null, colTotals.concat( [ 1 ] ) );
		var maxNodes = Math.max.apply( null, steps.map( function ( c ) { return c.length; } ).concat( [ 1 ] ) );
		var innerH = H - pad.t - pad.b;
		var scale = ( innerH - ( maxNodes - 1 ) * gap ) / maxTotal;

		// Lay out nodes, vertically centring each column.
		var nodes = {};
		steps.forEach( function ( col, i ) {
			var colH = colTotals[ i ] * scale + Math.max( 0, col.length - 1 ) * gap;
			var y = pad.t + ( innerH - colH ) / 2;
			col.forEach( function ( n ) {
				var nh = Math.max( 3, n.value * scale );
				nodes[ i + ':' + n.label ] = { x: colX( i ), y: y, h: nh, value: n.value, i: i, label: n.label, outCur: 0, inCur: 0 };
				y += nh + gap;
			} );
		} );

		// Resolve + order flows so stacked ribbons line up neatly.
		var flows = ( j.flows || [] ).map( function ( f ) {
			return { f: f, s: nodes[ f.from + ':' + f.fromLabel ], t: nodes[ f.to + ':' + f.toLabel ] };
		} ).filter( function ( x ) { return x.s && x.t; } );
		flows.sort( function ( a, b ) { return ( a.f.from - b.f.from ) || ( a.s.y - b.s.y ) || ( a.t.y - b.t.y ); } );

		var bands = '';
		flows.forEach( function ( x ) {
			var s = x.s, t = x.t, th = Math.max( 1, x.f.value * scale );
			var y0 = s.y + s.outCur; s.outCur += th;
			var y1 = t.y + t.inCur; t.inCur += th;
			var x0 = s.x + nodeW, x1 = t.x, cx = ( x0 + x1 ) / 2;
			bands += '<path d="M' + x0.toFixed( 1 ) + ',' + y0.toFixed( 1 )
				+ 'C' + cx.toFixed( 1 ) + ',' + y0.toFixed( 1 ) + ' ' + cx.toFixed( 1 ) + ',' + y1.toFixed( 1 ) + ' ' + x1.toFixed( 1 ) + ',' + y1.toFixed( 1 )
				+ 'L' + x1.toFixed( 1 ) + ',' + ( y1 + th ).toFixed( 1 )
				+ 'C' + cx.toFixed( 1 ) + ',' + ( y1 + th ).toFixed( 1 ) + ' ' + cx.toFixed( 1 ) + ',' + ( y0 + th ).toFixed( 1 ) + ' ' + x0.toFixed( 1 ) + ',' + ( y0 + th ).toFixed( 1 )
				+ 'Z" fill="' + jcolor( s.i ) + '" opacity="0.26"/>';
		} );

		var rects = '', labels = '';
		Object.keys( nodes ).forEach( function ( k ) {
			var n = nodes[ k ];
			rects += '<rect x="' + n.x + '" y="' + n.y.toFixed( 1 ) + '" width="' + nodeW + '" height="' + n.h.toFixed( 1 ) + '" rx="3" fill="' + jcolor( n.i ) + '"><title>' + esc( n.label ) + ' · ' + fmtFull( n.value ) + '</title></rect>';
			var last = n.i === nSteps - 1, lx = last ? n.x - 6 : n.x + nodeW + 6;
			labels += '<text x="' + lx.toFixed( 1 ) + '" y="' + ( n.y + n.h / 2 + 3 ).toFixed( 1 ) + '" text-anchor="' + ( last ? 'end' : 'start' ) + '" class="jlabel">' + esc( trunc( n.label, 18 ) ) + '</text>';
		} );

		var headers = '';
		for ( var i = 0; i < nSteps; i++ ) {
			var name = i === 0 ? M.jEntry : ( M.jStep || 'Step %d' ).replace( '%d', i + 1 );
			headers += '<text x="' + ( colX( i ) + nodeW / 2 ).toFixed( 1 ) + '" y="14" text-anchor="middle" class="jhead">' + esc( name ) + '</text>';
		}

		return '<svg viewBox="0 0 ' + W + ' ' + H + '" preserveAspectRatio="xMidYMid meet" class="journey-svg">' + bands + rects + labels + headers + '</svg>';
	}

	function renderJourney( j ) {
		var host = document.getElementById( 'defog-journey' );
		if ( ! host ) { return; }
		if ( ! j || ! j.steps || ! j.steps.length || ! j.sessions ) { host.hidden = true; host.innerHTML = ''; updateNav(); return; }
		var card = '<section class="card journey-card">'
			+ '<div class="card-head"><div class="card-title-wrap"><div class="ct">' + esc( M.jTitle ) + help( 'journey' ) + '</div><div class="cs">' + esc( M.jSub ) + '</div></div>'
			+ '<div class="ev-sum"><b>' + fmtFull( j.sessions ) + '</b> ' + esc( M.jSessions ) + '</div></div>'
			+ '<div class="journey-host">' + journeySVG( j ) + '</div></section>';
		host.hidden = false;
		host.innerHTML = '<div class="ecom-eyebrow">' + esc( M.jEyebrow ) + '<span class="ln"></span></div>'
			+ '<div class="defog-grid">' + card + '</div>';
		updateNav();
	}

	/* ---------- insights / recommendations (Pro) ---------- */
	var INS_IC = {
		'trend-up':   icTrend,
		'trend-down': '<svg viewBox="0 0 24 24" fill="none"><path d="M3 7l5 5 4-4 8 8M16 20h5v-5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>',
		'alert':      '<svg viewBox="0 0 24 24" fill="none"><path d="M12 3l9 16H3L12 3zM12 10v4M12 17h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>',
		'bulb':       '<svg viewBox="0 0 24 24" fill="none"><path d="M9 18h6M10 21h4M12 3a6 6 0 0 0-4 10.5c.6.6 1 1.3 1 2.1V16h6v-.4c0-.8.4-1.5 1-2.1A6 6 0 0 0 12 3z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>',
		'target':     '<svg viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="2"/><circle cx="12" cy="12" r="5" stroke="currentColor" stroke-width="2"/><circle cx="12" cy="12" r="1.6" fill="currentColor"/></svg>',
		'mobile':     '<svg viewBox="0 0 24 24" fill="none"><rect x="7" y="3" width="10" height="18" rx="2" stroke="currentColor" stroke-width="2"/><path d="M11 18h2" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>'
	};
	function renderInsights( list ) {
		var host = document.getElementById( 'defog-insights' );
		if ( ! host ) { return; }
		if ( ! list || ! list.length ) { host.hidden = true; host.innerHTML = ''; return; }
		var rows = list.map( function ( it ) {
			var icon = INS_IC[ it.icon ] || INS_IC.bulb;
			return '<div class="ins-row ins-' + esc( it.tone ) + '"><span class="ins-ic">' + icon + '</span>'
				+ '<div class="ins-tx"><div class="ins-t">' + esc( it.title ) + '</div><div class="ins-b">' + esc( it.body ) + '</div></div></div>';
		} ).join( '' );
		var card = '<section class="card ins-card">'
			+ '<div class="card-head"><div class="card-title-wrap"><div class="ct">' + esc( M.insTitle ) + help( 'insights' ) + '</div><div class="cs">' + esc( M.insSub ) + '</div></div></div>'
			+ '<div class="ins-list">' + rows + '</div></section>';
		host.hidden = false;
		host.innerHTML = '<div class="ecom-eyebrow">' + esc( M.insEyebrow ) + '<span class="ln"></span></div>'
			+ '<div class="defog-grid">' + card + '</div>';
	}

	/* ---------- page detail (drill-down) ---------- */
	var pdIcons = {
		views: '<svg viewBox="0 0 24 24" fill="none"><path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7-10-7-10-7z" stroke="currentColor" stroke-width="2"/><circle cx="12" cy="12" r="2.5" stroke="currentColor" stroke-width="2"/></svg>',
		visitors: '<svg viewBox="0 0 24 24" fill="none"><path d="M16 19a4 4 0 0 0-8 0M12 11a3 3 0 1 0 0-6 3 3 0 0 0 0 6z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>',
		time: '<svg viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="9" stroke="currentColor" stroke-width="2"/><path d="M12 7v5l3 2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>'
	};
	function timeFmt( s ) { s = Math.round( Number( s ) || 0 ); var m = Math.floor( s / 60 ), r = s % 60; return m ? ( m + 'm ' + r + 's' ) : ( r + 's' ); }
	function pdTile( icon, label, val, cls ) {
		return '<div class="tile ' + ( cls || '' ) + '"><div class="l">' + ( icon && pdIcons[ icon ] ? '<span class="tic">' + pdIcons[ icon ] + '</span>' : '' ) + esc( label ) + '</div><div class="v">' + val + '</div></div>';
	}
	function openPageDetail( path ) {
		document.getElementById( 'pdPath' ).textContent = path;
		document.getElementById( 'pdBody' ).innerHTML = '<div class="defog-empty">…</div>';
		document.getElementById( 'pdDrawer' ).classList.add( 'open' );
		document.getElementById( 'pdBackdrop' ).classList.add( 'open' );
		document.getElementById( 'pdDrawer' ).setAttribute( 'aria-hidden', 'false' );
		api( '/page?range=' + encodeURIComponent( state.range ) + '&path=' + encodeURIComponent( path ) )
			.then( renderPageDetail ).catch( function () {} );
	}
	function renderPageDetail( d ) {
		var bounceClass = d.bounce >= 70 ? 'warn' : ( d.bounce <= 45 ? 'good' : '' );
		var tiles = '<div class="tiles">'
			+ pdTile( 'views', M.dViews, fmtFull( d.pageviews ), '' )
			+ pdTile( 'visitors', M.dVisitors, fmtFull( d.visitors ), '' )
			+ pdTile( 'time', M.dTime, timeFmt( d.avgTime ), 'good' )
			+ pdTile( '', M.dBounce, d.bounce + ' %', bounceClass )
			+ pdTile( '', M.dEntry, d.entry + ' %', '' )
			+ pdTile( '', M.dExit, d.exit + ' %', '' )
			+ '</div>';
		var vals = ( d.series && d.series.values ) || [];
		var spark = '<div class="sec-t">' + esc( M.dViewsTime ) + '</div><svg class="pd-spark" viewBox="0 0 400 52" preserveAspectRatio="none"><path d="' + sparkPath( vals.length ? vals : [ 0, 0 ], 400, 52 ) + '" fill="none" stroke="var(--accent)" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/></svg>';
		var st = ( d.sources || [] ).reduce( function ( a, c ) { return a + c.visitors; }, 0 ) || 1;
		var smax = Math.max.apply( null, ( d.sources || [] ).map( function ( c ) { return c.visitors; } ).concat( [ 1 ] ) );
		var chan = '<div class="sec-t">' + esc( M.dSources ) + '</div><div class="chan">' + ( d.sources || [] ).map( function ( c ) {
			var m = chMeta( c.label );
			return '<div class="c-row"><div class="c-top"><span class="c-name"><span class="dot" style="background:' + m.color + '"></span>' + esc( m.name ) + '</span><span class="c-val">' + Math.round( c.visitors / st * 100 ) + ' %</span></div><div class="bar"><i style="width:' + ( c.visitors / smax * 100 ) + '%;background:' + m.color + '"></i></div></div>';
		} ).join( '' ) + '</div>';
		var dt = ( d.devices || [] ).reduce( function ( a, c ) { return a + c.count; }, 0 ) || 1;
		var dev = '<div class="sec-t">' + esc( M.dDevices ) + '</div><div class="dev">' + ( d.devices || [] ).map( function ( c ) {
			return '<div class="d"><div class="dv">' + Math.round( c.count / dt * 100 ) + '%</div><div class="dl">' + esc( devMeta( c.label ).name ) + '</div></div>';
		} ).join( '' ) + '</div>';
		document.getElementById( 'pdBody' ).innerHTML = tiles + spark + chan + dev;
		growBars( document.getElementById( 'pdBody' ) );
	}
	function closePageDetail() {
		document.getElementById( 'pdDrawer' ).classList.remove( 'open' );
		document.getElementById( 'pdBackdrop' ).classList.remove( 'open' );
		document.getElementById( 'pdDrawer' ).setAttribute( 'aria-hidden', 'true' );
	}

	/* ---------- orchestration ---------- */
	var firstScoped = true;
	function renderScoped( animate ) {
		renderKPIs(); renderChart(); renderChannels(); renderPages(); renderCountries(); renderDevices();
		renderEvents( state.stats.actions );
		renderInsights( state.stats.insights );
		renderEcom( state.stats.ecommerce );
		updateNav();
		if ( animate || firstScoped ) { growBars(); }
		firstScoped = false;
		// Extension point for add-ons (e.g. Pro's CSV export buttons).
		try {
			document.dispatchEvent( new CustomEvent( 'defog:render', { detail: { stats: state.stats, range: state.range } } ) );
		} catch ( e ) { /* older browsers: no add-on hooks */ }
		var sl = document.getElementById( 'scopeLabel' );
		if ( sl ) { sl.textContent = state.stats.kpis.label; }
		if ( animate ) {
			document.querySelectorAll( '.defog-grid .card:not(.live):not(.guide-card)' ).forEach( function ( c ) {
				c.classList.remove( 'refresh' ); void c.offsetWidth; c.classList.add( 'refresh' );
			} );
		}
	}
	function loadStats( animate ) {
		api( '/stats?range=' + encodeURIComponent( state.range ) ).then( function ( data ) {
			state.stats = data; state.lastUpdated = Date.now(); renderScoped( animate ); refreshFoot();
		} ).catch( function () {} );
	}
	function loadRealtime( done ) {
		api( '/realtime' )
			.then( renderRealtime )
			.catch( function () {} )
			.then( function () { if ( done ) { done(); } } );
	}

	/* ---------- realtime polling ----------
	 * Deliberately conservative: an admin who leaves this tab open should not
	 * keep hitting the server. Polling pauses while the tab is hidden, stops
	 * after a period of inactivity, and backs off automatically on slow hosts.
	 */
	var RT_BASE = 15000;              // Normal interval.
	var RT_MAX  = 120000;             // Ceiling when the server is slow.
	var RT_IDLE = 5 * 60 * 1000;      // Stop polling after this long without input.
	var rtDelay = RT_BASE;
	var rtTimer = null;
	var lastActivity = Date.now();

	function scheduleRealtime() {
		clearTimeout( rtTimer );
		rtTimer = setTimeout( tickRealtime, rtDelay );
	}
	function tickRealtime() {
		// Skip the request entirely when nobody is looking.
		if ( document.hidden || Date.now() - lastActivity > RT_IDLE ) {
			scheduleRealtime();
			return;
		}
		var started = Date.now();
		loadRealtime( function () {
			// Back off proportionally when responses are slow, so we never queue
			// requests faster than the server can answer them.
			var took = Date.now() - started;
			rtDelay = took > 1500 ? Math.min( RT_MAX, Math.round( took * 8 ) ) : RT_BASE;
			scheduleRealtime();
		} );
	}
	function wakeRealtime() {
		lastActivity = Date.now();
		rtDelay = RT_BASE;
		if ( ! document.hidden ) { tickRealtime(); }
	}
	// Journey is a heavier query, so it loads on its own — on open and range change,
	// never on the lightweight 60s auto-refresh. The /journey route is provided by
	// the separate add-on; without it the request simply fails and the section stays hidden.
	// The /journey route is provided by the separate add-on. If it is not
	// registered we get a 404 once and then stop asking — no repeated misses.
	var journeyUnavailable = false;
	function loadJourney() {
		if ( journeyUnavailable ) { return; }
		api( '/journey?range=' + encodeURIComponent( state.range ) ).then( function ( d ) {
			state.journey = d; renderJourney( d );
		} ).catch( function () {
			journeyUnavailable = true;
			renderJourney( null );
		} );
	}

	// Show nav chips only for sections that actually rendered (store needs Woo,
	// journey/insights need data), and highlight the section currently in view.
	function updateNav() {
		document.querySelectorAll( '#dnav [data-target]' ).forEach( function ( b ) {
			var el = document.getElementById( b.dataset.target );
			b.hidden = ! el || el.hidden;
		} );
		navActive();
	}
	function navActive() {
		var cur = 'overview', best = -Infinity;
		document.querySelectorAll( '#dnav [data-target]' ).forEach( function ( b ) {
			if ( b.hidden ) { return; }
			var el = document.getElementById( b.dataset.target );
			if ( ! el ) { return; }
			var top = el.getBoundingClientRect().top;
			if ( top <= 130 && top > best ) { best = top; cur = b.dataset.nav; }
		} );
		document.querySelectorAll( '#dnav button' ).forEach( function ( b ) {
			b.setAttribute( 'aria-current', b.dataset.nav === cur ? 'true' : 'false' );
		} );
	}

	// Toggle the "How to read this" guide beside live, and remember the choice.
	function setGuide( on ) {
		var app = document.getElementById( 'defog-app' );
		if ( ! app ) { return; }
		app.classList.toggle( 'guide-on', !! on );
		var gt = document.getElementById( 'guideToggle' );
		if ( gt ) { gt.setAttribute( 'aria-pressed', on ? 'true' : 'false' ); }
		try { localStorage.setItem( 'defog_guide_hidden', on ? '0' : '1' ); } catch ( e ) {}
	}

	/* ---------- footer freshness (relative "updated X ago") ---------- */
	function relTime( ts ) {
		var s = Math.round( ( Date.now() - ts ) / 1000 );
		if ( s < 45 ) { return M.updated; }
		var m = Math.round( s / 60 );
		if ( m < 60 ) { return ( M.updatedMin || '' ).replace( '%d', m ); }
		var h = Math.round( m / 60 );
		return ( M.updatedHr || '' ).replace( '%d', h );
	}
	function refreshFoot() {
		var el = document.getElementById( 'freshTxt' );
		if ( el && state.lastUpdated ) { el.textContent = relTime( state.lastUpdated ); }
	}

	/* ---------- feedback ("Missing something?") ---------- */
	function openFeedback() {
		document.getElementById( 'fbModal' ).classList.add( 'open' );
		document.getElementById( 'fbBackdrop' ).classList.add( 'open' );
		document.getElementById( 'fbModal' ).setAttribute( 'aria-hidden', 'false' );
		var ta = document.getElementById( 'fbText' );
		if ( ta ) { setTimeout( function () { ta.focus(); }, 60 ); }
	}
	function closeFeedback() {
		document.getElementById( 'fbModal' ).classList.remove( 'open' );
		document.getElementById( 'fbBackdrop' ).classList.remove( 'open' );
		document.getElementById( 'fbModal' ).setAttribute( 'aria-hidden', 'true' );
	}
	function sendFeedback() {
		var ta = document.getElementById( 'fbText' );
		var txt = ( ta && ta.value || '' ).trim();
		if ( ! txt ) { if ( ta ) { ta.focus(); } return; }
		// Build a mailto: link — opens the user's own email client, nothing is sent by the server.
		var subject = M.fbSubject || 'Defog Analytics feedback';
		var body = txt + '\n\n— Defog v' + ( cfg.version || '' ) + ' · ' + location.hostname;
		var mail = 'mailto:' + encodeURIComponent( cfg.feedbackEmail || '' )
			+ '?subject=' + encodeURIComponent( subject )
			+ '&body=' + encodeURIComponent( body );
		window.location.href = mail;
		closeFeedback();
	}

	function wire() {
		document.querySelectorAll( '.seg button' ).forEach( function ( b ) {
			b.onclick = function () {
				document.querySelectorAll( '.seg button' ).forEach( function ( x ) { x.setAttribute( 'aria-pressed', 'false' ); } );
				b.setAttribute( 'aria-pressed', 'true' ); state.range = b.dataset.range; loadStats( true ); loadJourney();
			};
		} );
		document.querySelectorAll( '.metric-toggle button' ).forEach( function ( b ) {
			b.onclick = function () {
				document.querySelectorAll( '.metric-toggle button' ).forEach( function ( x ) { x.setAttribute( 'aria-pressed', 'false' ); } );
				b.setAttribute( 'aria-pressed', 'true' ); state.metric = b.dataset.metric;
				if ( state.stats ) { renderChart(); }
			};
		} );
	}

	function init() {
		var app = document.getElementById( 'defog-app' );
		if ( ! app ) { return; }
		app.innerHTML = shell();
		wire();
		var pc = document.getElementById( 'pdClose' ), pb = document.getElementById( 'pdBackdrop' );
		if ( pc ) { pc.onclick = closePageDetail; }
		if ( pb ) { pb.onclick = closePageDetail; }

		// Feedback modal wiring.
		var fo = document.getElementById( 'fbOpen' ), fc = document.getElementById( 'fbClose' );
		var fbb = document.getElementById( 'fbBackdrop' ), fs = document.getElementById( 'fbSend' );
		if ( fo ) { fo.onclick = openFeedback; }
		if ( fc ) { fc.onclick = closeFeedback; }
		if ( fbb ) { fbb.onclick = closeFeedback; }
		if ( fs ) { fs.onclick = sendFeedback; }

		// Section navigation: chips scroll to their section; a passive scroll
		// listener keeps the active chip in sync (rAF-throttled).
		document.querySelectorAll( '#dnav button' ).forEach( function ( b ) {
			b.onclick = function () {
				var behavior = REDUCED ? 'auto' : 'smooth';
				if ( b.dataset.target ) {
					var el = document.getElementById( b.dataset.target );
					if ( el && ! el.hidden ) { el.scrollIntoView( { behavior: behavior, block: 'start' } ); }
				} else {
					window.scrollTo( { top: 0, behavior: behavior } );
				}
			};
		} );
		var navTick = false;
		window.addEventListener( 'scroll', function () {
			if ( navTick ) { return; }
			navTick = true;
			requestAnimationFrame( function () { navTick = false; navActive(); } );
		}, { passive: true } );

		// Dashboard guide: restore the remembered state, then wire toggle + hide.
		var guideHidden = false;
		try { guideHidden = localStorage.getItem( 'defog_guide_hidden' ) === '1'; } catch ( e ) {}
		setGuide( ! guideHidden );
		var gt = document.getElementById( 'guideToggle' ), gh = document.getElementById( 'guideHide' );
		if ( gt ) { gt.onclick = function () { setGuide( ! app.classList.contains( 'guide-on' ) ); }; }
		if ( gh ) { gh.onclick = function () { setGuide( false ); }; }

		document.addEventListener( 'keydown', function ( e ) { if ( e.key === 'Escape' ) { closePageDetail(); closeFeedback(); } } );
		loadStats( false );
		loadJourney();
		loadRealtime();
		scheduleRealtime();

		// Track interaction so polling can stand down on an abandoned tab.
		[ 'mousemove', 'keydown', 'click', 'scroll' ].forEach( function ( ev ) {
			document.addEventListener( ev, function () { lastActivity = Date.now(); }, { passive: true } );
		} );

		// Auto-refresh: silently reload the ranged dashboard every 60s (skip when the
		// tab is hidden to save requests), and keep the footer's "updated X ago" fresh.
		setInterval( function () { if ( ! document.hidden ) { loadStats( false ); } }, 60000 );
		setInterval( refreshFoot, 15000 );
		document.addEventListener( 'visibilitychange', function () {
			if ( ! document.hidden ) { loadStats( false ); refreshFoot(); wakeRealtime(); }
		} );
	}

	if ( document.readyState === 'loading' ) {
		document.addEventListener( 'DOMContentLoaded', init );
	} else { init(); }
}() );
