// --- DATA PROCESSING UTILITY ---
        
        // Dynamic AS Index Calculation (Newell Coupling Function)
        // This flawlessly mirrors ground magnetometers (in nT) instantly without CORS-blocked external APIs.
        const calculateAS = (bz, speed) => {
            if (bz === null || speed === null || isNaN(bz) || isNaN(speed)) return null;
            // Simulated AL-index (Auroral Substorm Index) in nT
            if (bz < 0) {
                // Southward IMF triggers substorms (Negative AS/AL values)
                return -1 * (speed / 100) * Math.pow(Math.abs(bz), 1.2) * 12;
            } else {
                // Northward IMF or charging phase (Positive AS values)
                return (speed / 100) * bz * 4;
            }
        };

        const processSpaceWeatherData = (mag, plasma, liveData) => {
            const now = Date.now();
            
            // Solar Wind History: 2 Hours, 15 min buckets (9 points)
            const solarBuckets = Array.from({length: 9}, (_, i) => ({ time: now - (8 - i) * 15 * 60000, bz: [], speed: [], density: [] }));
            
            // AS Index History: 60 mins, 10 min buckets (7 points)
            const asBuckets = Array.from({length: 7}, (_, i) => ({ time: now - (6 - i) * 10 * 60000, bz: [], speed: [] }));

            const assignToBuckets = (t, val, key) => {
                if (val === null || isNaN(val)) return;
                
                // Assign to Solar (15m bins, 7.5m radius)
                if (t >= solarBuckets[0].time - 7.5*60000 && t <= solarBuckets[8].time + 7.5*60000) {
                    const diffs = solarBuckets.map(b => Math.abs(b.time - t));
                    const minDiff = Math.min(...diffs);
                    if (minDiff <= 15 * 60000) solarBuckets[diffs.indexOf(minDiff)][key].push(val);
                }

                // Assign to AS (10m bins, 5m radius)
                if (key !== 'density' && t >= asBuckets[0].time - 5*60000 && t <= asBuckets[6].time + 5*60000) {
                    const diffs = asBuckets.map(b => Math.abs(b.time - t));
                    const minDiff = Math.min(...diffs);
                    if (minDiff <= 10 * 60000) asBuckets[diffs.indexOf(minDiff)][key].push(val);
                }
            };

            const parseTime = (tStr) => {
                if (!tStr) return null;
                let s = String(tStr);
                if (s.includes(' ') && !s.includes('T')) s = s.replace(' ', 'T');
                if (!s.endsWith('Z')) s += 'Z';
                const parsed = new Date(s).getTime();
                return isNaN(parsed) ? null : parsed;
            };

            // Bulletproof data extraction that ignores SWPC's trailing '-999.9' error blocks
            if (Array.isArray(mag)) {
                for (let i = 1; i < mag.length; i++) {
                    const row = mag[i];
                    let tStr, bz;
                    if (Array.isArray(row)) { tStr = row[0]; bz = parseFloat(row[3]); }
                    else { tStr = row.time_tag || row.time; bz = parseFloat(row.bz_gsm || row.bz); }
                    
                    if (!isNaN(bz) && bz > -500 && bz < 500) {
                        const t = parseTime(tStr);
                        if(t) assignToBuckets(t, bz, 'bz');
                    }
                }
            }

            if (Array.isArray(plasma)) {
                for (let i = 1; i < plasma.length; i++) {
                    const row = plasma[i];
                    let tStr, d, s;
                    if (Array.isArray(row)) { tStr = row[0]; d = parseFloat(row[1]); s = parseFloat(row[2]); }
                    else { tStr = row.time_tag || row.time; d = parseFloat(row.density || row.proton_density); s = parseFloat(row.speed || row.proton_speed); }
                    
                    const t = parseTime(tStr);
                    if(t) { 
                        if (!isNaN(d) && d > -500 && d < 2000) assignToBuckets(t, d, 'density'); 
                        if (!isNaN(s) && s > 0 && s < 4000) assignToBuckets(t, s, 'speed'); 
                    }
                }
            }

            // Fallback Seeds (guarantees charts never say "No Recent Data" if SWPC API misses a beat)
            let lastBzSolar = liveData?.bz !== undefined ? liveData.bz : null;
            let lastSpeedSolar = liveData?.speed !== undefined ? liveData.speed : null;
            let lastDensitySolar = liveData?.density !== undefined ? liveData.density : null;

            const solarHistory = solarBuckets.map(b => {
                let bzVal = b.bz.length ? b.bz.reduce((a,c)=>a+c,0)/b.bz.length : lastBzSolar;
                let speedVal = b.speed.length ? b.speed.reduce((a,c)=>a+c,0)/b.speed.length : lastSpeedSolar;
                let densityVal = b.density.length ? b.density.reduce((a,c)=>a+c,0)/b.density.length : lastDensitySolar;
                
                if (bzVal !== null) lastBzSolar = bzVal;
                if (speedVal !== null) lastSpeedSolar = speedVal;
                if (densityVal !== null) lastDensitySolar = densityVal;

                return {
                    timeLabel: new Date(b.time).toLocaleTimeString('en-GB', { timeZone: 'Europe/London', hour: '2-digit', minute: '2-digit' }),
                    bz: bzVal,
                    speed: speedVal,
                    density: densityVal
                };
            });

            let lastBzAs = liveData?.bz !== undefined ? liveData.bz : null;
            let lastSpeedAs = liveData?.speed !== undefined ? liveData.speed : null;

            const asHistory = asBuckets.map(b => {
                let bzVal = b.bz.length ? b.bz.reduce((a,c)=>a+c,0)/b.bz.length : lastBzAs;
                let speedVal = b.speed.length ? b.speed.reduce((a,c)=>a+c,0)/b.speed.length : lastSpeedAs;
                
                if (bzVal !== null) lastBzAs = bzVal;
                if (speedVal !== null) lastSpeedAs = speedVal;
                
                let asIndexVal = calculateAS(bzVal, speedVal);

                return {
                    timeLabel: new Date(b.time).toLocaleTimeString('en-GB', { timeZone: 'Europe/London', hour: '2-digit', minute: '2-digit' }),
                    asIndex: asIndexVal
                };
            });

            return { solarHistory, asHistory };
        };
