/**
 * EverShelf - Main Application JS
 * Complete pantry management with barcode scanning, AI identification,
 * Bring! shopping list integration, recipe generation, and TTS cooking mode.
 *
 * @author Stimpfl Daniel <evershelfproject@gmail.com>
 * @license MIT
 */

// ===== REMOTE LOGGING + ERROR REPORTING =====
// Two-tier system:
//  1. remoteLog() — batched INFO/WARN/ERROR → existing client_log endpoint (debug tail)
//  2. reportError() — immediate single POST → report_error endpoint → GitHub Issue

const _remoteLogBuffer = [];
let _remoteLogTimer = null;
const _origConsoleError = console.error.bind(console);
const _origConsoleWarn = console.warn.bind(console);

function remoteLog(level, ...args) {
    const msg = args.map(a => {
        if (a instanceof Error) return `${a.name}: ${a.message}`;
        if (typeof a === 'object') try { return JSON.stringify(a); } catch { return String(a); }
        return String(a);
    }).join(' ');
    _remoteLogBuffer.push(`[${level}] ${msg}`);
    if (!_remoteLogTimer) {
        _remoteLogTimer = setTimeout(flushRemoteLog, 2000);
    }
}

function flushRemoteLog() {
    _remoteLogTimer = null;
    if (_remoteLogBuffer.length === 0) return;
    const msgs = _remoteLogBuffer.splice(0);
    fetch(`api/index.php?action=client_log`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ messages: msgs })
    }).catch(() => {});
}

// Override console.error and console.warn to also send remotely
console.error = function(...args) {
    _origConsoleError(...args);
    remoteLog('ERROR', ...args);
};
console.warn = function(...args) {
    _origConsoleWarn(...args);
    remoteLog('WARN', ...args);
};

// ── Error reporter: creates/updates GitHub Issues ────────────────────────────
// Rate-limit client-side: max 1 report per fingerprint per page session.
const _reportedFingerprints = new Set();

function reportError(payload) {
    // Build fingerprint to deduplicate within the same page session
    const fp = `${payload.source}:${payload.type}:${String(payload.message).slice(0, 120)}`;
    if (_reportedFingerprints.has(fp)) return;
    _reportedFingerprints.add(fp);

    const body = Object.assign({
        source:     'pwa',
        version:    document.querySelector('.header-version')?.textContent?.trim() || '',
        url:        location.href,
        user_agent: navigator.userAgent,
    }, payload);

    fetch('api/index.php?action=report_error', {
        method:  'POST',
        headers: { 'Content-Type': 'application/json' },
        body:    JSON.stringify(body),
    }).catch(() => {}); // fire-and-forget; never throw from error handler
    // Note: the server will also skip issue creation if this version is not the latest.
}

// ── Webapp update notification ───────────────────────────────────────────────
// Checks both the deployed webapp version and the latest GitHub release.
// Fires on tab focus and every 5 minutes.
const _loadedVersion = (document.querySelector('.header-version')?.textContent?.trim() || '').replace(/^v/, '');

// ── Gemini AI availability ────────────────────────────────────────────────────
// Set to true in _initApp / syncSettingsFromDB once server confirms key is set.
// All AI entry points call _requireGemini() before opening camera / API calls.
let _geminiAvailable = false;
let _demoMode = false;

function _requireGemini() {
    if (_geminiAvailable) return true;
    showToast(
        '🤖 ' + t('error.no_api_key'),
        'warning',
        6000
    );
    return false;
}

// Update Gemini button visual state to signal no key configured
function _updateGeminiButtonState() {
    const btn = document.querySelector('.header-gemini-btn');
    if (!btn) return;
    if (_geminiAvailable) {
        btn.classList.remove('header-btn-no-ai');
        btn.removeAttribute('title');
        btn.setAttribute('title', 'Chat con Gemini');
    } else {
        btn.classList.add('header-btn-no-ai');
        btn.setAttribute('title', '🤖 Gemini non configurato — imposta GEMINI_API_KEY nelle impostazioni');
    }
}

function _applyDemoModeUI() {
    if (!_demoMode) return;
    // In demo mode Gemini is always "available" — no real key needed
    _geminiAvailable = true;
    _updateGeminiButtonState();
    // Hide the settings ⚙️ nav button
    document.querySelectorAll('.nav-btn[data-page="settings"]').forEach(btn => {
        btn.style.display = 'none';
    });
    // Prevent the setup wizard from showing
    const wizard = document.getElementById('setup-wizard');
    if (wizard) wizard.style.display = 'none';
    // Show a small demo badge in the header
    const headerLeft = document.getElementById('header-left');
    if (headerLeft && !document.getElementById('_demo_badge')) {
        const badge = document.createElement('span');
        badge.id = '_demo_badge';
        badge.textContent = 'DEMO';
        badge.style.cssText = 'font-size:0.6rem;font-weight:800;letter-spacing:0.08em;background:rgba(251,191,36,0.35);color:#fef3c7;border:1px solid rgba(251,191,36,0.5);border-radius:4px;padding:2px 5px;white-space:nowrap;';
        headerLeft.appendChild(badge);
    }
}

function _checkWebappUpdate() {
    const STORAGE_KEY  = '_evershelf_update_checked_at';
    const SEEN_KEY     = '_evershelf_update_seen_ts';
    const TTL_MS       = 5 * 60 * 1000;
    const now = Date.now();
    const lastCheck = parseInt(localStorage.getItem(STORAGE_KEY) || '0', 10);
    if (now - lastCheck < TTL_MS) return;
    localStorage.setItem(STORAGE_KEY, String(now));

    fetch('api/index.php?action=check_update', { method: 'GET' })
        .then(r => r.ok ? r.json() : null)
        .then(data => {
            if (!data) return;
            // Already showing — don't stack
            if (document.getElementById('_header_update_pill')) return;

            // ── Check 1: server has a newer version deployed since this page loaded ──
            const serverVer     = (data.webapp_version || '').replace(/^v/, '');
            const deployChanged = serverVer && _loadedVersion && serverVer !== _loadedVersion;

            // ── Check 2: a newer GitHub release not yet acknowledged ──
            const publishedAt  = data.published_at || '';
            const seenTs       = localStorage.getItem(SEEN_KEY) || '';
            const latestTag    = (data.latest_tag || '').replace(/^v/, '');
            const releaseNewer = publishedAt && publishedAt !== seenTs &&
                                 /^\d+\.\d+/.test(latestTag) &&
                                 _loadedVersion && latestTag !== _loadedVersion;

            if (!deployChanged && !releaseNewer) return;

            // ── Show update badge alongside the title (title stays intact) ──
            const badge = document.getElementById('header-update-badge');
            if (!badge) return;

            const versionLabel = deployChanged
                ? (serverVer ? `v${serverVer}` : 'Nuova versione')
                : (latestTag ? `v${latestTag}` : 'Nuova versione');

            const hideBadge = () => {
                badge.style.display = 'none';
                badge.innerHTML = '';
                if (!deployChanged) localStorage.setItem(SEEN_KEY, publishedAt);
            };

            badge.innerHTML =
                `<span class="header-update-badge-label">⬆️ ${versionLabel}</span>` +
                `<button class="header-update-btn" onclick="window.location.reload()">Aggiorna</button>` +
                `<button class="header-update-close" id="_header_update_close">✕</button>`;
            badge.style.display = 'inline-flex';

            document.getElementById('_header_update_close').onclick = (e) => {
                e.stopPropagation();
                hideBadge();
            };
            // Auto-hide after 60 s without marking as seen
            setTimeout(() => { if (badge.style.display !== 'none') hideBadge(); }, 60000);
        })
        .catch(() => {});
}

// ── Global uncaught error handler ────────────────────────────────────────────
window.addEventListener('error', function(e) {
    const msg = e.message || String(e.error);
    // Ignore benign third-party noise
    if (/Script error/i.test(msg)) return;
    remoteLog('UNCAUGHT', `${msg} at ${e.filename}:${e.lineno}:${e.colno}`);
    reportError({
        type:    'uncaught-error',
        message: msg,
        stack:   e.error?.stack || '',
        context: { filename: e.filename, lineno: e.lineno, colno: e.colno },
    });
});

window.addEventListener('unhandledrejection', function(e) {
    const reason = e.reason;
    const msg  = reason instanceof Error ? reason.message : String(reason);
    const stack = reason instanceof Error ? (reason.stack || '') : '';
    remoteLog('UNHANDLED_PROMISE', msg);
    reportError({
        type:    'unhandled-promise',
        message: msg,
        stack:   stack,
    });
});

// ===== CONFIGURATION =====
const API_BASE = 'api/index.php';

// ===== SMART SCALE GATEWAY =====
// Connects to the Android BLE-WebSocket gateway and provides auto weight reading.

let _scaleEs = null;           // EventSource for the SSE relay
let _scaleConnected = false;
let _scaleDevice = null;
let _scaleBattery = null;
let _scaleReconnectTimer = null;
let _scaleWeightCallback = null; // pending on-demand weight request callback
let _scaleLatestWeight = null;   // last received weight message
let _scaleAutoConfirmTimer = null; // countdown timer for auto-confirm after stable weight
let _scaleAutoConfirmRAF   = null; // rAF handle for auto-confirm progress bar animation
let _scaleStabilityTimer   = null; // setTimeout: wait 5 s stable before starting confirm bar
let _scaleStabilityRAF     = null; // rAF handle for stability progress bar in the live box
let _scaleStabilityVal     = null; // value we are currently timing for stability
let _scaleUserDismissed    = false; // user tapped or edited → don't retrigger for same value
let _scaleRecipeAutoFillPaused = false; // pause flag for recipe-use modal only
let _scaleLastConfirmedGrams = null; // grams of last auto-confirmed weight (to detect product change)
let _scaleLastStableGrams = null; // last accepted stable reading in grams (for jitter filtering)

function _scaleToGrams(value, unit) {
    if (!isFinite(value)) return null;
    const u = (unit || 'g').toLowerCase();
    if (u === 'kg') return value * 1000;
    if (u === 'lbs' || u === 'lb') return value * 453.592;
    if (u === 'oz') return value * 28.3495;
    return value; // g / ml treated as grams-equivalent for stability filtering
}

function scaleInit() {
    const s = getSettings();
    const indicator = document.getElementById('scale-status-indicator');
    if (!s.scale_enabled || !s.scale_gateway_url) {
        if (indicator) indicator.style.display = 'none';
        if (_scaleEs) { try { _scaleEs.close(); } catch(e) {} _scaleEs = null; }
        return;
    }
    if (indicator) indicator.style.display = '';
    _scaleConnect(s.scale_gateway_url);
}

function _scaleConnect(url) {
    if (_scaleEs) { try { _scaleEs.close(); } catch(e) {} _scaleEs = null; }
    if (_scaleReconnectTimer) { clearTimeout(_scaleReconnectTimer); _scaleReconnectTimer = null; }
    try {
        // Connect via the PHP SSE relay so the HTTPS page is not blocked by mixed-content
        _scaleEs = new EventSource('api/scale_relay.php?url=' + encodeURIComponent(url));
        _scaleEs.onopen  = () => _scaleUpdateStatus('searching');
        _scaleEs.onmessage = (evt) => {
            try { _scaleOnMessage(JSON.parse(evt.data)); } catch(e) {}
        };
        _scaleEs.onerror = () => {
            _scaleConnected = false;
            _scaleDevice = null;
            _scaleUpdateStatus('disconnected');
            // EventSource auto-reconnects; no manual timer needed
        };
    } catch(e) {
        _scaleUpdateStatus('error');
    }
}

function _scaleOnMessage(msg) {
    if (msg.type === 'status') {
        _scaleConnected = msg.state === 'connected';
        _scaleDevice = msg.device || null;
        _scaleBattery = msg.battery ?? null;
        _scaleUpdateStatus(_scaleConnected ? 'connected' : 'searching');
        // Refresh all scale UI elements immediately so buttons/live-box appear
        // without requiring a manual page refresh
        updateScaleReadButtons();
    } else if (msg.type === 'weight') {
        // Ignore negative weight values (tare artifacts, sensor noise)
        const rawValue = parseFloat(msg.value);
        if (rawValue < 0) return;

        // Ignore sub-gram jitter for stability decisions: only integer-gram changes matter.
        let effectiveStable = !!msg.stable;
        const grams = _scaleToGrams(rawValue, msg.unit);
        if (grams !== null) {
            if (effectiveStable) {
                _scaleLastStableGrams = grams;
            } else if (_scaleLastStableGrams !== null) {
                if (Math.round(grams) === Math.round(_scaleLastStableGrams)) {
                    effectiveStable = true;
                }
            }
            if (effectiveStable) {
                _scaleLastStableGrams = grams;
            }
        }

        const liveMsg = effectiveStable === msg.stable ? msg : { ...msg, stable: effectiveStable };
        _scaleLatestWeight = liveMsg;
        // Update live reading modal overlay if visible (scale-read modal)
        const live = document.getElementById('scale-reading-live');
        if (live) live.textContent = `${msg.value} ${msg.unit || 'kg'}${liveMsg.stable ? ' ✓' : ' …'}`;
        // Also update edit-form inline scale reading if visible
        const editLive = document.getElementById('edit-scale-reading');
        if (editLive) editLive.textContent = `${msg.value} ${msg.unit || 'kg'}${liveMsg.stable ? ' ✓' : ' …'}`;
        // Always update the persistent live box on the use page (every message, stable or not)
        _scaleUpdateLiveBox(liveMsg);
        // If weight is NOT stable: stop any running timer/bar but keep the sentinel value.
        // The sentinel is reset only when a genuinely different stable value arrives.
        if (!liveMsg.stable) {
            _cancelScaleTimersOnly();
        }
        // Fulfil pending callback on stable reading
        if (liveMsg.stable && _scaleWeightCallback) {
            const cb = _scaleWeightCallback;
            _scaleWeightCallback = null;
            cb(liveMsg);
        }
        // Drive stability logic on use page
        if (liveMsg.stable && _currentPageId === 'use') {
            _scaleAutoFillUse(liveMsg);
        }
        // Same for recipe-use modal
        if (liveMsg.stable && document.getElementById('ruse-quantity') && !_scaleRecipeAutoFillPaused) {
            _scaleAutoFillRecipeUse(liveMsg);
        }
    }
}

/**
 * Returns the liquid density (g/ml) for a product based on its name/category.
 * Used to convert scale grams → ml for products stored in ml.
 */
function _scaleDensityForProduct(product) {
    const n   = (product?.name     || '').toLowerCase();
    const cat = (product?.category || '').toLowerCase();
    // Oils (lighter than water)
    if (/olio.oliva|olive.oil/.test(n))               return 0.91;
    if (/olio.girasole|sunflower.oil/.test(n))         return 0.92;
    if (/\bolio\b|\boil\b/.test(n))                   return 0.92;
    // Spirits / alcohol (lighter than water)
    if (/vodka|whisky|whiskey|grappa|rum|gin\b/.test(n)) return 0.94;
    // Vinegar, wine, beer (close to water)
    if (/aceto|vinegar/.test(n))                      return 1.01;
    if (/\bvino\b|\bwine\b|\bbirra\b|\bbeer\b/.test(n)) return 1.00;
    // Milk & dairy liquids
    if (/\blatte\b|\bmilk\b/.test(n))                 return 1.03;
    if (/panna|cream/.test(n))                        return 1.01;
    if (/yogurt/.test(n))                             return 1.05;
    // Juice
    if (/succo|juice|spremuta/.test(n))               return 1.04;
    // Honey / syrups (dense)
    if (/miele|honey|sciroppo|syrup/.test(n))         return 1.40;
    // Water / sparkling
    if (/\bacqua\b|\bwater\b/.test(n))                return 1.00;
    // Category-level fallbacks
    if (/latticin/.test(cat))                         return 1.03;
    if (/condiment/.test(cat))                        return 0.92; // likely oil-based
    if (/bevand/.test(cat))                           return 1.00;
    return 1.00; // safe default (water)
}

/**
 * Update the persistent live-weight box on the use page (called on every weight message).
 * Shows raw scale reading in real time regardless of stability or unit compatibility.
 */
function _scaleUpdateLiveBox(msg) {
    const box    = document.getElementById('scale-live-box');
    if (!box) return;
    const s = getSettings();
    const active = s.scale_enabled && s.scale_gateway_url && _scaleConnected &&
                   _currentPageId === 'use';
    box.style.display = active ? '' : 'none';
    if (!active) return;

    const raw     = parseFloat(msg.value);
    const rawUnit = (msg.unit || 'kg').toLowerCase();
    // Convert to grams for the < 10 g threshold check
    let gForCheck = isFinite(raw) ? raw : 0;
    if (rawUnit === 'kg')  gForCheck = raw * 1000;
    if (rawUnit === 'lbs' || rawUnit === 'lb') gForCheck = raw * 453.592;

    const valEl   = document.getElementById('scale-live-val');
    const lblEl   = document.getElementById('scale-live-label');

    if (isFinite(raw) && gForCheck < 10 && gForCheck > 0) {
        // Weight too low — show red flashing warning
        box.classList.add('scale-low-weight');
        if (valEl) valEl.textContent = `${raw} ${msg.unit || 'kg'}`;
        if (lblEl) lblEl.textContent = t('scale.low_weight');
    } else {
        box.classList.remove('scale-low-weight');
        const stIcon = msg.stable ? ' ✓' : ' …';
        // Show converted ML if target unit is ml (instead of raw grams)
        let displayVal = `${isFinite(raw) ? raw : '—'} ${msg.unit || 'kg'}`;
        let targetUnit = null;
        if (_useConfMode && _useConfMode._activeUnit === 'sub') {
            targetUnit = (_useConfMode.packageUnit || '').toLowerCase();
        } else {
            targetUnit = _useNormalUnit;
        }
        if (targetUnit === 'ml' && rawUnit !== 'ml' && isFinite(raw) && raw > 0) {
            let grams = raw;
            if (rawUnit === 'kg') grams = raw * 1000;
            else if (rawUnit === 'lbs' || rawUnit === 'lb') grams = raw * 453.592;
            else if (rawUnit === 'oz') grams = raw * 28.3495;
            const density = _scaleDensityForProduct(currentProduct);
            const ml = Math.round(grams / density);
            displayVal = `${ml} ml`;
        }
        if (valEl) valEl.textContent = displayVal + stIcon;
        if (lblEl) {
            lblEl.textContent = '';
        }
    }
}

/**
 * Auto-fill: called on every STABLE weight message while on the use page.
 * - Updates the live box (conversion hint)
 * - After 5 s of stable unchanged value: fills the input and starts the confirm progress bar
 * - If value changes: resets the 5-s stability wait
 * - If user dismissed (touch/edit): does nothing for the same value; resets on value change
 */
function _scaleAutoFillUse(msg) {
    if (!msg) return;

    // Determine target unit
    let unit;
    if (_useConfMode && _useConfMode._activeUnit === 'sub') {
        unit = (_useConfMode.packageUnit || '').toLowerCase();
    } else {
        unit = _useNormalUnit;
    }
    if (unit !== 'g' && unit !== 'ml') return; // pz / conf-unit: ignore

    const rawVal = parseFloat(msg.value);
    if (!isFinite(rawVal) || rawVal <= 0) return;
    const srcUnit = (msg.unit || '').toLowerCase();

    // Normalise to grams
    let grams;
    let scaleAlreadyMl = false;
    if      (srcUnit === 'g')                       grams = rawVal;
    else if (srcUnit === 'kg')                      grams = rawVal * 1000;
    else if (srcUnit === 'lbs' || srcUnit === 'lb') grams = rawVal * 453.592;
    else if (srcUnit === 'oz')                      grams = rawVal * 28.3495;
    else if (srcUnit === 'ml')  { grams = rawVal; scaleAlreadyMl = true; }
    else                                            grams = rawVal;

    // Reject if raw grams < 10 (piatto vuoto / tara / rumore)
    if (grams < 10) {
        _cancelScaleStabilityWait(); // stop bar only; keep sentinel & userDismissed
        return;
    }

    // Reject if weight hasn't changed enough from last confirmed reading (same product still on scale)
    if (_scaleLastConfirmedGrams !== null && Math.abs(grams - _scaleLastConfirmedGrams) < 10) {
        return;
    }

    // Convert to target unit
    let val;
    let hintExtra = '';
    if (unit === 'g') {
        if (scaleAlreadyMl) {
            const density = _scaleDensityForProduct(currentProduct);
            val = Math.round(grams * density);
            if (density !== 1.00) hintExtra = ' ' + t('scale.density_hint', { density });
        } else {
            val = Math.round(grams);
        }
    } else {
        if (scaleAlreadyMl) {
            val = Math.round(grams);
        } else {
            const density = _scaleDensityForProduct(currentProduct);
            val = Math.round(grams / density);
            if (density !== 1.00) hintExtra = ' ' + t('scale.density_hint', { density });
        }
    }

    // Reject if converted value < 10 (density edge case)
    if (val < 10) {
        _cancelScaleStabilityWait();
        return;
    }

    if (val !== _scaleStabilityVal) {
        // New (different) weight → clear dismissal, restart stability wait
        _scaleStabilityVal = val;
        _scaleUserDismissed = false;
        _cancelScaleTimersOnly();
        _startScaleStabilityWait(() => {
            // Fill the input after 5 s of stable weight
            const inp = document.getElementById('use-quantity');
            if (inp) inp.value = val;
            // Start the 5-s confirm progress bar
            _startScaleAutoConfirm(() => {
                _scaleLastConfirmedGrams = grams;
                const form = document.querySelector('#page-use form');
                if (form) form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
            }, 'btn-use-submit');
        });
    } else if (!_scaleUserDismissed && !_scaleStabilityTimer && !_scaleAutoConfirmTimer) {
        // Same value, not dismissed, no timer running (e.g. after brief !stable interruption)
        // → restart stability wait so it eventually completes
        _cancelScaleTimersOnly();
        _startScaleStabilityWait(() => {
            const inp = document.getElementById('use-quantity');
            if (inp) inp.value = val;
            _startScaleAutoConfirm(() => {
                _scaleLastConfirmedGrams = grams;
                const form = document.querySelector('#page-use form');
                if (form) form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true }));
            }, 'btn-use-submit');
        });
    }
    // Same value + dismissed → do nothing (user explicitly dismissed this value)
    // Same value + timer running → do nothing (already counting down)
}

/**
 * Auto-fill ruse-quantity input from a stable scale reading (recipe-use modal).
 */
function _scaleAutoFillRecipeUse(msg) {
    if (!msg) return;
    let unit;
    if (_recipeUseConfMode && _recipeUseConfMode._activeUnit === 'sub') {
        unit = (_recipeUseConfMode.packageUnit || '').toLowerCase();
    } else {
        unit = _recipeUseNormalUnit;
    }
    if (unit !== 'g' && unit !== 'ml') return;

    const rawVal = parseFloat(msg.value);
    if (!isFinite(rawVal) || rawVal <= 0) return;
    const srcUnit = (msg.unit || '').toLowerCase();

    let grams;
    let scaleAlreadyMl = false;
    if      (srcUnit === 'g')                       grams = rawVal;
    else if (srcUnit === 'kg')                      grams = rawVal * 1000;
    else if (srcUnit === 'lbs' || srcUnit === 'lb') grams = rawVal * 453.592;
    else if (srcUnit === 'oz')                      grams = rawVal * 28.3495;
    else if (srcUnit === 'ml')  { grams = rawVal; scaleAlreadyMl = true; }
    else                                            grams = rawVal;

    let val;
    let hintExtra = '';
    if (unit === 'g') {
        if (scaleAlreadyMl) {
            const density = _scaleDensityForProduct(currentProduct);
            val = Math.round(grams * density);
            if (density !== 1.00) hintExtra = ' ' + t('scale.density_hint', { density });
        } else {
            val = Math.round(grams);
        }
    } else {
        if (scaleAlreadyMl) {
            val = Math.round(grams);
        } else {
            const density = _scaleDensityForProduct(currentProduct);
            val = Math.round(grams / density);
            if (density !== 1.00) hintExtra = ' ' + t('scale.density_hint', { density });
        }
    }

    // Update live box in modal — show the already-converted value in the target unit
    const livVal   = document.getElementById('ruse-scale-live-val');
    const livLabel = document.getElementById('ruse-scale-live-label');
    const livStatus = document.getElementById('ruse-scale-live-status');
    if (livVal) {
        // val is already converted to target unit (g or ml); show it directly
        if (val >= 10) {
            livVal.textContent = `${val} ${unit}`;
        } else {
            // val not usable yet — show raw reading
            livVal.textContent = `${msg.value} ${msg.unit || 'kg'}`;
        }
    }
    if (livStatus) livStatus.textContent = msg.stable ? t('scale.stable') : '…';

    // Update live hint in modal with the raw scale reading always
    const hint = document.getElementById('ruse-scale-hint');
    if (hint) {
        hint.textContent = `⚖️ Bilancia: ${msg.value} ${msg.unit || 'kg'}${msg.stable ? ' ✓' : ' …'}`;
        if (unit === 'ml' && srcUnit !== 'ml') {
            hint.textContent += ' ' + t('scale.ml_hint');
        }
        hint.style.display = '';
    }

    if (val < 10) {
        _cancelScaleStabilityWait(); // stop bar only; keep sentinel
        if (livLabel) livLabel.textContent = t('scale.weight_too_low');
        return;
    }

    // Reject if weight hasn't changed enough from last confirmed reading
    if (_scaleLastConfirmedGrams !== null && Math.abs(grams - _scaleLastConfirmedGrams) < 10) {
        return;
    }

    if (val !== _scaleStabilityVal) {
        _scaleStabilityVal = val;
        _scaleUserDismissed = false;
        _cancelScaleTimersOnly();
        if (livLabel) livLabel.textContent = t('scale.weight_detected');
        // Hide confirm bar when new value arrives
        const confirmWrap = document.getElementById('ruse-scale-confirm-wrap');
        if (confirmWrap) confirmWrap.style.display = 'none';
        _startScaleStabilityWait(() => {
            const inp = document.getElementById('ruse-quantity');
            if (inp) inp.value = val;
            if (hint) {
                hint.textContent = `⚖️ Peso bilancia: ${val} ${unit}${hintExtra}`;
                hint.style.display = '';
            }
            if (livLabel) livLabel.textContent = t('scale.auto_confirm', { val, unit });
            if (livVal) livVal.style.color = '#22c55e';
            const confirmWrap2 = document.getElementById('ruse-scale-confirm-wrap');
            if (confirmWrap2) { confirmWrap2.style.display = ''; }
            const confirmBar = document.getElementById('ruse-scale-confirm-bar');
            if (confirmBar) confirmBar.style.width = '100%';
            _startScaleAutoConfirm(() => {
                _scaleLastConfirmedGrams = grams;
                if (livVal) livVal.style.color = '';
                submitRecipeUse(false);
            }, 'btn-ruse-submit');
        });
    } else if (!_scaleUserDismissed && !_scaleStabilityTimer && !_scaleAutoConfirmTimer) {
        _cancelScaleTimersOnly();
        if (livLabel) livLabel.textContent = t('scale.weight_detected');
        _startScaleStabilityWait(() => {
            const inp = document.getElementById('ruse-quantity');
            if (inp) inp.value = val;
            if (livLabel) livLabel.textContent = t('scale.auto_confirm', { val, unit });
            if (livVal) livVal.style.color = '#22c55e';
            const confirmWrap3 = document.getElementById('ruse-scale-confirm-wrap');
            if (confirmWrap3) confirmWrap3.style.display = '';
            const confirmBar2 = document.getElementById('ruse-scale-confirm-bar');
            if (confirmBar2) confirmBar2.style.width = '100%';
            _startScaleAutoConfirm(() => {
                _scaleLastConfirmedGrams = grams;
                if (livVal) livVal.style.color = '';
                submitRecipeUse(false);
            }, 'btn-ruse-submit');
        });
    }
}

/** Cancel auto-confirm countdown on any screen press (touch = dismiss). */
function _cancelScaleAutoConfirmOnTouch() {
    _cancelScaleAutoConfirm(true);
}

/**
 * Cancel timers, animations and button styles — does NOT touch _scaleStabilityVal
 * or _scaleUserDismissed. Use this when weight goes unstable so the sentinel
 * is preserved and the same value can resume counting when stability returns.
 */
function _cancelScaleTimersOnly() {
    if (_scaleAutoConfirmTimer) { clearTimeout(_scaleAutoConfirmTimer); _scaleAutoConfirmTimer = null; }
    if (_scaleAutoConfirmRAF)   { cancelAnimationFrame(_scaleAutoConfirmRAF); _scaleAutoConfirmRAF = null; }
    _cancelScaleStabilityWait();
    const useBtn  = document.getElementById('btn-use-submit');
    const ruseBtn = document.getElementById('btn-ruse-submit');
    if (useBtn)  useBtn.style.background = '';
    if (ruseBtn) ruseBtn.style.background = '';
    // Reset modal confirm bar and live val colour
    const confirmBar = document.getElementById('ruse-scale-confirm-bar');
    const livVal     = document.getElementById('ruse-scale-live-val');
    const confirmWrap = document.getElementById('ruse-scale-confirm-wrap');
    if (confirmBar) { confirmBar.style.width = '100%'; }
    if (confirmWrap) confirmWrap.style.display = 'none';
    if (livVal) livVal.style.color = '';
    const livLabel = document.getElementById('ruse-scale-live-label');
    if (livLabel && livLabel.textContent.startsWith('✅')) {
        livLabel.textContent = t('scale.cancelled_replace');
    }
    document.removeEventListener('pointerdown', _cancelScaleAutoConfirmOnTouch, true);
}

/**
 * Full cancel: stops timers AND updates state flags.
 * @param {boolean} fromTouch  true = user tapped → set userDismissed
 *                             false = programmatic (page nav, closeModal, oninput) → reset sentinel
 */
function _cancelScaleAutoConfirm(fromTouch) {
    _cancelScaleTimersOnly();
    if (fromTouch) {
        _scaleUserDismissed = true;
    } else {
        _scaleStabilityVal = null;
        _scaleLastConfirmedGrams = null;
    }
}

/** Stop the stability wait and reset its progress bar(s). */
function _cancelScaleStabilityWait() {
    if (_scaleStabilityTimer) { clearTimeout(_scaleStabilityTimer); _scaleStabilityTimer = null; }
    if (_scaleStabilityRAF)   { cancelAnimationFrame(_scaleStabilityRAF); _scaleStabilityRAF = null; }
    const bar  = document.getElementById('scale-live-progress-bar');
    const bar2 = document.getElementById('ruse-scale-progress-bar');
    if (bar)  bar.style.width = '0%';
    if (bar2) bar2.style.width = '0%';
}

/**
 * Start a 10-second stability wait with an animated progress bar.
 * Updates both #scale-live-progress-bar (use page) and #ruse-scale-progress-bar (recipe modal).
 * Calls onStable() when weight unchanged for 5 s.
 */
function _startScaleStabilityWait(onStable) {
    _cancelScaleStabilityWait();
    const duration = 5000;
    const start = performance.now();
    const bar  = document.getElementById('scale-live-progress-bar');
    const bar2 = document.getElementById('ruse-scale-progress-bar');

    function tick() {
        const pct = Math.min(100, ((performance.now() - start) / duration) * 100);
        if (bar)  bar.style.width = pct + '%';
        if (bar2) bar2.style.width = pct + '%';
        if (pct < 100) { _scaleStabilityRAF = requestAnimationFrame(tick); }
    }
    _scaleStabilityRAF = requestAnimationFrame(tick);

    _scaleStabilityTimer = setTimeout(() => {
        _scaleStabilityTimer = null;
        if (_scaleStabilityRAF) { cancelAnimationFrame(_scaleStabilityRAF); _scaleStabilityRAF = null; }
        if (bar)  bar.style.width = '0%';
        if (bar2) bar2.style.width = '0%';
        onStable();
    }, duration);
}
function _startScaleAutoConfirm(onConfirm, btnId) {
    if (_scaleAutoConfirmTimer) { clearTimeout(_scaleAutoConfirmTimer); _scaleAutoConfirmTimer = null; }
    if (_scaleAutoConfirmRAF)   { cancelAnimationFrame(_scaleAutoConfirmRAF); _scaleAutoConfirmRAF = null; }
    const btn = btnId ? document.getElementById(btnId) : null;
    const baseBg = btn ? getComputedStyle(btn).backgroundColor : '';
    // Also update the modal countdown bar if present
    const ruseCountdownBar = document.getElementById('ruse-scale-confirm-bar');
    const duration = 5000;
    const start = performance.now();

    function tick() {
        const elapsed = performance.now() - start;
        const pct = Math.min(100, (elapsed / duration) * 100);
        // Reverse (countdown): button fill shrinks from right to left
        if (btn) {
            btn.style.background =
                `linear-gradient(to left, rgba(255,255,255,0.35) ${100 - pct}%, rgba(255,255,255,0) ${100 - pct}%), ${baseBg}`;
        }
        // Modal countdown progress bar shrinks
        if (ruseCountdownBar) ruseCountdownBar.style.width = (100 - pct) + '%';
        if (elapsed < duration) { _scaleAutoConfirmRAF = requestAnimationFrame(tick); }
    }
    _scaleAutoConfirmRAF = requestAnimationFrame(tick);

    _scaleAutoConfirmTimer = setTimeout(() => {
        _scaleAutoConfirmTimer = null;
        if (btn) btn.style.background = '';
        if (ruseCountdownBar) ruseCountdownBar.style.width = '0%';
        document.removeEventListener('pointerdown', _cancelScaleAutoConfirmOnTouch, true);
        onConfirm();
    }, duration);

    document.addEventListener('pointerdown', _cancelScaleAutoConfirmOnTouch, true);
}

/**
 * Update the scale status indicator icon/class.
 */
function _scaleUpdateStatus(state) {
    const el = document.getElementById('scale-status-indicator');
    if (!el) return;
    el.className = `header-btn scale-status-indicator scale-status-${state}`;
    const labels = {
        connected:    `⚖️ ${t('scale.status_connected')}${_scaleDevice ? ': ' + _scaleDevice : ''}`,
        searching:    `⚖️ ${t('scale.status_searching')}`,
        disconnected: `⚖️ ${t('scale.status_disconnected')}`,
        error:        `⚖️ ${t('scale.status_error')}`,
    };
    el.title = labels[state] || '';
}

/**
 * Show a brief toast with the current scale connection status when the icon is tapped.
 */
function _scaleShowInfo() {
    const state = _scaleConnected ? 'connected' : 'disconnected';
    const msgs = {
        connected:    `⚖️ ${t('scale.status_connected')}${_scaleDevice ? ': ' + _scaleDevice : ''}${_scaleBattery != null ? ' 🔋' + _scaleBattery + '%' : ''}`,
        searching:    `⚖️ ${t('scale.status_searching')}`,
        disconnected: `⚖️ ${t('scale.status_disconnected')}`,
        error:        `⚖️ ${t('scale.status_error')}`,
    };
    const el = document.getElementById('scale-status-indicator');
    const cls = el ? [...el.classList].find(c => c.startsWith('scale-status-') && c !== 'scale-status-indicator') : null;
    const key = cls ? cls.replace('scale-status-', '') : state;
    showToast(msgs[key] || msgs[state], key === 'connected' ? 'success' : 'info');
}

/**
 * Show the scale reading modal and wait for a stable weight, then populate the input.
 * @param {string} targetInputId  — ID of the <input> to fill
 * @param {Function} getUnit      — function that returns the current unit string ('g', 'ml', 'kg')
 */
function readScaleWeight(targetInputId, getUnit) {
    if (!_scaleConnected) {
        showToast('⚖️ ' + t('scale.not_connected'), 'error');
        return;
    }
    const unit = typeof getUnit === 'function' ? getUnit() : getUnit;
    _scaleShowReadingModal(targetInputId, unit);
    _scaleWeightCallback = (msg) => {
        let val = parseFloat(msg.value);
        const srcUnit = (msg.unit || 'kg').toLowerCase();
        // Convert to target unit
        if (srcUnit === 'kg' && unit === 'g')   val = Math.round(val * 1000);
        if (srcUnit === 'g'  && unit === 'kg')  val = +(val / 1000).toFixed(3);
        if (srcUnit === 'lbs'|| srcUnit === 'lb') {
            val = val * 453.592;
            if (unit === 'kg') val = +(val / 1000).toFixed(2); else val = Math.round(val);
        }
        if (srcUnit === 'kg' && unit === 'ml')  val = Math.round(val * 1000); // approximate (water density)
        const inp = document.getElementById(targetInputId);
        if (inp) { inp.value = val; inp.dispatchEvent(new Event('input')); }
        closeModal();
        showToast(`⚖️ ${val} ${unit}`, 'success');
    };
    // Weight data streams continuously via SSE; _scaleWeightCallback fires on the next stable reading
}

/**
 * Inline scale reading for the edit-inventory modal.
 * Shows a live weight display inside the form and fills edit-qty on stable reading.
 */
function readScaleForEdit() {
    if (!_scaleConnected) { showToast('⚖️ ' + t('scale.not_connected'), 'error'); return; }
    const section = document.getElementById('edit-scale-section');
    const btn = document.getElementById('btn-scale-edit');
    if (section) section.style.display = '';
    if (btn) btn.style.display = 'none';

    _scaleWeightCallback = (msg) => {
        const editQty = document.getElementById('edit-qty');
        const editUnit = document.getElementById('edit-unit');
        if (!editQty || !editUnit) return;

        let unit = editUnit.value;
        const isConf = unit === 'conf';
        let confSize = 0;
        if (isConf) confSize = parseFloat(document.getElementById('edit-conf-size')?.value) || 0;

        let raw = parseFloat(msg.value);
        const srcUnit = (msg.unit || 'kg').toLowerCase();
        let grams;
        if      (srcUnit === 'kg')                      grams = raw * 1000;
        else if (srcUnit === 'lbs' || srcUnit === 'lb') grams = raw * 453.592;
        else if (srcUnit === 'oz')                      grams = raw * 28.3495;
        else                                            grams = raw; // g or ml

        let val;
        if (isConf && confSize > 0) {
            val = Math.round((grams / confSize) * 100) / 100;
        } else {
            val = Math.round(grams);
        }

        editQty.value = val;
        editQty.dispatchEvent(new Event('input'));
        if (section) section.style.display = 'none';
        if (btn) btn.style.display = '';
        showToast(`⚖️ ${val} ${unit}`, 'success');
    };
}

function _scaleShowReadingModal(targetInputId, unit) {
    document.getElementById('modal-content').innerHTML = `
        <div class="modal-header">
            <h3>⚖️ ${t('scale.reading_title')}</h3>
            <button class="modal-close" onclick="closeModal(); _scaleWeightCallback = null;">✕</button>
        </div>
        <div style="padding:16px;text-align:center">
            <p style="margin-bottom:16px">${t('scale.place_on_scale')}</p>
            <div id="scale-reading-live" class="scale-reading-live">— — —</div>
            <p class="settings-hint" style="margin-top:12px">${t('scale.waiting_stable')}</p>
        </div>
    `;
    document.getElementById('modal-overlay').style.display = 'flex';
}

/**
 * Show/hide "⚖️ Leggi dalla bilancia" buttons based on current settings and unit.
 * Called after unit change or when navigating to the add/use form.
 */
function updateScaleReadButtons() {
    const s = getSettings();
    const ready = s.scale_enabled && s.scale_gateway_url;

    const btnAdd = document.getElementById('btn-scale-add');
    if (btnAdd) {
        const addUnit = document.getElementById('add-unit')?.value;
        btnAdd.style.display = (ready && (addUnit === 'g' || addUnit === 'ml')) ? '' : 'none';
    }
    const btnUse = document.getElementById('btn-scale-use');
    if (btnUse) {
        btnUse.style.display = (ready && (_useNormalUnit === 'g' || _useNormalUnit === 'ml')) ? '' : 'none';
    }
    // Live box: visible when scale enabled + connected + on use page + compatible unit
    const liveBox = document.getElementById('scale-live-box');
    if (liveBox) {
        const isWeightUnit = (_useNormalUnit === 'g' || _useNormalUnit === 'ml') ||
                             (_useConfMode && (_useConfMode.packageUnit === 'g' || _useConfMode.packageUnit === 'ml'));
        liveBox.style.display = (ready && _scaleConnected && _currentPageId === 'use' && isWeightUnit) ? '' : 'none';
    }
}

function onScaleEnabledChange() {
    const s = getSettings();
    const el = document.getElementById('setting-scale-enabled');
    s.scale_enabled = el ? el.checked : false;
    saveSettingsToStorage(s);
    scaleInit();
    updateScaleReadButtons();
}

function testScaleConnection() {
    const urlEl = document.getElementById('setting-scale-url');
    const statusEl = document.getElementById('scale-test-status');
    if (!urlEl || !statusEl) return;
    const url = urlEl.value.trim();
    if (!url) { showToast(t('scale.no_url'), 'error'); return; }

    statusEl.textContent = t('scale.testing');
    statusEl.className = 'settings-status';
    statusEl.style.display = 'block';

    const ac = new AbortController();
    const timeout = setTimeout(() => {
        ac.abort();
        statusEl.textContent = '❌ ' + t('scale.timeout');
        statusEl.className = 'settings-status error';
    }, 8000);
    fetch('api/scale_ping.php?url=' + encodeURIComponent(url), { signal: ac.signal })
        .then(r => r.json())
        .then(data => {
            clearTimeout(timeout);
            if (data.ok) {
                statusEl.textContent = '✅ ' + t('scale.connected_ok');
                statusEl.className = 'settings-status success';
            } else {
                statusEl.textContent = '❌ ' + (data.error || t('scale.error_connect'));
                statusEl.className = 'settings-status error';
            }
        })
        .catch(e => {
            clearTimeout(timeout);
            if (e.name !== 'AbortError') {
                statusEl.textContent = '❌ ' + t('scale.error_connect');
                statusEl.className = 'settings-status error';
            }
        });
}

async function discoverScaleGateway() {
    const btn    = document.getElementById('btn-scale-discover');
    const status = document.getElementById('scale-discover-status');
    if (!btn || !status) return;

    btn.disabled = true;
    btn.textContent = '⏳';
    status.style.display = 'block';
    status.textContent = '🔍 Scanning local network for scale gateway…';

    try {
        const res  = await fetch('api/scale_discover.php', { signal: AbortSignal.timeout(8000) });
        const data = await res.json();

        if (data.error) {
            status.textContent = '❌ ' + data.error;
        } else if (data.found && data.found.length > 0) {
            const url = data.found[0];
            const urlEl = document.getElementById('setting-scale-url');
            if (urlEl) urlEl.value = url;
            status.textContent = '✅ Gateway found: ' + url + (data.found.length > 1 ? ' (+' + (data.found.length - 1) + ' more)' : '');
            status.style.color = 'var(--color-success, #059669)';
            // Auto-save
            const s = getSettings();
            s.scale_gateway_url = url;
            saveSettingsToStorage(s);
            scaleInit();
        } else {
            status.textContent = '❌ No gateway found on ' + (data.subnet || 'local network') + '. Make sure the Android app is running and on the same Wi-Fi.';
        }
    } catch(e) {
        status.textContent = '❌ Discovery failed: ' + (e.message || 'timeout');
    }

    btn.disabled = false;
    btn.textContent = '🔍 Auto';
}

// ===== i18n TRANSLATION SYSTEM =====
let _i18nStrings = null;   // current language translations (flat)
let _i18nFallback = null;  // Italian fallback (flat)
let _currentLang = localStorage.getItem('evershelf_lang') || navigator.language?.slice(0, 2) || 'it';
const _SUPPORTED_LANGS = { it: 'Italiano', en: 'English', de: 'Deutsch' };
if (!_SUPPORTED_LANGS[_currentLang]) _currentLang = 'it';

// Flatten nested JSON: { a: { b: "x" } } → { "a.b": "x" }
function _flattenI18n(obj, prefix = '') {
    const result = {};
    for (const [k, v] of Object.entries(obj)) {
        const key = prefix ? `${prefix}.${k}` : k;
        if (v && typeof v === 'object' && !Array.isArray(v)) {
            Object.assign(result, _flattenI18n(v, key));
        } else {
            result[key] = v;
        }
    }
    return result;
}

// Translation function: t('toast.thrown_away', {name: 'Latte'})
function t(key, params) {
    let str = (_i18nStrings && _i18nStrings[key]) || (_i18nFallback && _i18nFallback[key]) || key;
    if (params) {
        for (const [k, v] of Object.entries(params)) {
            str = str.replace(new RegExp(`\\{${k}\\}`, 'g'), v);
        }
    }
    return str;
}

// Load translations from JSON files
async function loadTranslations(lang) {
    lang = lang || _currentLang;
    try {
        // Always load Italian as fallback
        if (!_i18nFallback) {
            const fbRes = await fetch(`translations/it.json?v=${Date.now()}`);
            if (fbRes.ok) _i18nFallback = _flattenI18n(await fbRes.json());
        }
        if (lang === 'it') {
            _i18nStrings = _i18nFallback;
        } else {
            const res = await fetch(`translations/${encodeURIComponent(lang)}.json?v=${Date.now()}`);
            if (res.ok) _i18nStrings = _flattenI18n(await res.json());
            else _i18nStrings = _i18nFallback;
        }
        _currentLang = lang;
        localStorage.setItem('evershelf_lang', lang);
        _applyI18nToLabels();
        translatePage();
    } catch (e) {
        console.warn('i18n: Failed to load translations for', lang, e);
        _i18nStrings = _i18nFallback;
    }
}

// Update LOCATIONS / SHOPPING_SECTIONS labels from translations
function _applyI18nToLabels() {
    if (!_i18nStrings) return;
    for (const key of Object.keys(LOCATIONS)) {
        const tKey = `locations.${key}`;
        if (_i18nStrings[tKey]) LOCATIONS[key].label = _i18nStrings[tKey];
    }
    for (const sec of SHOPPING_SECTIONS) {
        const tKey = `shopping_sections.${sec.key}`;
        if (_i18nStrings[tKey]) sec.label = _i18nStrings[tKey];
    }
}

// Translate all elements with data-i18n attributes
function translatePage() {
    document.querySelectorAll('[data-i18n]').forEach(el => {
        const key = el.getAttribute('data-i18n');
        if (key) el.textContent = t(key);
    });
    document.querySelectorAll('[data-i18n-html]').forEach(el => {
        const key = el.getAttribute('data-i18n-html');
        if (key) el.innerHTML = t(key);
    });
    document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
        const key = el.getAttribute('data-i18n-placeholder');
        if (key) el.placeholder = t(key);
    });
    document.querySelectorAll('[data-i18n-title]').forEach(el => {
        const key = el.getAttribute('data-i18n-title');
        if (key) el.title = t(key);
    });
    // Update HTML lang attribute
    document.documentElement.lang = _currentLang;
    // Populate language selector if present
    _populateLanguageSelector();
}

// Populate the language selector dropdown
function _populateLanguageSelector() {
    const sel = document.getElementById('setting-language');
    if (!sel) return;
    sel.innerHTML = '';
    for (const [code, name] of Object.entries(_SUPPORTED_LANGS)) {
        const opt = document.createElement('option');
        opt.value = code;
        opt.textContent = name;
        if (code === _currentLang) opt.selected = true;
        sel.appendChild(opt);
    }
}

// Change language and reload the page
function changeLanguage(lang) {
    if (lang === _currentLang) return;
    localStorage.setItem('evershelf_lang', lang);
    location.reload();
}

const LOCATIONS = {
    'dispensa': { icon: '🗄️', label: t('locations.dispensa') },
    'frigo': { icon: '🧊', label: t('locations.frigo') },
    'freezer': { icon: '❄️', label: t('locations.freezer') },
    'altro': { icon: '📦', label: t('locations.altro') },
};
const CATEGORY_ICONS = {
    'latticini': '🥛', 'carne': '🥩', 'pesce': '🐟', 'frutta': '🍎',
    'verdura': '🥬', 'pasta': '🍝', 'pane': '🍞', 'surgelati': '🧊',
    'bevande': '🥤', 'condimenti': '🧂', 'snack': '🍪', 'conserve': '🥫',
    'cereali': '🌾', 'igiene': '🧴', 'pulizia': '🧹', 'altro': '📦'
};

// Auto-detect location based on category and product name
const CATEGORY_LOCATION = {
    'latticini': 'frigo', 'carne': 'frigo', 'pesce': 'frigo',
    'frutta': 'frigo', 'verdura': 'frigo', 'surgelati': 'freezer',
    'pasta': 'dispensa', 'pane': 'dispensa', 'bevande': 'dispensa',
    'condimenti': 'dispensa', 'snack': 'dispensa', 'conserve': 'dispensa',
    'cereali': 'dispensa', 'igiene': 'altro', 'pulizia': 'altro', 'altro': 'dispensa'
};

// Shopping section (reparto) map — groups categories into grocery departments
const SHOPPING_SECTIONS = [
    { key: 'frutta_verdura', icon: '🥬', label: t('shopping_sections.frutta_verdura'), cats: new Set(['frutta','verdura']) },
    { key: 'carne_pesce',    icon: '🥩', label: t('shopping_sections.carne_pesce'),    cats: new Set(['carne','pesce']) },
    { key: 'latticini',      icon: '🥛', label: t('shopping_sections.latticini'),      cats: new Set(['latticini']) },
    { key: 'pane_dolci',     icon: '🍞', label: t('shopping_sections.pane_dolci'),     cats: new Set(['pane','snack','cereali']) },
    { key: 'pasta',          icon: '🍝', label: t('shopping_sections.pasta'),          cats: new Set(['pasta']) },
    { key: 'conserve',       icon: '🥫', label: t('shopping_sections.conserve'),       cats: new Set(['conserve','condimenti']) },
    { key: 'surgelati',      icon: '❄️',  label: t('shopping_sections.surgelati'),      cats: new Set(['surgelati']) },
    { key: 'bevande',        icon: '🥤', label: t('shopping_sections.bevande'),        cats: new Set(['bevande']) },
    { key: 'pulizia_igiene', icon: '🧴', label: t('shopping_sections.pulizia_igiene'), cats: new Set(['igiene','pulizia']) },
    { key: 'altro',          icon: '📦', label: t('shopping_sections.altro'),          cats: new Set(['altro']) },
];

function getItemSection(name) {
    const cat = guessCategoryFromName(name) || 'altro';
    for (const s of SHOPPING_SECTIONS) { if (s.cats.has(cat)) return s; }
    return SHOPPING_SECTIONS[SHOPPING_SECTIONS.length - 1];
}

const URGENCY_WEIGHT = { critical: 4, high: 3, medium: 2, low: 1 };
const URGENCY_BG = {
    critical: 'rgba(194,65,12,0.14)',
    high:     'rgba(234,88,12,0.09)',
    medium:   'rgba(245,158,11,0.07)',
    low:      'rgba(34,197,94,0.05)',
};

// Map Open Food Facts categories to local categories
function mapToLocalCategory(ofCategory, productName) {
    if (!ofCategory) {
        // No category tag — try to guess from product name
        return guessCategoryFromName(productName || '');
    }
    const cat = ofCategory.toLowerCase();
    // Direct match with our local keys
    for (const key of Object.keys(CATEGORY_ICONS)) {
        if (cat === key) return key;
    }
    
    // Handle specific Open Food Facts tags FIRST (before generic regex)
    // "plant-based-foods-and-beverages" is a catch-all — use product name to decide
    if (/plant-based-foods/.test(cat)) {
        return guessCategoryFromName(productName || '');
    }
    // "beverages-and-beverages-preparations" = actual beverages
    if (/^en:beverages/.test(cat)) return 'bevande';
    // sweeteners = condimenti
    if (/sweetener|dolcific/.test(cat)) return 'condimenti';
    
    // Specific tag patterns
    if (/dairy|lait|cheese|fromage|yoghurt|milk|latticin|latte/.test(cat)) return 'latticini';
    if (/meat|viande|carne|sausage|salum|prosciutt/.test(cat)) return 'carne';
    if (/fish|poisson|pesce|seafood|tuna|tonno|salmone/.test(cat)) return 'pesce';
    if (/fruit|frutta|juice|succo|apple|banana/.test(cat)) return 'frutta';
    if (/vegetable|verdur|legum|salad|insalat|tomato|pomodor/.test(cat)) return 'verdura';
    if (/pasta|rice|riso|noodle|spaghetti|penne|grain/.test(cat)) return 'pasta';
    if (/bread|pane|forno|biscott|toast|cracker|grissini|fette/.test(cat)) return 'pane';
    if (/frozen|surgelé|surgel|gelat/.test(cat)) return 'surgelati';
    if (/sauce|condiment|oil|olio|vinegar|aceto|mayo|ketchup|spice|salt|sugar|zuccher/.test(cat)) return 'condimenti';
    if (/snack|chip|crisp|chocolate|cioccolat|candy|biscuit|cookie|wafer|merendine|patatine/.test(cat)) return 'snack';
    if (/conserve|canned|can|pelati|passata|preserve|jam|marmellat|miele|honey/.test(cat)) return 'conserve';
    if (/cereal|muesli|granola|oat|fiocchi/.test(cat)) return 'cereali';
    if (/hygiene|soap|shampoo|igien|dentifricio|deodorant/.test(cat)) return 'igiene';
    if (/clean|detergent|pulizia|detersiv/.test(cat)) return 'pulizia';
    // Beverage check LAST (to avoid false matches on compound tags)
    if (/^(?!.*plant-based).*(beverage|drink|boisson|bevand|water|acqua|beer|birra|wine|vino|coffee|caffè|tea\b)/.test(cat)) return 'bevande';
    return 'altro';
}

// Guess a local category purely from product name
function guessCategoryFromName(name) {
    if (!name) return 'altro';
    const n = name.toLowerCase();
    // Pasta & Rice
    if (/spaghetti|penne|fusilli|rigatoni|linguine|orecchiette|farfalle|pasta\b|riso\b|basmati|carnaroli|arborio/.test(n)) return 'pasta';
    // Pane & Forno
    if (/pane\b|fette biscottate|grissini|cracker|toast|piadina|piadelle|focaccia|panini|sandwich|taralli/.test(n)) return 'pane';
    // Conserve
    if (/passata|pelati|pomodoro|sugo|polpa di pomod|marmellata|miele|legumi|ceci|fagioli|lenticchie|olive/.test(n)) return 'conserve';
    // Condimenti
    if (/olio\b|aceto|sale\b|pepe\b|zucchero|zuccher|farina|maionese|ketchup|senape|salsa/.test(n)) return 'condimenti';
    // Bevande
    if (/acqua|birra|vino|succo|spremuta|coca.cola|aranciata|caffè|tè\b|tea\b|latte\b/.test(n)) return 'bevande';
    // Latticini
    if (/latte\b|yogurt|formaggio|mozzarella|burro|panna|ricotta|mascarpone|gorgonzola|parmigiano|grana\b/.test(n)) return 'latticini';
    // Carne
    if (/pollo|manzo|maiale|vitello|tacchino|prosciutto|salame|bresaola|mortadella|wurstel|speck/.test(n)) return 'carne';
    // Pesce
    if (/tonno|salmone|merluzzo|pesce|sgombro|gamberi|acciughe/.test(n)) return 'pesce';
    // Frutta
    if (/mela|mele|banana|arancia|pera|fragola|uva|kiwi|limone|frutta/.test(n)) return 'frutta';
    // Verdura
    if (/insalata|zucchina|pomodor|cipolla|carota|spinaci|rucola|peperoni|melanzane|broccoli|patata/.test(n)) return 'verdura';
    // Surgelati
    if (/surgelat|frozen|findus|4.salti|gelato/.test(n)) return 'surgelati';
    // Snack
    if (/biscott|cioccolat|nutella|merendine|patatine|caramelle|wafer|sfornatini/.test(n)) return 'snack';
    // Cereali
    if (/cereali|muesli|fiocchi|granola|polenta/.test(n)) return 'cereali';
    // Igiene / Pulizia
    if (/sapone|shampoo|dentifricio|deodorante/.test(n)) return 'igiene';
    if (/detersivo|pulito|sgrassatore/.test(n)) return 'pulizia';
    return 'altro';
}

// ─────────────────────────────────────────────────────────────────────────────
// Embedding-based category classifier (async, @xenova/transformers)
// ─────────────────────────────────────────────────────────────────────────────

// Canonical descriptions for each local category (used as embedding anchors).
const _CATEGORY_DESCRIPTIONS = {
    latticini:  'latte yogurt formaggio burro panna mozzarella latticini dairy',
    carne:      'carne pollo manzo maiale vitello prosciutto salame bresaola meat',
    pesce:      'pesce tonno salmone merluzzo gamberi seafood fish',
    frutta:     'frutta mela banana arancia pera fragola uva kiwi fruit',
    verdura:    'verdura insalata zucchina carota cipolla spinaci tomato vegetables',
    pasta:      'pasta spaghetti penne fusilli riso risotto noodles rice',
    pane:       'pane fette biscottate grissini cracker toast bread bakery',
    surgelati:  'surgelati congelato frozen gelato ice cream',
    bevande:    'acqua birra vino succo caffè tè bevande drinks beverages',
    condimenti: 'olio aceto sale zucchero farina ketchup maionese senape spezie condiments',
    snack:      'biscotti cioccolato patatine snack caramelle wafer merendine',
    conserve:   'conserve pelati passata marmellata miele legumi ceci beans canned',
    cereali:    'cereali muesli granola fiocchi d\'avena oat breakfast cereal',
    igiene:     'sapone shampoo dentifricio deodorante igiene personale hygiene',
    pulizia:    'detersivo detergente pulizia casa sgrassatore cleaning',
    altro:      'prodotto generico varie altro miscellaneous',
};

// In-memory cache: productName → category (avoids re-embedding the same product)
const _embeddingCache = new Map();

/**
 * Cosine similarity between two Float32Array vectors.
 */
function _cosineSim(a, b) {
    let dot = 0, na = 0, nb = 0;
    for (let i = 0; i < a.length; i++) {
        dot += a[i] * b[i];
        na  += a[i] * a[i];
        nb  += b[i] * b[i];
    }
    return dot / (Math.sqrt(na) * Math.sqrt(nb) + 1e-9);
}

/**
 * Mean-pool a [1, tokens, dims] tensor → Float32Array of length dims.
 */
function _meanPool(tensor) {
    const [, tokens, dims] = tensor.dims;
    const data = tensor.data;
    const out  = new Float32Array(dims);
    for (let t = 0; t < tokens; t++) {
        for (let d = 0; d < dims; d++) {
            out[d] += data[t * dims + d];
        }
    }
    for (let d = 0; d < dims; d++) out[d] /= tokens;
    return out;
}

/**
 * Async: returns the best-matching category key for `productName`.
 * Returns null if the model is unavailable or similarity is too low.
 * THRESHOLD 0.30 — below this the regex fallback is more reliable.
 */
async function classifyCategoryByEmbedding(productName) {
    if (!productName) return null;
    const key = productName.toLowerCase().trim();
    if (_embeddingCache.has(key)) return _embeddingCache.get(key);

    if (typeof window._getCategoryPipeline !== 'function') return null;
    const pipe = await window._getCategoryPipeline();
    if (!pipe) return null;

    try {
        const labels = Object.keys(_CATEGORY_DESCRIPTIONS);
        const texts  = [key, ...labels.map(l => _CATEGORY_DESCRIPTIONS[l])];

        // Embed all texts in one batched call for efficiency
        const output  = await pipe(texts, { pooling: 'mean', normalize: true });
        const vectors = labels.map((_, i) => {
            const t = output[i + 1];
            // output[i] may be a Tensor or already a plain array-like
            return t.dims ? _meanPool(t) : new Float32Array(t.data ?? t);
        });
        const queryVec = output[0].dims
            ? _meanPool(output[0])
            : new Float32Array(output[0].data ?? output[0]);

        let bestLabel = null, bestSim = 0;
        for (let i = 0; i < labels.length; i++) {
            const sim = _cosineSim(queryVec, vectors[i]);
            if (sim > bestSim) { bestSim = sim; bestLabel = labels[i]; }
        }

        const result = (bestSim >= 0.30 && bestLabel !== 'altro') ? bestLabel : null;
        _embeddingCache.set(key, result);
        return result;
    } catch (e) {
        console.warn('[EverShelf] Embedding classify error:', e);
        return null;
    }
}

// Determine safety level for expired products
// Returns { level: 'danger'|'warning'|'ok', icon, label, tip }
function getExpiredSafety(item, daysExpired) {
    const cat = mapToLocalCategory(item.category || '', item.name || '');
    const loc = (item.location || '').toLowerCase();
    const inFreezer = loc === 'freezer';
    const inFrigo = loc === 'frigo';

    // === FREEZER: il congelamento allunga molto la vita ===
    // Carne/pesce in freezer: +3 mesi. Verdura/frutta: +6 mesi. Pane: +2 mesi.
    // Latticini in freezer: +1-2 mesi. Tutto il resto: +3-6 mesi.
    if (inFreezer) {
        const highRiskFreezer = ['carne', 'pesce'];
        const medRiskFreezer = ['latticini', 'pane'];
        const produceRiskFreezer = ['verdura', 'frutta'];

        let bonusDays;
        if (highRiskFreezer.includes(cat)) bonusDays = 90;       // +3 mesi
        else if (produceRiskFreezer.includes(cat)) bonusDays = 180; // +6 mesi
        else if (medRiskFreezer.includes(cat)) bonusDays = 60;    // +2 mesi
        else bonusDays = 120;                                      // +4 mesi default

        const effectiveDays = daysExpired - bonusDays;

        if (effectiveDays <= 0) {
            return { level: 'ok', icon: '✅', label: t('status.ok'), tip: t('status.tip_freezer_ok').replace('{n}', bonusDays - daysExpired) };
        }
        if (effectiveDays <= 30) {
            return { level: 'warning', icon: '👀', label: t('status.check'), tip: t('status.tip_freezer_check') };
        }
        return { level: 'danger', icon: '🗑️', label: t('status.discard'), tip: t('status.tip_freezer_danger') };
    }

    // === FRIGO e DISPENSA ===
    const highRisk = ['latticini', 'carne', 'pesce', 'verdura', 'frutta'];
    const medRisk = ['pane', 'surgelati'];

    if (highRisk.includes(cat)) {
        if (inFrigo && daysExpired <= 2) {
            return { level: 'warning', icon: '👀', label: t('status.check'), tip: t('status.tip_highRisk_check') };
        }
        return { level: 'danger', icon: '🗑️', label: t('status.discard'), tip: t('status.tip_highRisk_danger') };
    }

    if (medRisk.includes(cat)) {
        if (daysExpired <= 7) {
            return { level: 'warning', icon: '👀', label: t('status.check'), tip: t('status.tip_medRisk_check1') };
        }
        if (daysExpired <= 30) {
            return { level: 'warning', icon: '👀', label: t('status.check'), tip: t('status.tip_medRisk_check2') };
        }
        return { level: 'danger', icon: '🗑️', label: t('status.discard'), tip: t('status.tip_medRisk_danger') };
    }

    // LOW RISK - lunga conservazione (pasta, conserve, condimenti, cereali, snack)
    if (daysExpired <= 30) {
        return { level: 'ok', icon: '✅', label: t('status.ok'), tip: t('status.tip_lowRisk_ok') };
    }
    if (daysExpired <= 180) {
        return { level: 'warning', icon: '👀', label: t('status.check'), tip: t('status.tip_lowRisk_check') };
    }
    return { level: 'danger', icon: '🗑️', label: t('status.discard'), tip: t('status.tip_lowRisk_danger') };
}

// Localized labels for local categories
const CATEGORY_LABELS = {
    'latticini': `🥛 ${t('categories.latticini')}`, 'carne': `🥩 ${t('categories.carne')}`, 'pesce': `🐟 ${t('categories.pesce')}`,
    'frutta': `🍎 ${t('categories.frutta')}`, 'verdura': `🥬 ${t('categories.verdura')}`, 'pasta': `🍝 ${t('categories.pasta')}`,
    'pane': `🍞 ${t('categories.pane')}`, 'surgelati': `🧊 ${t('categories.surgelati')}`, 'bevande': `🥤 ${t('categories.bevande')}`,
    'condimenti': `🧂 ${t('categories.condimenti')}`, 'snack': `🍪 ${t('categories.snack')}`, 'conserve': `🥫 ${t('categories.conserve')}`,
    'cereali': `🌾 ${t('categories.cereali')}`, 'igiene': `🧴 ${t('categories.igiene')}`, 'pulizia': `🧹 ${t('categories.pulizia')}`,
    'altro': `📦 ${t('categories.altro')}`
};

// Detect best unit/quantity from Open Food Facts quantity_info string
// Returns the actual package weight/volume as default (e.g. 700g → unit:'g', quantity:700)
function detectUnitAndQuantity(quantityInfo) {
    if (!quantityInfo) return { unit: 'pz', quantity: 1, weightInfo: '' };
    const q = quantityInfo.toLowerCase().trim();
    // Match multi-pack patterns like "6 x 1l", "4 x 125g" → confezioni
    const multiMatch = q.match(/(\d+)\s*x\s*([\d.,]+)\s*(ml|l|g|kg|cl)/i);
    if (multiMatch) {
        const count = parseInt(multiMatch[1]);
        let perUnitVal = parseFloat(multiMatch[2].replace(',', '.'));
        let perUnitUnit = multiMatch[3].toLowerCase();
        if (perUnitUnit === 'cl') { perUnitUnit = 'ml'; perUnitVal *= 10; }
        if (perUnitUnit === 'kg') { perUnitUnit = 'g'; perUnitVal *= 1000; }
        if (perUnitUnit === 'l') { perUnitUnit = 'ml'; perUnitVal *= 1000; }
        return { unit: 'conf', quantity: perUnitVal, packageUnit: perUnitUnit, confCount: count, weightInfo: quantityInfo };
    }
    // Match single package patterns like "500 g", "1 l", "750 ml", "1.5 kg"
    const match = q.match(/([\d.,]+)\s*(kg|g|l|ml|cl)/i);
    if (match) {
        let unit = match[2].toLowerCase();
        let val = parseFloat(match[1].replace(',', '.'));
        if (unit === 'cl') { unit = 'ml'; val *= 10; }
        if (unit === 'kg') { unit = 'g'; val *= 1000; }
        if (unit === 'l') { unit = 'ml'; val *= 1000; }
        return { unit, quantity: val, weightInfo: quantityInfo };
    }
    return { unit: 'pz', quantity: 1, weightInfo: quantityInfo };
}

// Estimate expiry days based on category/product type
const EXPIRY_DAYS = {
    'latticini': 7, 'carne': 4, 'pesce': 3, 'frutta': 7, 'verdura': 7,
    'pasta': 730, 'pane': 4, 'surgelati': 180, 'bevande': 365, 'condimenti': 365,
    'snack': 180, 'conserve': 730, 'cereali': 365, 'igiene': 1095, 'pulizia': 1095, 'altro': 180
};

// More specific expiry by product name keywords
function estimateExpiryDays(product, location) {
    const name = (product.name || '').toLowerCase();
    const cat = (product.category || '').toLowerCase();
    const loc = (location || '').toLowerCase();
    
    let days;
    
    // Specific product overrides
    if (/latte\s+(fresco|intero|parzial|scremato)/.test(name)) days = 7;
    else if (/latte\s+uht|latte\s+a\s+lunga/.test(name)) days = 90;
    else if (/yogurt/.test(name)) days = 21;
    else if (/mozzarella|burrata|stracciatella/.test(name)) days = 5;
    else if (/formaggio\s+(fresco|ricotta|mascarpone|stracchino|crescenza)/.test(name)) days = 10;
    else if (/parmigiano|grana|pecorino|provolone/.test(name)) days = 60;
    else if (/burro/.test(name)) days = 60;
    else if (/panna/.test(name)) days = 14;
    else if (/prosciutto\s+cotto|mortadella|wurstel/.test(name)) days = 7;
    else if (/prosciutto\s+crudo|salame|bresaola|speck/.test(name)) days = 30;
    else if (/nduja/.test(name)) days = 90;
    else if (/uova/.test(name)) days = 28;
    else if (/pane\s+fresco|pane\s+in\s+cassetta/.test(name)) days = 5;
    else if (/pane\s+confezionato|pan\s+carr|pancarrè/.test(name)) days = 14;
    else if (/insalata|rucola|spinaci\s+freschi/.test(name)) days = 5;
    else if (/pollo|tacchino|maiale|manzo|vitello|sovracosci|cosci/.test(name)) days = 3;
    else if (/salmone|tonno\s+fresco|pesce/.test(name) && !/tonno\s+in\s+scatola|tonno\s+rio/.test(name)) days = 2;
    else if (/tonno\s+in\s+scatola|tonno\s+rio|sgombro\s+in/.test(name)) days = 1095;
    else if (/surgelat|frozen|findus|4\s*salti/.test(name)) days = 180;
    else if (/gelato/.test(name)) days = 365;
    else if (/succo|spremuta/.test(name)) days = 7;
    else if (/birra|vino/.test(name)) days = 365;
    else if (/acqua/.test(name)) days = 365;
    else if (/mela|mele\b/.test(name)) days = 7;
    else if (/arancia|arance|mandarini|agrumi/.test(name)) days = 7;
    else if (/banana|banane/.test(name)) days = 5;
    else if (/pera|pere\b|fragola|fragole|uva|kiwi/.test(name)) days = 5;
    else if (/carota|carote|zucchina|zucchine|peperoni|melanzane/.test(name)) days = 7;
    else if (/broccoli|cavolfiore|cavolo|spinaci|bietola/.test(name)) days = 5;
    else if (/cipolla|cipolle/.test(name)) days = 10;
    else if (/patata|patate/.test(name)) days = 14;
    else if (/biscott|cracker|grissini|fette\s+biscott/.test(name)) days = 180;
    else if (/nutella|marmellata|miele/.test(name)) days = 365;
    else if (/passata|pelati|pomodor/.test(name)) days = 730;
    else if (/olio|aceto/.test(name)) days = 548;
    else {
        // Fallback to category
        days = 180; // generic default
        for (const [key, d] of Object.entries(EXPIRY_DAYS)) {
            if (cat.includes(key)) { days = d; break; }
        }
    }
    
    // Fridge extends shelf life for produce and short-lived items (sealed only)
    if (loc === 'frigo') {
        // Specific fridge-friendly produce overrides
        if (/mela|mele/.test(name)) days = Math.max(days, 28);
        else if (/arancia|arance|agrumi|mandarini|limone|limoni/.test(name)) days = Math.max(days, 21);
        else if (/carota|carote/.test(name)) days = Math.max(days, 21);
        else if (/cipolla/.test(name)) days = Math.max(days, 14);
        else if (/patata|patate/.test(name)) days = Math.max(days, 21);
        else if (/pera|pere/.test(name)) days = Math.max(days, 21);
        else if (/kiwi/.test(name)) days = Math.max(days, 28);
        else if (/uva/.test(name)) days = Math.max(days, 14);
        else if (/fragola|fragole/.test(name)) days = Math.max(days, 7);
        else if (/peperoni/.test(name)) days = Math.max(days, 14);
        else if (/zucchina|zucchine/.test(name)) days = Math.max(days, 14);
        else if (/melanzane/.test(name)) days = Math.max(days, 14);
        else if (/broccoli|cavolfiore|cavolo/.test(name)) days = Math.max(days, 10);
        // General fridge bonus: fruits and vegs that aren't already long
        else if (days <= 7 && (/frutta|fruit/.test(cat) || /verdur|vegetable|plant-based/.test(cat))) {
            days = Math.round(days * 2); // ~double shelf life in fridge
        }
    }
    
    // Freezer extends shelf life significantly
    if (loc === 'freezer' && days < 180) {
        // Fresh meat/fish: 3-6 months in freezer
        if (days <= 4) days = 120;
        // Short-lived (cheese, dairy, bread): 2-3 months
        else if (days <= 14) days = 75;
        // Medium (yogurt, cured meats): 3-4 months
        else if (days <= 30) days = 120;
        // Already long-lasting: at least 6 months
        else days = Math.max(days, 180);
    }
    
    return days;
}

function formatEstimatedExpiry(days) {
    if (days <= 7) return t('expiry.days_approx').replace('{n}', days);
    if (days <= 30) return t('expiry.weeks_approx').replace('{n}', Math.round(days / 7));
    if (days <= 365) return t('expiry.months_approx').replace('{n}', Math.round(days / 30));
    return t('expiry.years_approx').replace('{n}', Math.round(days / 365));
}

/**
 * Estimate shelf life in days for an OPENED product.
 * Much shorter than sealed shelf life — based on typical "once opened, consume within X days".
 */
function estimateOpenedExpiryDays(product, location) {
    const name = (product.name || '').toLowerCase();
    const cat = (product.category || '').toLowerCase();
    const loc = (location || '').toLowerCase();

    // ── A: Non-perishables — check BEFORE location ──────────────────────
    if (/\bsale\b|\bsel\s+mar|\bsalt\b/.test(name) && !/\b(salmone|salame|salsa)\b/.test(name)) return 9999;
    if (/\bzucchero\b|\bsugar\b/.test(name)) return 9999;
    if (/\bmiele\b/.test(name)) return 9999;
    if (/\baceto\b/.test(name)) return 9999;
    if (/\bbicarbonato\b|\blievito\s+chimico\b/.test(name)) return 9999;

    // ── B: Spirits ───────────────────────────────────────────────────────
    if (/\b(sambuca|rum\b|brandy|whiskey|whisky|vodka|gin\b|grappa|amaro|aperol|campari|limoncello|cognac|porto|marsala|baileys|amaretto|vermouth)\b/.test(name)) return 730;

    // ── C: Long-life regardless of location ─────────────────────────────
    if (/\b(aroma|estratto|essenza|vanilli|colorante)\b/.test(name)) return 730;
    if (/\b(t[eè]\b|tea\b|tisana|camomilla|verbena|infuso|rooibos)\b/.test(name)) return 730;
    if (/\b(caff[eè]|coffee|nespresso)\b/.test(name)) return 365;
    if (/\bolio\b/.test(name)) return 365;
    if (/salsa\s+di\s+soia|soy\s*sauce/.test(name)) return 90; // soy sauce fine opened anywhere
    if (loc !== 'frigo') {
        if (/\b(pasta|spaghetti|penne|rigatoni|fusilli|farfalle|tagliatelle|linguine|bucatini|lasagn|tortiglioni)\b/.test(name)) return 365;
        if (/\b(riso|risotto|orzo|farro|quinoa|couscous)\b/.test(name) && !/\b(pronto|cotto)\b/.test(name)) return 365;
        if (/\b(polenta|semola|maizena|amido|farina)\b/.test(name)) return 180;
    }

    // ── D: Freezer ───────────────────────────────────────────────────────
    if (loc === 'freezer') return 90;

    // ── E: Pantry fallbacks ───────────────────────────────────────────────
    if (loc !== 'frigo') {
        if (/\b(biscott[io]|cookies|wafer|tarall[io]|crackers?)\b/.test(name)) return 60;
        if (/\b(muesli|cereali|corn\s*flakes|granola|fiocchi)\b/.test(name)) return 60;
        if (/\b(confettura|marmellata)\b/.test(name)) return 90;
        if (/\b(nutella|cioccolat)\b/.test(name)) return 90;
        if (/\bpane\b/.test(name)) return 4;
        return 60;
    }

    if (/latte\s+(fresco|intero|parzial|scremato)/.test(name)) return 3;
    if (/latte\s+(uht|a\s+lunga)/.test(name)) return 7;
    // Long-life mountain/brand milks stored in pantry before use (UHT)
    if (/latte.*(montagna|alta\s+qual|parmalat|granarolo|esselunga|conservaz|microfiltrat)/i.test(name)) return 7;
    if (/\blatte\b/.test(name)) return 4;
    if (/\byogurt\b/.test(name)) return 5;
    if (/mozzarella|burrata|stracciatella/.test(name)) return 3;
    if (/philadelphia|spalmabile/.test(name)) return 7;
    if (/formaggio.*(fresco|ricotta|mascarpone|stracchino|crescenza)/.test(name)) return 5;
    if (/parmigiano|grana|pecorino|provolone/.test(name)) return 21;
    if (/formaggio/.test(name)) return 10;
    if (/\bburro\b/.test(name)) return 30;
    if (/\bpanna\b/.test(name)) return 4;
    if (/prosciutto\s+cotto|mortadella|wurstel/.test(name)) return 5;
    if (/prosciutto\s+crudo|salame|bresaola|speck|pancetta|nduja/.test(name)) return 7;
    if (/\b(pollo|tacchino|maiale|manzo|vitello|agnello)\b/.test(name)) return 2;
    if (/salmone|tonno\s+fresco|pesce(?!\s+in)/.test(name)) return 2;
    if (/\b(passata|pelati|polpa|sugo|salsa\s+di\s+pomodoro)\b/.test(name)) return 5;
    if (/insalata|rucola|spinaci|lattuga|crescione|germogli/.test(name)) return 2;
    if (/\b(succo|spremuta)\b/.test(name)) return 3;
    if (/\b(birra|beer)\b/.test(name)) return 3;
    if (/\bvino\b/.test(name)) return 5;
    if (/tonno\s+in\s+scatola|tonno\s+rio|sgombro\s+in/.test(name)) return 4;
    // Fruit opened/cut in fridge
    if (/\bavocado\b/.test(name)) return 2;
    if (/\b(banana|banane|fragola|lampone|pesca|albicocca|ciliegia|mango|papaya)\b/.test(name)) return 2;
    if (/\b(mela|pera|nettarina|prugna|kiwi|ananas|uva|melone|anguria)\b/.test(name)) return 3;
    if (/\b(arancia|mandarino|pompelmo|clementina|limone)\b/.test(name)) return 3;
    // Vegetables opened/cut in fridge
    if (/\b(zucchina|zucchine|melanzana|pomodor)\b/.test(name)) return 3;
    if (/\b(peperone|peperoni)\b/.test(name)) return 3;
    if (/\b(broccolo|broccoli|cavolfiore|cavolo)\b/.test(name)) return 3;
    if (/\bsedano\b|\bfinocchio\b/.test(name)) return 3;
    if (/\b(cipolla|cipolle|cipollotto|scalogno|porro)\b/.test(name)) return 4;
    if (/\b(carota|carote)\b/.test(name)) return 5;
    if (/\b(patata|patate|tubero)\b/.test(name)) return 3;
    if (/\baglio\b/.test(name)) return 10;

    // ── G: Fridge condiments ─────────────────────────────────────────────
    if (/maionese|mayo|mayon/.test(name)) return 90;
    if (/\bketchup\b/.test(name)) return 90;
    if (/\b(senape|mustard)\b/.test(name)) return 90;
    if (/salsa\s+di\s+soia|soy\s*sauce/.test(name)) return 90;
    if (/\b(tabasco|worcestershire|sriracha)\b/.test(name)) return 180;
    if (/confettura|marmellata/.test(name)) return 60;
    if (/nutella|cioccolat/.test(name)) return 60;

    // ── H: Category fallbacks ────────────────────────────────────────────
    if (/dairy|latticin/.test(cat)) return 5;
    if (/meat|carne/.test(cat)) return 3;
    if (/fish|pesce/.test(cat)) return 2;
    if (/fruit|frutta/.test(cat)) return 7;
    if (/verdur|vegetable/.test(cat)) return 5;
    if (/conserve/.test(cat)) return 7;
    if (/condimenti|sauce/.test(cat)) return 30;
    if (/bevand|beverage/.test(cat)) return 5;

    return 5; // safe default for fridge
}

function addDays(days) {
    const d = new Date();
    d.setDate(d.getDate() + days);
    return d.toISOString().split('T')[0];
}

// Guess location from product name keywords (fallback if no category)
function guessLocationFromName(name) {
    const n = (name || '').toLowerCase();
    // Frigo keywords
    if (/latte|yogurt|formaggio|mozzarella|burro|panna|uova|prosciutto|salame|wurstel|ricotta|mascarpone|gorgonzola|insalata|rucola|spinaci|pollo|manzo|maiale|salmone|tonno fresco|bresaola/.test(n)) return 'frigo';
    // Freezer keywords
    if (/surgel|frozen|gelato|ghiaccioli|bastoncini|findus|4 salti|pizza surgel|verdure surgel|minestrone surg/.test(n)) return 'freezer';
    // Dispensa keywords
    if (/pasta|riso|farina|zucchero|sale|olio|aceto|biscott|cracker|grissini|caffè|tè|the |tea |tonno|pelati|passata|legumi|ceci|fagioli|lenticchie|cereali|muesli|marmell|nutella|miele|cioccolat/.test(n)) return 'dispensa';
    return null; // unknown
}

function guessLocation(product) {
    // 1. Category-based
    if (product.category) {
        const cat = product.category.toLowerCase().replace(/^en:/, '').split(',')[0].trim();
        // Check our map
        for (const [key, loc] of Object.entries(CATEGORY_LOCATION)) {
            if (cat.includes(key)) return loc;
        }
        // Open Food Facts categories
        if (/dairy|lait|cheese|fromage|yoghurt|milk|latticin/i.test(cat)) return 'frigo';
        if (/meat|viande|carne|fish|poisson|pesce/i.test(cat)) return 'frigo';
        if (/frozen|surgelé|surgel/i.test(cat)) return 'freezer';
        if (/fruit|vegetable|verdur|frutta/i.test(cat)) return 'frigo';
        if (/beverage|drink|boisson|bevand/i.test(cat)) return 'dispensa';
        if (/pasta|cereal|grain|bread|biscuit|snack|sauce|condiment|conserv|can/i.test(cat)) return 'dispensa';
    }
    // 2. Name-based fallback
    const nameLoc = guessLocationFromName(product.name);
    if (nameLoc) return nameLoc;
    // 3. Default
    return 'dispensa';
}

// ===== STATE =====
let currentProduct = null;
let currentInventory = [];
let _actionInventoryItems = [];
let currentLocation = '';
let scannerStream = null;
let quaggaRunning = false;
let aiStream = null;
let _scanZoomLevel = 1; // 1 or 2

async function toggleScanZoom() {
    _scanZoomLevel = _scanZoomLevel === 1 ? 2 : 1;
    const btn = document.getElementById('scan-zoom-btn');
    if (btn) btn.textContent = `x${_scanZoomLevel}`;
    if (scannerStream) {
        const track = scannerStream.getVideoTracks()[0];
        if (track) {
            const caps = track.getCapabilities ? track.getCapabilities() : {};
            if (caps.zoom) {
                // Hardware zoom (Android Chrome)
                const z = _scanZoomLevel === 2
                    ? Math.min(caps.zoom.max, caps.zoom.min * 2 || 2)
                    : caps.zoom.min;
                try { await track.applyConstraints({ advanced: [{ zoom: z }] }); } catch(e) {}
            } else {
                // Software zoom via CSS scale on the video element
                const video = document.getElementById('scanner-video');
                if (video) video.style.transform = _scanZoomLevel === 2 ? 'scale(2)' : 'scale(1)';
            }
        }
    }
}

// ===== CAMERA HELPER =====
function getCameraConstraints(extraVideo = {}) {
    const s = getSettings();
    const mode = s.camera_facing || 'environment';
    // Front cameras on older devices often have lower resolution — don't over-request
    const isFront = (mode === 'user');
    const videoConstraints = {
        width: { ideal: isFront ? 640 : 1280 },
        height: { ideal: isFront ? 480 : 720 },
        ...extraVideo
    };
    if (mode === 'environment' || mode === 'user') {
        videoConstraints.facingMode = mode;
    } else {
        // Specific deviceId selected
        videoConstraints.deviceId = { exact: mode };
    }
    return { video: videoConstraints };
}

function isFrontCamera() {
    const s = getSettings();
    return (s.camera_facing || 'environment') === 'user';
}

async function enumerateCameras() {
    try {
        // Need a temporary stream to get device labels
        const tempStream = await navigator.mediaDevices.getUserMedia({ video: true });
        const devices = await navigator.mediaDevices.enumerateDevices();
        tempStream.getTracks().forEach(t => t.stop());
        return devices.filter(d => d.kind === 'videoinput');
    } catch(e) {
        return [];
    }
}

// ===== SETTINGS / CONFIG =====
let _settingsCache = null;
let _settingsDirty = false;

function getSettings() {
    if (!_settingsCache) {
        try {
            _settingsCache = JSON.parse(localStorage.getItem('evershelf_settings') || '{}');
        } catch(e) { _settingsCache = {}; }
    }
    const s = _settingsCache;
    // Build recipe_prefs array from individual booleans
    s.recipe_prefs = [];
    if (s.pref_veloce) s.recipe_prefs.push('veloce');
    if (s.pref_pocafame) s.recipe_prefs.push('pocafame');
    if (s.pref_scadenze) s.recipe_prefs.push('scadenze');
    if (s.pref_healthy) s.recipe_prefs.push('salutare');
    if (s.pref_opened) s.recipe_prefs.push('opened');
    if (s.pref_zerowaste) s.recipe_prefs.push('zerowaste');
    s.dietary_restrictions = s.dietary || '';
    return s;
}

function saveSettingsToStorage(settings) {
    _settingsCache = settings;
    localStorage.setItem('evershelf_settings', JSON.stringify(settings));
    // Persist to DB
    _settingsDirty = true;
    _debouncedSyncSettings();
}

const _debouncedSyncSettings = debounce(function() {
    if (!_settingsDirty) return;
    _settingsDirty = false;
    const s = getSettings();
    // Don't sync secrets or device-specific settings to shared DB
    const shared = {
        default_persons: s.default_persons,
        pref_veloce: s.pref_veloce,
        pref_pocafame: s.pref_pocafame,
        pref_scadenze: s.pref_scadenze,
        pref_healthy: s.pref_healthy,
        pref_opened: s.pref_opened,
        pref_zerowaste: s.pref_zerowaste,
        dietary: s.dietary,
        appliances: s.appliances,
    };
    api('app_settings_save', {}, 'POST', { settings: { user_prefs: shared } }).catch(() => {});
}, 1000);

function debounce(fn, ms) {
    let t; return function(...args) { clearTimeout(t); t = setTimeout(() => fn(...args), ms); };
}

async function syncSettingsFromDB() {
    try {
        // Primary: load from server .env
        const serverSettings = await api('get_settings');
        _geminiAvailable = !!(serverSettings.gemini_key_set);
        _demoMode = !!serverSettings.demo_mode;
        _updateGeminiButtonState();
        _applyDemoModeUI();
        const s = getSettings();
        const serverKeys = ['default_persons','pref_veloce','pref_pocafame','pref_scadenze',
            'pref_healthy','pref_opened','pref_zerowaste','dietary','appliances',
            'camera_facing','scale_enabled','scale_gateway_url',
            'meal_plan_enabled','tts_enabled','tts_url','tts_token',
            'tts_method','tts_auth_type','tts_content_type','tts_payload_key',
            'screensaver_enabled'];
        for (const key of serverKeys) {
            if (serverSettings[key] !== undefined && serverSettings[key] !== null && serverSettings[key] !== '') {
                s[key] = serverSettings[key];
            }
        }
        _settingsCache = s;
        localStorage.setItem('evershelf_settings', JSON.stringify(s));
        // Also load review_confirmed from DB
        const res = await api('app_settings_get');
        if (res.success && res.settings) {
            if (res.settings.review_confirmed) {
                _reviewConfirmedCache = res.settings.review_confirmed;
            }
        }
    } catch(e) { /* offline, use local */ }
}

async function loadSettingsUI() {
    const s = getSettings();
    document.getElementById('setting-gemini-key').value = s.gemini_key || '';
    document.getElementById('setting-bring-email').value = s.bring_email || '';
    document.getElementById('setting-bring-password').value = s.bring_password || '';
    document.getElementById('setting-default-persons').value = s.default_persons || 1;
    document.getElementById('setting-pref-veloce').checked = !!s.pref_veloce;
    document.getElementById('setting-pref-pocafame').checked = !!s.pref_pocafame;
    document.getElementById('setting-pref-scadenze').checked = !!s.pref_scadenze;
    document.getElementById('setting-pref-healthy').checked = !!s.pref_healthy;
    document.getElementById('setting-pref-opened').checked = !!s.pref_opened;
    document.getElementById('setting-pref-zerowaste').checked = !!s.pref_zerowaste;
    const ssEl = document.getElementById('setting-screensaver-enabled');
    if (ssEl) ssEl.checked = s.screensaver_enabled === true;
    document.getElementById('setting-dietary').value = s.dietary || '';
    // Camera
    const cameraSelect = document.getElementById('setting-camera-facing');
    if (cameraSelect) cameraSelect.value = s.camera_facing || 'environment';
    loadCameraDevices();
    renderAppliances(s.appliances || []);
    const mealPlanEnabled = s.meal_plan_enabled !== false;
    const mpEnabledEl = document.getElementById('setting-meal-plan-enabled');
    if (mpEnabledEl) mpEnabledEl.checked = mealPlanEnabled;
    const mpConfigSection = document.getElementById('meal-plan-config-section');
    if (mpConfigSection) mpConfigSection.style.display = mealPlanEnabled ? '' : 'none';
    const mpLegendCard = document.getElementById('meal-plan-legend-card');
    if (mpLegendCard) mpLegendCard.style.display = mealPlanEnabled ? '' : 'none';
    renderMealPlanEditor();
    // Render legend
    const legend = document.querySelector('.mplan-legend');
    if (legend) {
        legend.innerHTML = getMealPlanTypes().map(mpt =>
            `<span class="mplan-badge" style="opacity:0.85">${mpt.icon} ${mpt.label}</span>`
        ).join('');
    }
    // TTS settings — init defaults on first load
    if (!s._tts_initialized) {
        s.tts_url = s.tts_url || '';
        s.tts_token = s.tts_token || '';
        s.tts_payload_key = s.tts_payload_key || 'message';
        s.tts_method = s.tts_method || 'POST';
        s.tts_auth_type = s.tts_auth_type || 'bearer';
        s.tts_content_type = s.tts_content_type || 'application/json';
        s.tts_enabled = s.tts_enabled !== undefined ? s.tts_enabled : false;
        // Default engine: 'server' if a URL was already configured, else 'browser'
        if (!s.tts_engine) s.tts_engine = s.tts_url ? 'server' : 'browser';
        s.tts_voice = s.tts_voice || '';
        s.tts_rate = s.tts_rate !== undefined ? s.tts_rate : 1;
        s.tts_pitch = s.tts_pitch !== undefined ? s.tts_pitch : 1;
        s._tts_initialized = true;
        saveSettingsToStorage(s);
    }
    const ttsEnabledEl = document.getElementById('setting-tts-enabled');
    if (ttsEnabledEl) ttsEnabledEl.checked = s.tts_enabled === true;
    const ttsEngineEl = document.getElementById('setting-tts-engine');
    if (ttsEngineEl) { ttsEngineEl.value = s.tts_engine || 'browser'; onTtsEngineChange(ttsEngineEl.value); }
    const ttsRateEl = document.getElementById('setting-tts-rate');
    if (ttsRateEl) { ttsRateEl.value = s.tts_rate || 1; document.getElementById('tts-rate-label').textContent = parseFloat(s.tts_rate || 1).toFixed(1); }
    const ttsPitchEl = document.getElementById('setting-tts-pitch');
    if (ttsPitchEl) { ttsPitchEl.value = s.tts_pitch || 1; document.getElementById('tts-pitch-label').textContent = parseFloat(s.tts_pitch || 1).toFixed(1); }
    _initBrowserTtsVoices(s.tts_voice || '');
    const ttsUrlEl = document.getElementById('setting-tts-url');
    if (ttsUrlEl) ttsUrlEl.value = s.tts_url || '';
    const ttsMethEl = document.getElementById('setting-tts-method');
    if (ttsMethEl) ttsMethEl.value = s.tts_method || 'POST';
    const ttsAuthTypeEl = document.getElementById('setting-tts-auth-type');
    if (ttsAuthTypeEl) { ttsAuthTypeEl.value = s.tts_auth_type || 'bearer'; onTtsAuthTypeChange(ttsAuthTypeEl.value); }
    const ttsTokenEl = document.getElementById('setting-tts-token');
    if (ttsTokenEl) ttsTokenEl.value = s.tts_token || '';
    const ttsAuthHdrNameEl = document.getElementById('setting-tts-auth-header-name');
    if (ttsAuthHdrNameEl) ttsAuthHdrNameEl.value = s.tts_auth_header_name || '';
    const ttsAuthHdrValEl = document.getElementById('setting-tts-auth-header-value');
    if (ttsAuthHdrValEl) ttsAuthHdrValEl.value = s.tts_auth_header_value || '';
    const ttsCtEl = document.getElementById('setting-tts-content-type');
    if (ttsCtEl) ttsCtEl.value = s.tts_content_type || 'application/json';
    const ttsPayloadKeyEl = document.getElementById('setting-tts-payload-key');
    if (ttsPayloadKeyEl) ttsPayloadKeyEl.value = s.tts_payload_key || 'message';
    const ttsExtraEl = document.getElementById('setting-tts-extra-fields');
    if (ttsExtraEl) ttsExtraEl.value = s.tts_extra_fields || '';
    
    // Load server-side settings as primary source
    try {
        const serverSettings = await api('get_settings');
        // Merge all server settings into local cache (server wins)
        const serverKeys = ['bring_email',
            'default_persons','pref_veloce','pref_pocafame','pref_scadenze',
            'pref_healthy','pref_opened','pref_zerowaste','dietary','appliances',
            'camera_facing','scale_enabled','scale_gateway_url',
            'meal_plan_enabled',
            'tts_enabled','tts_url','tts_token','tts_method','tts_auth_type',
            'tts_content_type','tts_payload_key'];
        // Note: gemini_key is never sent from server; settings_token_set is metadata only
        const settingsTokenRequired = !!serverSettings.settings_token_set;
        const tokenHintEl = document.getElementById('settings-token-status-hint');
        if (tokenHintEl) tokenHintEl.style.display = settingsTokenRequired ? 'block' : 'none';
        let changed = false;
        for (const key of serverKeys) {
            if (serverSettings[key] !== undefined && serverSettings[key] !== null && serverSettings[key] !== '') {
                s[key] = serverSettings[key];
                changed = true;
            }
        }
        if (changed) {
            _settingsCache = s;
            localStorage.setItem('evershelf_settings', JSON.stringify(s));
            // Re-populate UI with merged values
            document.getElementById('setting-gemini-key').value = s.gemini_key || '';
            document.getElementById('setting-bring-email').value = s.bring_email || '';
            document.getElementById('setting-bring-password').value = s.bring_password || '';
            document.getElementById('setting-default-persons').value = s.default_persons || 1;
            document.getElementById('setting-pref-veloce').checked = !!s.pref_veloce;
            document.getElementById('setting-pref-pocafame').checked = !!s.pref_pocafame;
            document.getElementById('setting-pref-scadenze').checked = !!s.pref_scadenze;
            document.getElementById('setting-pref-healthy').checked = !!s.pref_healthy;
            document.getElementById('setting-pref-opened').checked = !!s.pref_opened;
            document.getElementById('setting-pref-zerowaste').checked = !!s.pref_zerowaste;
            document.getElementById('setting-dietary').value = s.dietary || '';
            if (cameraSelect) cameraSelect.value = s.camera_facing || 'environment';
            renderAppliances(s.appliances || []);
            if (ttsEnabledEl) ttsEnabledEl.checked = s.tts_enabled === true;
            if (ttsUrlEl) ttsUrlEl.value = s.tts_url || '';
            if (ttsTokenEl) ttsTokenEl.value = s.tts_token || '';
            if (ttsMethEl) ttsMethEl.value = s.tts_method || 'POST';
            if (ttsAuthTypeEl) ttsAuthTypeEl.value = s.tts_auth_type || 'bearer';
            if (ttsCtEl) ttsCtEl.value = s.tts_content_type || 'application/json';
            if (ttsPayloadKeyEl) ttsPayloadKeyEl.value = s.tts_payload_key || 'message';
            if (scaleEnabledUiEl) scaleEnabledUiEl.checked = !!s.scale_enabled;
            if (scaleUrlUiEl) scaleUrlUiEl.value = s.scale_gateway_url || '';
            const mpEnabledUp = s.meal_plan_enabled !== false;
            if (mpEnabledEl) mpEnabledEl.checked = mpEnabledUp;
            if (mpConfigSection) mpConfigSection.style.display = mpEnabledUp ? '' : 'none';
            if (mpLegendCard) mpLegendCard.style.display = mpEnabledUp ? '' : 'none';
        }
    } catch(e) { /* offline, use local */ }
    // Scale settings
    const scaleEnabledUiEl = document.getElementById('setting-scale-enabled');
    if (scaleEnabledUiEl) scaleEnabledUiEl.checked = !!s.scale_enabled;
    const scaleUrlUiEl = document.getElementById('setting-scale-url');
    if (scaleUrlUiEl) scaleUrlUiEl.value = s.scale_gateway_url || '';
    // Hide kiosk download banner if running inside Android WebView (kiosk mode)
    const kioskBanner = document.getElementById('kiosk-download-banner');
    if (kioskBanner && /; wv\)/.test(navigator.userAgent)) {
        kioskBanner.style.display = 'none';
    }
}

// ── Kiosk overlay: X (close) + ↻ (refresh) buttons ───────────────────
// Injected into #header-left (left zone of the 3-column header).
// Only shown when _kioskBridge JS interface is available (Android WebView).
function _injectKioskOverlay() {
    if (typeof _kioskBridge === 'undefined') return;
    if (document.getElementById('_kiosk_overlay')) return;

    const headerLeft = document.getElementById('header-left');
    if (!headerLeft) return;

    const wrap = document.createElement('div');
    wrap.id = '_kiosk_overlay';
    wrap.style.cssText = 'display:flex;gap:6px;align-items:center;';

    const btnStyle = 'background:rgba(255,255,255,0.2);border:none;color:#fff;width:34px;height:34px;border-radius:50%;font-size:15px;cursor:pointer;display:flex;align-items:center;justify-content:center;-webkit-tap-highlight-color:transparent;touch-action:manipulation;';

    // Exit button
    const exitBtn = document.createElement('button');
    exitBtn.id = '_kiosk_exit_btn';
    exitBtn.textContent = '\u2715';
    exitBtn.title = 'Esci dal kiosk';
    exitBtn.style.cssText = btnStyle;
    exitBtn.addEventListener('click', (e) => {
        e.stopPropagation();
        if (confirm(t('confirm.kiosk_exit'))) _kioskBridge.exit();
    });

    // Refresh button
    const refBtn = document.createElement('button');
    refBtn.id = '_kiosk_refresh_btn';
    refBtn.textContent = '\u21bb';
    refBtn.title = 'Aggiorna pagina';
    refBtn.style.cssText = btnStyle.replace('font-size:15px', 'font-size:18px');
    refBtn.addEventListener('click', (e) => {
        e.stopPropagation();
        _kioskBridge.hardReload();
    });

    wrap.appendChild(exitBtn);
    wrap.appendChild(refBtn);
    headerLeft.appendChild(wrap);
}

function renderAppliances(appliances) {
    const container = document.getElementById('appliances-list');
    if (!appliances || appliances.length === 0) {
        container.innerHTML = '<p style="color:var(--text-muted);font-size:0.85rem;padding:8px 0">Nessun elettrodomestico aggiunto</p>';
        return;
    }
    container.innerHTML = appliances.map((a, i) => `
        <div class="appliance-item">
            <span>🔌 ${escapeHtml(a)}</span>
            <button class="appliance-remove" onclick="removeAppliance(${i})" title="Rimuovi">✕</button>
        </div>
    `).join('');
}

async function loadCameraDevices() {
    const select = document.getElementById('setting-camera-facing');
    if (!select) return;
    const s = getSettings();
    const current = s.camera_facing || 'environment';
    // Remove old device-specific options (keep first 2: environment, user)
    while (select.options.length > 2) select.remove(2);
    const cameras = await enumerateCameras();
    cameras.forEach(cam => {
        const opt = document.createElement('option');
        opt.value = cam.deviceId;
        opt.textContent = cam.label || `Camera ${cam.deviceId.slice(0, 8)}…`;
        select.appendChild(opt);
    });
    select.value = current;
}

function addAppliance() {
    const input = document.getElementById('new-appliance-input');
    const name = (input.value || '').trim();
    if (!name) return;
    const s = getSettings();
    if (!s.appliances) s.appliances = [];
    if (s.appliances.some(a => a.toLowerCase() === name.toLowerCase())) {
        showToast(t('error.appliance_exists'), 'error');
        return;
    }
    s.appliances.push(name);
    saveSettingsToStorage(s);
    renderAppliances(s.appliances);
    input.value = '';
    showToast(t('toast.appliance_added'), 'success');
}

function addApplianceQuick(name) {
    const s = getSettings();
    if (!s.appliances) s.appliances = [];
    if (s.appliances.some(a => a.toLowerCase() === name.toLowerCase())) {
        showToast(t('error.already_exists'), 'error');
        return;
    }
    s.appliances.push(name);
    saveSettingsToStorage(s);
    renderAppliances(s.appliances);
    showToast(`${name} aggiunto`, 'success');
}

function removeAppliance(idx) {
    const s = getSettings();
    if (!s.appliances) return;
    s.appliances.splice(idx, 1);
    saveSettingsToStorage(s);
    renderAppliances(s.appliances);
}

async function saveSettings() {
    const s = getSettings();
    s.gemini_key = document.getElementById('setting-gemini-key').value.trim();
    s.bring_email = document.getElementById('setting-bring-email').value.trim();
    s.bring_password = document.getElementById('setting-bring-password').value.trim();
    s.default_persons = parseInt(document.getElementById('setting-default-persons').value) || 1;
    s.pref_veloce = document.getElementById('setting-pref-veloce').checked;
    s.pref_pocafame = document.getElementById('setting-pref-pocafame').checked;
    s.pref_scadenze = document.getElementById('setting-pref-scadenze').checked;
    s.pref_healthy = document.getElementById('setting-pref-healthy').checked;
    s.pref_opened = document.getElementById('setting-pref-opened').checked;
    s.pref_zerowaste = document.getElementById('setting-pref-zerowaste').checked;
    s.dietary = document.getElementById('setting-dietary').value.trim();
    // Camera
    s.camera_facing = document.getElementById('setting-camera-facing').value;
    // Screensaver
    const ssEl = document.getElementById('setting-screensaver-enabled');
    if (ssEl) s.screensaver_enabled = ssEl.checked;
    // Meal plan enabled toggle
    const mpEnabledEl = document.getElementById('setting-meal-plan-enabled');
    if (mpEnabledEl) s.meal_plan_enabled = mpEnabledEl.checked;
    // TTS settings
    const ttsEnabledEl = document.getElementById('setting-tts-enabled');
    if (ttsEnabledEl) s.tts_enabled = ttsEnabledEl.checked;
    const ttsUrlEl2 = document.getElementById('setting-tts-url');
    if (ttsUrlEl2) s.tts_url = ttsUrlEl2.value.trim();
    const ttsEngineEl2 = document.getElementById('setting-tts-engine');
    if (ttsEngineEl2) s.tts_engine = ttsEngineEl2.value;
    const ttsVoiceEl2 = document.getElementById('setting-tts-voice');
    if (ttsVoiceEl2) s.tts_voice = ttsVoiceEl2.value;
    const ttsRateEl2 = document.getElementById('setting-tts-rate');
    if (ttsRateEl2) s.tts_rate = parseFloat(ttsRateEl2.value) || 1;
    const ttsPitchEl2 = document.getElementById('setting-tts-pitch');
    if (ttsPitchEl2) s.tts_pitch = parseFloat(ttsPitchEl2.value) || 1;
    const ttsMethEl2 = document.getElementById('setting-tts-method');
    if (ttsMethEl2) s.tts_method = ttsMethEl2.value;
    const ttsAuthTypeEl2 = document.getElementById('setting-tts-auth-type');
    if (ttsAuthTypeEl2) s.tts_auth_type = ttsAuthTypeEl2.value;
    const ttsTokenEl2 = document.getElementById('setting-tts-token');
    if (ttsTokenEl2) s.tts_token = ttsTokenEl2.value.trim();
    const ttsAuthHdrNameEl2 = document.getElementById('setting-tts-auth-header-name');
    if (ttsAuthHdrNameEl2) s.tts_auth_header_name = ttsAuthHdrNameEl2.value.trim();
    const ttsAuthHdrValEl2 = document.getElementById('setting-tts-auth-header-value');
    if (ttsAuthHdrValEl2) s.tts_auth_header_value = ttsAuthHdrValEl2.value.trim();
    const ttsCtEl2 = document.getElementById('setting-tts-content-type');
    if (ttsCtEl2) s.tts_content_type = ttsCtEl2.value;
    const ttsPayloadKeyEl2 = document.getElementById('setting-tts-payload-key');
    if (ttsPayloadKeyEl2) s.tts_payload_key = ttsPayloadKeyEl2.value.trim() || 'message';
    const ttsExtraEl2 = document.getElementById('setting-tts-extra-fields');
    if (ttsExtraEl2) s.tts_extra_fields = ttsExtraEl2.value.trim();
    // Scale settings
    const scaleEnabledEl = document.getElementById('setting-scale-enabled');
    if (scaleEnabledEl) s.scale_enabled = scaleEnabledEl.checked;
    const scaleUrlEl = document.getElementById('setting-scale-url');
    if (scaleUrlEl) s.scale_gateway_url = scaleUrlEl.value.trim();
    saveSettingsToStorage(s);
    
    // Save ALL settings to server .env
    try {
        const settingsToken = document.getElementById('setting-settings-token')?.value.trim() || '';
        const tokenHeader = settingsToken ? { 'X-Settings-Token': settingsToken } : {};
        const result = await api('save_settings', {}, 'POST', {
            ...(s.gemini_key ? { gemini_key: s.gemini_key } : {}),
            bring_email: s.bring_email,
            ...(s.bring_password ? { bring_password: s.bring_password } : {}),
            default_persons: s.default_persons,
            pref_veloce: s.pref_veloce,
            pref_pocafame: s.pref_pocafame,
            pref_scadenze: s.pref_scadenze,
            pref_healthy: s.pref_healthy,
            pref_opened: s.pref_opened,
            pref_zerowaste: s.pref_zerowaste,
            dietary: s.dietary,
            appliances: s.appliances,
            camera_facing: s.camera_facing,
            scale_enabled: s.scale_enabled,
            scale_gateway_url: s.scale_gateway_url,
            meal_plan_enabled: s.meal_plan_enabled,
            screensaver_enabled: s.screensaver_enabled,
            tts_enabled: s.tts_enabled,
            tts_url: s.tts_url,
            tts_token: s.tts_token,
            tts_method: s.tts_method,
            tts_auth_type: s.tts_auth_type,
            tts_content_type: s.tts_content_type,
            tts_payload_key: s.tts_payload_key,
        }, tokenHeader);
        const statusEl = document.getElementById('settings-status');
        if (result.success) {
            statusEl.className = 'settings-status success';
            statusEl.textContent = `✅ ${t('settings.saved')}`;
        } else {
            statusEl.className = 'settings-status error';
            const errMsg = result.error === 'unauthorized'
                ? '🔒 Token non valido o mancante'
                : `⚠️ ${t('settings.saved_local_error').replace('{error}', result.error || '')}`;
            statusEl.textContent = errMsg;
        }
        statusEl.style.display = 'block';
        setTimeout(() => statusEl.style.display = 'none', 4000);
    } catch(e) {
        const statusEl = document.getElementById('settings-status');
        statusEl.className = 'settings-status success';
        statusEl.textContent = `✅ ${t('settings.saved_local')}`;
        statusEl.style.display = 'block';
        setTimeout(() => statusEl.style.display = 'none', 4000);
    }
}

function switchSettingsTab(btn, tabId) {
    document.querySelectorAll('.settings-tab').forEach(t => t.classList.remove('active'));
    document.querySelectorAll('.settings-panel').forEach(p => p.classList.remove('active'));
    btn.classList.add('active');
    document.getElementById(tabId).classList.add('active');
}

function togglePasswordVisibility(inputId) {
    const input = document.getElementById(inputId);
    input.type = input.type === 'password' ? 'text' : 'password';
}

// ===== API HELPER =====
async function api(action, params = {}, method = 'GET', body = null, extraHeaders = {}) {
    // In demo mode, all Bring! write operations are no-ops
    if (_demoMode) {
        const BRING_WRITE_ACTIONS = ['bring_add', 'bring_remove', 'bring_migrate_names', 'bring_set_spec'];
        if (BRING_WRITE_ACTIONS.includes(action)) {
            return { success: true, added: 0, removed: 0, skipped: 0, _demo: true };
        }
        // bring_list returns the in-memory demo list
        if (action === 'bring_list') {
            return { success: true, purchase: shoppingItems, listUUID: 'demo-list', _demo: true };
        }
    }
    let url = `${API_BASE}?action=${action}`;
    if (method === 'GET') {
        Object.entries(params).forEach(([k, v]) => {
            url += `&${encodeURIComponent(k)}=${encodeURIComponent(v)}`;
        });
    }
    const opts = { method };
    if (body) {
        opts.headers = { 'Content-Type': 'application/json', ...extraHeaders };
        opts.body = JSON.stringify(body);
    } else if (Object.keys(extraHeaders).length > 0) {
        opts.headers = { ...extraHeaders };
    }
    const res = await fetch(url, opts);
    if (!res.ok) {
        remoteLog('API_ERROR', `${action} HTTP ${res.status}`);
        // Report HTTP 5xx as server errors (not 4xx which are usually user errors)
        if (res.status >= 500) {
            reportError({
                type:    'api-server-error',
                message: `API ${action} returned HTTP ${res.status}`,
                context: { action, status: res.status },
            });
        }
    }
    const data = await res.json();
    if (data && data.error) {
        remoteLog('API_FAIL', `${action}: ${data.error}`);
    }
    return data;
}

// ===== PAGE NAVIGATION =====
// Track current page for auto-refresh
let _currentPageId = 'dashboard';
let _currentPageParam = null;

// Refresh current page data without full navigation
function refreshCurrentPage() {
    switch(_currentPageId) {
        case 'dashboard': loadDashboard(); break;
        case 'inventory': loadInventory(); break;
        case 'shopping': loadShoppingList(); break;
        case 'products': loadAllProducts(); break;
        case 'recipe':   loadRecipeArchive(); break;
        case 'log':      loadLog(); break;
        // scan/ai/settings/chat: nessun dato live da ricaricare
    }
}

function showPage(pageId, param = null) {
    _currentPageId = pageId;
    _currentPageParam = param;
    // Hide all pages
    document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
    // Show target page
    const page = document.getElementById(`page-${pageId}`);
    if (page) page.classList.add('active');

    // Clear search inputs when navigating away
    const invSearch = document.getElementById('inventory-search');
    if (invSearch) invSearch.value = '';
    const prodSearch = document.getElementById('products-search');
    if (prodSearch) prodSearch.value = '';
    
    // Update nav
    document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active'));
    const navBtn = document.querySelector(`.nav-btn[data-page="${pageId}"]`);
    if (navBtn) navBtn.classList.add('active');
    
    // Page-specific init
    switch(pageId) {
        case 'dashboard':
            loadDashboard();
            break;
        case 'inventory':
            if (param !== null) {
                currentLocation = param;
                filterLocation(param);
            }
            loadInventory();
            break;
        case 'scan': initScanner(); clearQuickNameResults(); updateSpesaBanner();
            // Pre-warm the embedding model the first time user visits scan page
            if (typeof window._getCategoryPipeline === 'function' && !window._categoryPipelineReady) {
                window._getCategoryPipeline(); // fire-and-forget
            }
            break;
        case 'products': loadAllProducts(); break;
        case 'shopping': loadShoppingList(); break;
        case 'recipe': loadRecipeArchive(); break;
        case 'log': loadLog(); break;
        case 'ai': initAICamera(); break;
        case 'settings': loadSettingsUI(); break;
        case 'chat': if (_requireGemini()) initChat(); break;
    }

    // Auto-refresh banner notifications while on dashboard (every 5 min)
    if (_bannerRefreshTimer) { clearInterval(_bannerRefreshTimer); _bannerRefreshTimer = null; }
    if (pageId === 'dashboard') {
        _bannerRefreshTimer = setInterval(() => loadBannerAlerts(), 5 * 60 * 1000);
    }
    
    // Stop scanner when leaving scan page
    if (pageId !== 'scan' && pageId !== 'ai') {
        stopScanner();
    }
    
    // Scroll to top
    window.scrollTo(0, 0);
}

// ===== ANTI-WASTE SECTION =====

const WASTE_BENCHMARKS = {
    it: { avgWasteRate: 22, avgKgMonth: 5.4, costPerKg: 8.2, currency: '€', countryKey: 'antiwaste.country_it', rangeMin: 8,  rangeMax: 36 },
    de: { avgWasteRate: 20, avgKgMonth: 6.5, costPerKg: 7.7, currency: '€', countryKey: 'antiwaste.country_de', rangeMin: 7,  rangeMax: 34 },
    en: { avgWasteRate: 30, avgKgMonth: 9.2, costPerKg: 8.5, currency: '$', countryKey: 'antiwaste.country_en', rangeMin: 12, rangeMax: 50 },
};
const _AW_KG_PER_EVENT = 0.5;
let _awRefreshTimer = null;
let _awFactTimer    = null;
let _awBadgeTimer   = null;

// ── Embedded fallback facts (used when offline / API not yet loaded) ──
const AW_FACTS_FALLBACK = {
    it: [
        "Nel 2024 ogni italiano spreca ~554 g di cibo a settimana (Waste Watcher 2024)",
        "Lo spreco domestico in Italia vale oltre €7,5 miliardi l'anno",
        "La frutta fresca è l'alimento più sprecato in Italia: ~22g/persona/settimana",
        "Nel mondo si sprecano ~1,05 miliardi di tonnellate di cibo ogni anno (UNEP 2024)",
        "Il 19% del cibo globale disponibile al consumo viene buttato (UNEP 2024)",
        "Le famiglie sono responsabili del 60% dello spreco alimentare totale",
        "Lo spreco alimentare conta per l'8-10% delle emissioni globali di gas serra",
        "Se fosse un Paese, lo spreco alimentare sarebbe il 3° emettitore di CO₂ al mondo",
        "Lo spreco alimentare consuma il 25% dell'acqua dolce usata in agricoltura",
        "Un'area grande quanto la Cina viene coltivata per cibo mai mangiato",
        "Lo spreco alimentare costa al mondo ~€1.000 miliardi l'anno",
        "Il lunedì è il giorno in cui gli italiani buttano più cibo (residui del weekend)",
        "Solo il 30% degli italiani sa distinguere 'da consumarsi entro' da 'preferibilmente entro'",
        "Il ricorso al congelatore riduce lo spreco domestico del 20%",
        "1 kg di pane sprecato = 1.300 litri d'acqua consumati inutilmente",
        "Sprecare 1 hamburger = stessa acqua di una doccia da 90 minuti",
        "Lo spreco alimentare pro capite in Italia è ~29 kg/anno (domestico)",
        "Il 42% degli italiani dichiara di sprecare meno grazie all'aumento dei prezzi",
        "Solo il 15% degli italiani chiede la 'doggy bag' al ristorante",
        "Un quarto del cibo sprecato basterebbe a sfamare tutti gli affamati del mondo",
        "La Legge Gadda (166/2016) è tra le norme anti-spreco più avanzate d'Europa",
        "Il Sud Italia spreca in media l'8% in più rispetto al Nord",
        "Nel 2024 oltre 780 milioni di persone hanno sofferto la fame nel mondo (FAO)",
        "Educare i bambini a scuola riduce lo spreco familiare del 15%",
        "Il packaging intelligente potrebbe ridurre lo spreco del 15%",
    ],
    de: [
        "Deutsche Haushalte werfen pro Person rund 82 kg Lebensmittel pro Jahr weg (Destatis 2024)",
        "Weltweit werden ~1,05 Milliarden Tonnen Lebensmittel pro Jahr verschwendet (UNEP 2024)",
        "19% des global verfügbaren Lebensmittelangebots landet im Müll (UNEP 2024)",
        "Haushalte verursachen 60% der gesamten Lebensmittelverschwendung",
        "Lebensmittelverschwendung ist für 8-10% der globalen Treibhausgase verantwortlich",
        "Wäre Lebensmittelverschwendung ein Land, wäre es der 3. größte CO₂-Emittent",
        "25% des in der Landwirtschaft genutzten Süßwassers wird für nie gegessenes Essen verbraucht",
        "1 kg verschwendetes Rindfleisch ≈ 27 kg CO₂-Emissionen",
        "Das Einfrieren reduziert Haushaltsabfälle um bis zu 20%",
        "Nur ein Viertel der verschwendeten Lebensmittel würde alle Hungernden ernähren",
        "Schlaue Verpackungen könnten den Lebensmittelabfall um 15% senken",
    ],
    en: [
        "~1.05 billion tonnes of food are wasted globally every year (UNEP 2024)",
        "19% of food available for human consumption is wasted globally (UNEP 2024)",
        "Households account for 60% of all food waste globally",
        "Food waste represents 8-10% of global greenhouse gas emissions",
        "If food waste were a country, it would be the world's 3rd largest CO₂ emitter",
        "25% of freshwater used in farming grows food that is never eaten",
        "Food waste costs the world ~$1 trillion per year",
        "30–40% of the US food supply is wasted each year (USDA 2021)",
        "Americans spend ~$1,800/year on food they never eat",
        "Just a quarter of wasted food would feed all the world's hungry",
        "Smart packaging could cut food waste by 15%",
        "1 kg of wasted bread = 1,300 litres of water wasted",
        "Wasting one hamburger uses as much water as a 90-minute shower",
        "Teaching children about food waste reduces household waste by 15%",
        "In 2024, over 780 million people faced hunger despite global food abundance (FAO)",
    ],
};

// Live facts cache (loaded from API daily, falls back to embedded)
let _awLiveFacts = null;
const _AW_FACTS_LS_KEY = 'aw_facts_v2';
const _AW_FACTS_TS_KEY = 'aw_facts_ts_v2';

/** Load facts from localStorage cache or fetch from server (once per day). */
async function _awLoadFacts() {
    const cached = localStorage.getItem(_AW_FACTS_LS_KEY);
    const ts = parseInt(localStorage.getItem(_AW_FACTS_TS_KEY) || '0');
    const age = Date.now() - ts;

    // Use localStorage cache if < 24 h old
    if (cached && age < 86_400_000) {
        try { _awLiveFacts = JSON.parse(cached); return; } catch {}
    }

    // Try fetching from server if online
    if (!navigator.onLine) return;
    try {
        const data = await api('food_facts');
        if (data && data.it && data.it.length > 0) {
            _awLiveFacts = data;
            localStorage.setItem(_AW_FACTS_LS_KEY, JSON.stringify(data));
            localStorage.setItem(_AW_FACTS_TS_KEY, String(Date.now()));
        }
    } catch {}
}

/** Return current facts array for the active language. */
function _awGetFacts() {
    const src = _awLiveFacts || AW_FACTS_FALLBACK;
    return src[_currentLang] || src['it'] || AW_FACTS_FALLBACK['it'];
}

/** Fetch fresh stats and re-render the anti-waste section. */
function _awFetchAndRender() {
    if (!navigator.onLine) { _updateAwLiveDot(false); return; }
    api('stats').then(s => {
        _renderAntiWasteSection(
            s.used_30d      || 0, s.wasted_30d      || 0,
            s.used_prev_30d || 0, s.wasted_prev_30d || 0,
            s.used_prev_60d || 0, s.wasted_prev_60d || 0,
            true
        );
    }).catch(() => _updateAwLiveDot(false));
}

/** Update just the live indicator dot without re-rendering the whole card. */
function _updateAwLiveDot(online) {
    const dot = document.querySelector('.aw-live-dot');
    if (!dot) return;
    dot.className = 'aw-live-dot ' + (online ? 'aw-live-on' : 'aw-live-off');
    dot.title = online ? t('antiwaste.live_on') : t('antiwaste.live_off');
}

/** Start/stop the 60-second auto-refresh based on connectivity. */
function _startAntiWasteAutoRefresh() {
    clearInterval(_awRefreshTimer);
    if (navigator.onLine) _awRefreshTimer = setInterval(_awFetchAndRender, 60_000);
}

/**
 * Start badge rotation: shows only as many badges as fit in one row (auto-measured),
 * cycles through all with a fade every 5 minutes.
 * Call AFTER the row is already in the DOM with the initial slice rendered.
 */
function _startBadgeRotation(allBadges, maxVisible) {
    clearInterval(_awBadgeTimer);
    const row = document.getElementById('aw-badges-row');
    if (!row || allBadges.length <= maxVisible) return;

    let start = 0;
    const render = () => {
        const slice = [];
        for (let i = 0; i < maxVisible; i++) {
            slice.push(allBadges[(start + i) % allBadges.length]);
        }
        row.innerHTML = slice.join('');
    };

    const rotate = () => {
        if (!row.isConnected) { clearInterval(_awBadgeTimer); return; }
        row.style.opacity = '0';
        setTimeout(() => {
            start = (start + 1) % allBadges.length;
            render();
            row.style.opacity = '1';
        }, 380);
    };

    // Rotate every 5 minutes
    _awBadgeTimer = setInterval(rotate, 5 * 60_000);
}

/** Build one trend mini-card. */
function _awTrendCard(rate, label, maxRate) {
    if (rate === null) {
        return `<div class="aw-tcard aw-tcard-empty">
            <span class="aw-tc-label">${label}</span>
            <span class="aw-tc-rate">–</span>
            <div class="aw-tc-minibar"><div style="width:0"></div></div>
        </div>`;
    }
    const cls    = rate <= 8 ? 'good' : rate <= 20 ? 'ok' : 'bad';
    const barPct = Math.max(4, Math.round((rate / Math.max(maxRate, 5)) * 100));
    return `<div class="aw-tcard aw-tcard-${cls}">
        <span class="aw-tc-label">${label}</span>
        <span class="aw-tc-rate">${rate}%</span>
        <div class="aw-tc-minibar"><div style="width:${barPct}%"></div></div>
    </div>`;
}

/** Arrow between two consecutive trend values. */
function _awTrendArrow(prev, curr) {
    if (prev === null || curr === null) return null;
    const d = curr - prev;
    if (d <= -3) return { sym: '↓', cls: 'aw-arrow-good' };
    if (d >= 3)  return { sym: '↑', cls: 'aw-arrow-bad'  };
    return           { sym: '→', cls: 'aw-arrow-ok'   };
}

function _renderAntiWasteSection(used30, wasted30, usedP30, wastedP30, usedP60, wastedP60, isOnline = navigator.onLine) {
    const section = document.getElementById('waste-chart-section');
    const total30 = used30 + wasted30;
    if (total30 === 0) { section.style.display = 'none'; return; }
    section.style.display = 'block';

    const bm      = WASTE_BENCHMARKS[_currentLang] || WASTE_BENCHMARKS['it'];
    const country = t(bm.countryKey);
    const myRate  = Math.round((wasted30 / total30) * 100);
    const avgRate = bm.avgWasteRate;

    // Grade
    let grade, gradeClass;
    if      (myRate <= 3)  { grade = 'A+'; gradeClass = 'ap'; }
    else if (myRate <= 8)  { grade = 'A';  gradeClass = 'a';  }
    else if (myRate <= 15) { grade = 'B';  gradeClass = 'b';  }
    else if (myRate <= 25) { grade = 'C';  gradeClass = 'c';  }
    else                   { grade = 'D';  gradeClass = 'd';  }

    // Savings vs average
    const avgWastedEvents = total30 * (avgRate / 100);
    const savedEvents     = Math.max(0, avgWastedEvents - wasted30);
    const savedKg         = +(savedEvents * _AW_KG_PER_EVENT).toFixed(1);
    const savedMoney      = Math.round(savedKg * bm.costPerKg);
    const savedCO2        = +(savedKg * 2.5).toFixed(1);

    // Status
    let statusMsg, statusCls;
    if (myRate < avgRate) {
        statusMsg = t('antiwaste.better').replace('{country}', country).replace('{diff}', avgRate - myRate);
        statusCls = 'aw-status-good';
    } else if (myRate > avgRate) {
        statusMsg = t('antiwaste.worse').replace('{country}', country);
        statusCls = 'aw-status-bad';
    } else {
        statusMsg = t('antiwaste.on_par').replace('{country}', country);
        statusCls = 'aw-status-ok';
    }

    // Single stacked bar: avg always fills 88% of track width; you fills proportionally inside
    const scale   = Math.max(myRate, avgRate, 1);
    const avgPct  = 88; // avg always = reference width
    const youPct  = +((myRate / scale) * 88).toFixed(1); // your bar, same scale
    const youLabel = t('antiwaste.you').split(' ')[0]; // "Tu" / "You" / "Du"

    // Annual totals for comparison bar
    const myAnnualKg  = Math.round(wasted30 * _AW_KG_PER_EVENT * 12);
    const avgAnnualKg = Math.round(bm.avgKgMonth * 12);
    const annualInfo  = t('antiwaste.annual_info')
        .replace('{you}', myAnnualKg)
        .replace('{avg}', avgAnnualKg);

    // Build all badge objects (shown 4 at a time, rotated every 5 min)
    const diffPct = avgRate - myRate;
    const allBadges = [];
    allBadges.push(`<span class="aw-badge aw-badge-rate">
        <span class="aw-badge-icon">📊</span>
        <span class="aw-badge-body"><b>${myRate}%</b><small>${t('antiwaste.badge_rate')}</small></span>
    </span>`);
    if (wasted30 > 0) allBadges.push(`<span class="aw-badge aw-badge-wasted">
        <span class="aw-badge-icon">🗑️</span>
        <span class="aw-badge-body"><b>${wasted30}</b><small>${t('antiwaste.badge_wasted')}</small></span>
    </span>`);
    if (savedMoney > 0) allBadges.push(`<span class="aw-badge aw-badge-money">
        <span class="aw-badge-icon">💰</span>
        <span class="aw-badge-body"><b>${bm.currency}${savedMoney}/m</b><small>${t('antiwaste.badge_saved_money')}</small></span>
    </span>`);
    if (savedCO2 > 0) allBadges.push(`<span class="aw-badge aw-badge-co2">
        <span class="aw-badge-icon">🌍</span>
        <span class="aw-badge-body"><b>−${savedCO2} kg</b><small>CO₂</small></span>
    </span>`);
    if (diffPct > 0) allBadges.push(`<span class="aw-badge aw-badge-better">
        <span class="aw-badge-icon">✅</span>
        <span class="aw-badge-body"><b>−${diffPct}%</b><small>${t('antiwaste.badge_better')}</small></span>
    </span>`);

    // Initial render: show all badges (row uses nowrap so they overflow off-screen, no wrapping)
    // We'll measure and trim in requestAnimationFrame below.
    const initBadges = allBadges.join('');

    // Facts
    const facts   = _awGetFacts();
    const factIdx = Math.floor(Math.random() * facts.length);

    const liveCls = isOnline ? 'aw-live-on'          : 'aw-live-off';
    const liveTip = isOnline ? t('antiwaste.live_on') : t('antiwaste.live_off');

    section.innerHTML = `
        <div class="aw-header">
            <div class="aw-title-row">
                <span class="aw-live-dot ${liveCls}" title="${liveTip}"></span>
                <h3 class="aw-title">${t('antiwaste.title')}</h3>
            </div>
            <span class="aw-grade aw-grade-${gradeClass}" title="${t('antiwaste.grade_label')}">${grade}</span>
        </div>

        <div class="aw-cmp-wrap">
            <div class="aw-cmp-bar-track">
                <div id="aw-bar-avg" class="aw-cmp-bar-fill-avg"></div>
                <div id="aw-bar-you" class="aw-cmp-bar-fill-you"></div>
            </div>
            <div class="aw-cmp-legend">
                <span class="aw-cmp-legend-you">▮ ${youLabel} <strong>${myRate}%</strong></span>
                <span class="aw-cmp-legend-avg">${country} <strong>${avgRate}%</strong> ▮</span>
            </div>
            <p class="aw-status-inline ${statusCls}">${statusMsg} &nbsp;·&nbsp; ${annualInfo}</p>
        </div>

        ${allBadges.length > 0 ? `<div id="aw-badges-row" class="aw-savings-row">${initBadges}</div>` : ''}

        <div class="aw-fact-rotator">
            <span class="aw-fact-icon">💡</span>
            <span id="aw-fact-text" class="aw-fact-text">${facts[factIdx]}</span>
        </div>

        <div class="aw-source">${(_awLiveFacts && _awLiveFacts.source) || t('antiwaste.source')}</div>
    `;

    // After DOM insertion: animate bars + measure how many badges actually fit in one row
    requestAnimationFrame(() => {
        // Animate comparison bars
        const barYou = document.getElementById('aw-bar-you');
        const barAvg = document.getElementById('aw-bar-avg');
        if (barYou) { barYou.style.width = youPct + '%'; setTimeout(() => barYou.classList.add('loaded'), 100); }
        if (barAvg) { barAvg.style.width = avgPct + '%'; setTimeout(() => barAvg.classList.add('loaded'), 100); }

        // Measure how many badges fit in one row
        const row = document.getElementById('aw-badges-row');
        if (!row || !allBadges.length) return;

        const GAP = 6; // matches CSS gap
        const rowW = row.offsetWidth;

        // Measure each badge width by reading the rendered children
        const kids = [...row.children];
        let totalW = 0;
        let fit = 0;
        for (const el of kids) {
            const bw = el.offsetWidth;
            if (fit > 0) totalW += GAP;
            totalW += bw;
            if (totalW > rowW + 1) break; // +1 for sub-pixel rounding
            fit++;
        }
        fit = Math.max(1, fit);

        // Trim visible row to the fit count
        row.innerHTML = allBadges.slice(0, fit).join('');

        // Start rotation only if there are more badges than fit
        _startBadgeRotation(allBadges, fit);
    });

    // Fact rotation (every 6 s)
    if (_awFactTimer) clearInterval(_awFactTimer);
    if (facts.length > 1) {
        let idx = factIdx;
        _awFactTimer = setInterval(() => {
            const el = document.getElementById('aw-fact-text');
            if (!el) { clearInterval(_awFactTimer); return; }
            el.classList.add('aw-fact-fade');
            setTimeout(() => {
                idx = (idx + 1) % facts.length;
                el.textContent = facts[idx];
                el.classList.remove('aw-fact-fade');
            }, 420);
        }, 5 * 60_000);
    }
}

// ===== DASHBOARD =====
async function loadDashboard() {
    try {
        const [summaryData, statsData] = await Promise.all([
            api('inventory_summary'),
            api('stats')
        ]);
        
        // Update stat cards
        const summary = summaryData.summary || [];
        let total = 0;
        ['dispensa', 'frigo', 'freezer'].forEach(loc => {
            const s = summary.find(x => x.location === loc);
            const count = s ? s.product_count : 0;
            const el = document.getElementById(`stat-${loc}`);
            el.textContent = count;
            total += count;
        });
        // Add non-standard locations
        summary.forEach(s => {
            if (!['dispensa', 'frigo', 'freezer'].includes(s.location)) {
                total += s.product_count;
            }
        });
        // Load shopping list count from Bring!
        loadShoppingCount();
        
        // Quick recipe button - show when there are expiring products
        const recipeBar = document.getElementById('quick-recipe-bar');
        if (statsData.expiring_soon && statsData.expiring_soon.length > 0) {
            recipeBar.style.display = 'block';
        } else {
            recipeBar.style.display = 'none';
        }
        
        // Expiring items
        const expiringSection = document.getElementById('alert-expiring');
        const expiringList = document.getElementById('expiring-list');
        if (statsData.expiring_soon && statsData.expiring_soon.length > 0) {
            expiringSection.style.display = 'block';
            expiringList.innerHTML = statsData.expiring_soon.map(item => {
                const days = daysUntilExpiry(item.expiry_date);
                let badgeText, badgeClass;
                if (days === 0) { badgeText = t('expiry.today'); badgeClass = 'today'; }
                else if (days === 1) { badgeText = t('expiry.tomorrow'); badgeClass = 'expiring'; }
                else if (days <= 7) { badgeText = t('expiry.days').replace('{days}', days); badgeClass = 'expiring'; }
                else if (days <= 30) { badgeText = t('expiry.days_compact').replace('{n}', days); badgeClass = 'expiring-soon'; }
                else { const m = Math.round(days/30); badgeText = m <= 1 ? t('expiry.days_compact').replace('{n}', days) : t('expiry.months_approx').replace('{n}', m); badgeClass = 'expiring-later'; }
                const qtyDisplay = formatQuantity(item.quantity, item.unit, item.default_quantity, item.package_unit);
                return `
                <div class="alert-item alert-item-clickable" onclick="showAlertItemDetail(${item.id}, ${item.product_id})">
                    <div class="alert-item-info">
                        <span class="alert-item-name">${escapeHtml(item.name)}</span>
                        ${item.brand ? `<span class="alert-item-brand">${escapeHtml(item.brand)}</span>` : ''}
                    </div>
                    <div class="alert-item-badges">
                        <span class="alert-item-qty">📦 ${qtyDisplay}</span>
                        <span class="alert-item-badge ${badgeClass}">${badgeText}</span>
                    </div>
                </div>`;
            }).join('');
        } else {
            expiringSection.style.display = 'none';
        }
        
        // Expired items — items in the freezer that are still within the safety window are hidden
        const expiredSection = document.getElementById('alert-expired');
        const expiredList = document.getElementById('expired-list');
        const visibleExpired = (statsData.expired || []).filter(item => {
            const days = Math.abs(daysUntilExpiry(item.expiry_date));
            return getExpiredSafety(item, days).level !== 'ok';
        });
        if (visibleExpired.length > 0) {
            expiredSection.style.display = 'block';
            expiredList.innerHTML = visibleExpired.map(item => {
                const days = Math.abs(daysUntilExpiry(item.expiry_date));
                let daysText;
                if (days === 0) daysText = t('expiry.expired_today');
                else if (days === 1) daysText = t('expiry.expired_yesterday');
                else daysText = t('expiry.expired_days').replace('{days}', days);
                const safety = getExpiredSafety(item, days);
                const locIcon = item.location === 'freezer' ? '❄️' : item.location === 'frigo' ? '🧊' : '';
                const qtyDisplayExp = formatQuantity(item.quantity, item.unit, item.default_quantity, item.package_unit);
                return `
                <div class="alert-item expired-item alert-item-clickable" onclick="showAlertItemDetail(${item.id}, ${item.product_id})">
                    <div class="alert-item-info">
                        <span class="alert-item-name">${locIcon ? locIcon + ' ' : ''}${escapeHtml(item.name)}</span>
                        ${item.brand ? `<span class="alert-item-brand">${escapeHtml(item.brand)}</span>` : ''}
                        <span class="alert-item-qty">📦 ${qtyDisplayExp}</span>
                    </div>
                    <div class="alert-item-badges">
                        <span class="alert-item-badge expired">${daysText}</span>
                        <span class="safety-badge safety-${safety.level}" title="${safety.tip}">${safety.icon} ${safety.label}</span>
                    </div>
                </div>`;
            }).join('');
        } else {
            expiredSection.style.display = 'none';
        }
        
        // Banner alerts (suspicious quantities + consumption predictions)
        loadBannerAlerts();

        // Anti-waste section (load facts first so rotation has full dataset)
        await _awLoadFacts();
        _renderAntiWasteSection(
            statsData.used_30d      || 0, statsData.wasted_30d      || 0,
            statsData.used_prev_30d || 0, statsData.wasted_prev_30d || 0,
            statsData.used_prev_60d || 0, statsData.wasted_prev_60d || 0,
            navigator.onLine
        );
        _startAntiWasteAutoRefresh();

        // Opened (partially used products with known package capacity)
        const openedSection = document.getElementById('alert-opened');
        const openedList = document.getElementById('opened-list');
        if (statsData.opened && statsData.opened.length > 0) {
            // Sorted server-side by days_to_expiry ASC
            openedSection.style.display = 'block';
            const MAX_SHOWN = 20;
            const visible = statsData.opened.slice(0, MAX_SHOWN);
            const extra = statsData.opened.length - visible.length;
            openedList.innerHTML = visible.map(item => {
                const locInfo = LOCATIONS[item.location] || { icon: '📦', label: item.location };
                const qty = parseFloat(item.quantity);
                const pkgSize = parseFloat(item.default_quantity);
                const unitLabels = { 'ml': 'ml', 'g': 'g', 'pz': 'pz' };
                let qtyText = '';

                if (item.unit === 'conf') {
                    const pkgUnit = item.package_unit;
                    const pkgLabel = unitLabels[pkgUnit] || pkgUnit;
                    const wholeConf = Math.floor(qty + 0.001);
                    const frac = Math.round((qty - wholeConf) * 1000) / 1000;
                    const remainderAmt = frac * pkgSize;
                    const remainderText = formatSubRemainder(remainderAmt, pkgUnit);
                    if (wholeConf > 0 && remainderAmt >= 1) {
                        qtyText = `${wholeConf} conf (da ${pkgSize}${pkgLabel}) + ${remainderText}`;
                    } else if (wholeConf > 0) {
                        qtyText = `${wholeConf} conf (da ${pkgSize}${pkgLabel})`;
                    } else {
                        qtyText = remainderText;
                    }
                } else {
                    const unitLabel = unitLabels[item.unit] || item.unit || '';
                    const wholePackages = Math.floor(qty / pkgSize + 0.001);
                    const remainder = Math.round((qty - wholePackages * pkgSize) * 100) / 100;
                    if (wholePackages > 0 && remainder > 0.01) {
                        qtyText = `${wholePackages} × ${pkgSize}${unitLabel} + ${Math.round(remainder)}${unitLabel} ${t('inventory.qty_remainder_suffix')}`;
                    } else if (remainder > 0.01) {
                        qtyText = `${Math.round(remainder)}${unitLabel} / ${pkgSize}${unitLabel}`;
                    } else {
                        qtyText = `${qty}${unitLabel}`;
                    }
                }

                // Expiry badge
                const days = item.days_to_expiry;
                const isEdible = item.is_edible;
                let expiryBadge = '';
                if (days !== null && days !== undefined) {
                    let expiryClass, expiryText;
                    if (!isEdible) {
                        expiryClass = 'opened-expiry-spoiled';
                        expiryText = t('expiry.badge_expired');
                    } else if (days > 365) {
                        expiryClass = 'opened-expiry-ok';
                        expiryText = t('expiry.badge_stable');
                    } else if (days === 0) {
                        expiryClass = 'opened-expiry-today';
                        expiryText = t('expiry.badge_today');
                    } else if (days <= 2) {
                        expiryClass = 'opened-expiry-urgent';
                        expiryText = t('expiry.badge_expiring_short').replace('{n}', days);
                    } else if (days <= 5) {
                        expiryClass = 'opened-expiry-soon';
                        expiryText = t('expiry.badge_expiring_short').replace('{n}', days);
                    } else {
                        expiryClass = 'opened-expiry-ok';
                        expiryText = t('expiry.badge_ok_still').replace('{n}', days);
                    }
                    const vacuumNote = item.vacuum_sealed ? ' 🔒' : '';
                    expiryBadge = `<span class="alert-item-badge opened-expiry ${expiryClass}">${expiryText}${vacuumNote}</span>`;
                }

                return `
                <div class="alert-item alert-item-clickable${!isEdible ? ' alert-item-spoiled' : ''}" onclick="showAlertItemDetail(${item.id}, ${item.product_id})">
                    <div class="alert-item-info">
                        <span class="alert-item-name">${escapeHtml(item.name)}</span>
                        ${item.brand ? `<span class="alert-item-brand">${escapeHtml(item.brand)}</span>` : ''}
                    </div>
                    <div class="alert-item-badges">
                        <span class="alert-item-qty">${locInfo.icon} ${locInfo.label}</span>
                        <span class="alert-item-badge opened">${qtyText}</span>
                        ${expiryBadge}
                    </div>
                </div>`;
            }).join('') + (extra > 0 ? `<div class="alert-more-note">${t('dashboard.more_opened').replace('{n}', extra)}</div>` : '');
        } else {
            openedSection.style.display = 'none';
        }
        
    } catch (err) {
        console.error('Dashboard load error:', err);
    }
}

function openedFraction(item) {
    const qty = parseFloat(item.quantity);
    const pkgSize = parseFloat(item.default_quantity);
    if (item.unit === 'conf') {
        return qty - Math.floor(qty + 0.001);
    }
    return (qty - Math.floor(qty / pkgSize + 0.001) * pkgSize) / pkgSize;
}

function quickRecipeSuggestion() {
    if (!_requireGemini()) return;
    // Navigate to chat and auto-send a prompt about expiring products
    showPage('chat');
    setTimeout(() => {
        document.getElementById('chat-input').value = 'Suggeriscimi una ricetta veloce PER UNA PERSONA usando i prodotti che scadono prima! Ignora i prodotti in freezer (hanno scadenze molto lunghe), concentrati su frigo e dispensa.';
        sendChatMessage();
    }, 500);
}

// === SUSPICIOUS QUANTITY THRESHOLDS ===
const QTY_THRESHOLDS = {
    'pz':   { min: 0.3,  max: 50 },
    'conf': { min: 0.3,  max: 50 },
    'g':    { min: 3,    max: 10000 },
    'ml':   { min: 3,    max: 10000 },
};

function isSuspiciousQty(qty, unit) {
    const n = parseFloat(qty);
    if (isNaN(n) || n <= 0) return false;
    const th = QTY_THRESHOLDS[unit] || QTY_THRESHOLDS['pz'];
    return n < th.min || n > th.max;
}

function isSuspiciousDefaultQty(defaultQty, unit, packageUnit) {
    const n = parseFloat(defaultQty);
    if (!n || n <= 0) return false;
    const checkUnit = (unit === 'conf' && packageUnit) ? packageUnit : unit;
    const th = QTY_THRESHOLDS[checkUnit] || QTY_THRESHOLDS['pz'];
    return n > th.max;
}

function getReviewConfirmed() {
    return _reviewConfirmedCache || {};
}
let _reviewConfirmedCache = {};

function setReviewConfirmed(inventoryId) {
    const c = getReviewConfirmed();
    c[inventoryId] = Date.now();
    _reviewConfirmedCache = c;
    api('app_settings_save', {}, 'POST', { settings: { review_confirmed: c } }).catch(() => {});
}

// === ALERT BANNER SYSTEM (replaces old review table) ===
let _bannerQueue = [];   // array of { type, data } — 'review' or 'prediction'
let _bannerIndex = 0;
let _bannerEditPending = false;  // true when editing from banner → dismiss after save
let _bannerRefreshTimer = null;  // periodic refresh while on dashboard

/**
 * Load suspicious quantities + consumption predictions + expired + expiring soon,
 * merge into a single banner queue and show the first item.
 */
async function loadBannerAlerts() {
    _bannerQueue = [];
    _bannerIndex = 0;
    const banner = document.getElementById('alert-banner');
    if (!banner) { console.warn('[Banner] #alert-banner not found'); return; }

    try {
        const [invData, predData, anomalyData, finishedData] = await Promise.all([
            api('inventory_list'),
            api('consumption_predictions').catch(err => { console.warn('[Banner] predictions fetch failed:', err); return { predictions: [] }; }),
            api('inventory_anomalies').catch(err => { console.warn('[Banner] anomalies fetch failed:', err); return { anomalies: [] }; }),
            api('inventory_finished_items').catch(err => { console.warn('[Banner] finished_items fetch failed:', err); return { finished: [] }; }),
        ]);
        const items = invData.inventory || [];
        const confirmed = getReviewConfirmed();

        // 1. Expired products (highest priority) - derived from inventory
        // Also considers opened_at: if item is opened and its opened-shelf-life has passed, it's expired too
        items.forEach(item => {
            if (!item.expiry_date && !item.opened_at) return;
            if (confirmed['exp_' + item.id]) return;

            let daysExpired = null;

            // Check raw expiry date
            if (item.expiry_date) {
                const rawDays = daysUntilExpiry(item.expiry_date);
                if (rawDays < 0) daysExpired = Math.abs(rawDays);
            }

            // Check effective expiry based on opened_at
            if (item.opened_at) {
                const openDays = estimateOpenedExpiryDays(item, item.location);
                const openedTs = new Date(item.opened_at).getTime();
                const effectiveExpiry = new Date(openedTs + openDays * 86400000);
                const today = new Date(); today.setHours(0, 0, 0, 0);
                const openedDiff = Math.round((effectiveExpiry.getTime() - today.getTime()) / 86400000);
                if (openedDiff < 0) {
                    const openedExpiredDays = Math.abs(openedDiff);
                    if (daysExpired === null || openedExpiredDays > daysExpired) daysExpired = openedExpiredDays;
                }
            }

            if (daysExpired === null) return; // not expired by any measure
            // Skip items the freezer bonus still considers safe — no need to alarm the user
            if (getExpiredSafety(item, daysExpired).level === 'ok') return;
            _bannerQueue.push({ type: 'expired', data: { ...item, days_expired: daysExpired } });
        });

        // 2. Suspicious quantities ("expiring soon" shown only in dashboard sections, not in banner)
        // Group items by product identity to detect sibling entries in other locations.
        // A "low quantity" alert is suppressed when other stock of the same product exists
        // (e.g. 191 ml of milk in the fridge is fine if there are 11 sealed packages in the pantry).
        const _productKey = item => item.barcode || `${item.name}||${item.brand || ''}`;
        const _productGroups = {};
        items.forEach(item => {
            const k = _productKey(item);
            if (!_productGroups[k]) _productGroups[k] = [];
            _productGroups[k].push(item);
        });

        items.forEach(item => {
            if (confirmed[item.id]) return;
            const t_ = QTY_THRESHOLDS[item.unit] || QTY_THRESHOLDS['pz'];
            const qty = parseFloat(item.quantity);
            let isLow  = !isNaN(qty) && qty > 0 && qty < t_.min;
            let isHigh = !isNaN(qty) && qty > t_.max;

            // For conf unit: evaluate thresholds on total sub-unit volume when possible,
            // not on raw package count. "400 conf" with no package size is uninterpretable
            // (could be grams entered with the wrong unit) — skip the high check.
            if (item.unit === 'conf') {
                const pkgSize = parseFloat(item.default_quantity);
                if (pkgSize > 0 && item.package_unit) {
                    const totalSub = qty * pkgSize;
                    const subTh = QTY_THRESHOLDS[item.package_unit] || QTY_THRESHOLDS['pz'];
                    isLow  = totalSub > 0 && totalSub < subTh.min;
                    isHigh = totalSub > subTh.max;
                } else {
                    // No package size known — can't judge quantity; suppress high-qty noise
                    isHigh = false;
                }
            }

            const suspDq = isSuspiciousDefaultQty(item.default_quantity, item.unit, item.package_unit);

            if (!isLow && !isHigh && !suspDq) return;

            // Suppress low-qty warning when sibling entries for the same product exist
            // in other locations — the user is simply tracking a partial/opened unit.
            if (isLow && !isHigh && !suspDq) {
                const siblings = (_productGroups[_productKey(item)] || []).filter(s => s.id !== item.id && parseFloat(s.quantity) > 0);
                if (siblings.length > 0) return;
            }

            let warning;
            if (suspDq && !isLow && !isHigh) warning = '📦 Conf. sospetta';
            else if (isLow) warning = '⬇️ Troppo poco';
            else warning = '⬆️ Troppo';
            _bannerQueue.push({ type: 'review', data: { ...item, warning, _isLow: isLow } });
        });

        // 4. Consumption predictions that don't match actual quantity
        const predictions = predData.predictions || [];
        predictions.forEach(pred => {
            if (confirmed['pred_' + pred.inventory_id]) return;
            _bannerQueue.push({ type: 'prediction', data: pred });
        });

        // 5. Inventory anomalies (qty doesn't match transaction history)
        const anomalies = anomalyData.anomalies || [];
        anomalies.forEach(an => {
            if (confirmed['an_' + an.dismiss_key]) return;
            _bannerQueue.push({ type: 'anomaly', data: an });
        });

        // 6. Finished products: inventory hit 0, waiting for user confirmation
        const finished = finishedData.finished || [];
        finished.forEach(fin => {
            if (confirmed['fin_' + fin.product_id]) return;
            _bannerQueue.push({ type: 'finished', data: fin });
        });

        // Sort by priority (highest first)
        _bannerQueue.sort((a, b) => _bannerPriority(b) - _bannerPriority(a));

        console.log(`[Banner] queue ready: ${_bannerQueue.length} items (${items.length} inv, ${predictions.length} pred, ${Object.keys(confirmed).length} confirmed)`);

    } catch (e) {
        console.error('[Banner] loadBannerAlerts error:', e);
    }

    if (_bannerQueue.length > 0) {
        _bannerIndex = 0;
        renderBannerItem();
        initBannerSwipe();
    } else {
        banner.style.display = 'none';
    }
}

/**
 * Compute a numeric priority score for a banner item.
 * Higher = more important = shown first.
 *
 * Priority tiers:
 *   1000+ : expired (longer ago = higher)
 *   500-799: anomalies (data discrepancies)
 *   200-499: suspicious quantities (low stock > high stock > package)
 *   100-199: consumption predictions (higher deviation% = higher)
 */
function _bannerPriority(entry) {
    switch (entry.type) {
        case 'expired': {
            const d = entry.data.days_expired || 0;
            // Expired longer = more urgent; base 1000 + days (capped)
            return 1000 + Math.min(d, 500);
        }
        case 'review': {
            const w = entry.data.warning || '';
            // Low stock is more urgent than too-much
            if (w.includes('Troppo poco')) return 400;
            if (w.includes('Troppo')) return 300;
            return 200; // package suspicion
        }
        case 'prediction': {
            const dev = entry.data.deviation_pct || 0;
            // Higher deviation = more important, capped at 99
            return 100 + Math.min(dev, 99);
        }
        case 'anomaly': {
            // Phantom (inflated qty) = 250, Missing = 260 (slightly higher, means data is clearly wrong)
            return entry.data.direction === 'missing' ? 260 : 250;
        }
        case 'finished':
            return 600; // product ran out — confirm before removing from DB
        default:
            return 0;
    }
}

function renderBannerItem() {
    const banner = document.getElementById('alert-banner');
    if (!banner || _bannerQueue.length === 0) { if (banner) banner.style.display = 'none'; return; }
    if (_bannerIndex >= _bannerQueue.length) _bannerIndex = 0;

    const entry = _bannerQueue[_bannerIndex];
    const iconEl    = document.getElementById('alert-banner-icon');
    const titleEl   = document.getElementById('alert-banner-title');
    const detailEl  = document.getElementById('alert-banner-detail');
    const actionsEl = document.getElementById('alert-banner-actions');
    const counterEl = document.getElementById('alert-banner-counter');
    const s = getSettings();
    const hasScale = s.scale_enabled && s.scale_gateway_url && _scaleConnected;

    if (entry.type === 'expired') {
        const item = entry.data;
        const qtyDisplay = formatQuantity(item.quantity, item.unit, item.default_quantity, item.package_unit);
        const daysText = item.days_expired === 0
            ? t('expiry.expired_today_long')
            : t('expiry.expired_ago_long').replace('{n}', item.days_expired);
        const safety = getExpiredSafety(item, item.days_expired);
        if (safety.level === 'danger') {
            banner.className = 'alert-banner banner-expired banner-expired-danger';
            iconEl.textContent = '🚫';
        } else if (safety.level === 'warning') {
            banner.className = 'alert-banner banner-expired banner-expired-warning';
            iconEl.textContent = '👀';
        } else {
            banner.className = 'alert-banner banner-expired banner-expired-ok';
            iconEl.textContent = '✅';
        }
        const expiredSuffix = safety.level === 'ok'
            ? t('expiry.expired_suffix_ok')
            : safety.level === 'warning'
                ? t('expiry.expired_suffix_warning')
                : t('expiry.expired_suffix');
        titleEl.textContent = `${item.name}${item.brand ? ' (' + item.brand + ')' : ''} ${expiredSuffix}`;
        const baseDetail = t('dashboard.banner_expired_detail').replace('{when}', daysText).replace('{qty}', qtyDisplay);
        detailEl.innerHTML = `${baseDetail} <span class="banner-safety-tip banner-safety-${safety.level}">${safety.icon} ${safety.tip}</span>`;
        let btns = '';
        if (safety.level !== 'danger') {
            btns += `<button class="btn-banner btn-banner-use" onclick="bannerQuickUse()">${t('dashboard.banner_expired_action_use')}</button>`;
        }
        btns += `<button class="btn-banner btn-banner-throw${safety.level === 'danger' ? ' btn-banner-throw-primary' : ''}" onclick="bannerThrowAway()">${t('dashboard.banner_expired_action_throw')}</button>`;
        btns += `<button class="btn-banner btn-banner-edit" onclick="editBannerExpiry()">${t('dashboard.banner_expired_action_edit')}</button>`;
        if (safety.level === 'danger') {
            btns += `<button class="btn-banner btn-banner-use btn-banner-use-danger" onclick="bannerQuickUse()">${t('dashboard.banner_expired_action_use')}</button>`;
        }
        btns += `<button class="btn-banner btn-banner-ok" onclick="dismissBannerExpired()">${t('dashboard.banner_review_dismiss')}</button>`;
        actionsEl.innerHTML = btns;

    } else if (entry.type === 'review') {
        const item = entry.data;
        // For conf unit with known package size, display the sub-unit total (e.g., 800g)
        // instead of a raw conf count that could be confused with "N confezioni".
        let qtyDisplay;
        if (item.unit === 'conf' && parseFloat(item.default_quantity) > 0 && item.package_unit) {
            const totalSub = Math.round(parseFloat(item.quantity) * parseFloat(item.default_quantity));
            qtyDisplay = `${totalSub} ${item.package_unit}`;
        } else {
            qtyDisplay = formatQuantity(item.quantity, item.unit, item.default_quantity, item.package_unit);
        }
        const suspDq = isSuspiciousDefaultQty(item.default_quantity, item.unit, item.package_unit);
        const isLow  = !!item._isLow; // set when banner item was built
        const t_ = QTY_THRESHOLDS[item.unit] || QTY_THRESHOLDS['pz'];
        banner.className = 'alert-banner';
        iconEl.textContent = '⚠️';
        let titleText, detailText;
        if (suspDq && !isLow) {
            titleText = `${t('dashboard.banner_review_unusual_pkg_title')}: ${item.name}${item.brand ? ' (' + item.brand + ')' : ''}`;
            detailText = t('dashboard.banner_review_unusual_pkg_detail', { qty: item.default_quantity, unit: item.package_unit });
        } else if (isLow) {
            titleText = `${t('dashboard.banner_review_low_qty_title')}: ${item.name}${item.brand ? ' (' + item.brand + ')' : ''}`;
            detailText = t('dashboard.banner_review_low_qty_detail', { qty: qtyDisplay });
        } else {
            titleText = `${t('dashboard.banner_review_high_qty_title')}: ${item.name}${item.brand ? ' (' + item.brand + ')' : ''}`;
            detailText = t('dashboard.banner_review_high_qty_detail', { qty: qtyDisplay });
        }
        titleEl.textContent = titleText;
        detailEl.textContent = detailText;
        let btns = `<button class="btn-banner btn-banner-ok" onclick="confirmBannerReview()">${t('dashboard.banner_review_action_ok')}</button>`;
        if (isLow) {
            btns += `<button class="btn-banner btn-banner-finish" onclick="bannerFinishAll()">${t('dashboard.banner_review_action_finish')}</button>`;
        }
        btns += `<button class="btn-banner btn-banner-edit" onclick="editBannerReview()">${t('dashboard.banner_review_action_edit')}</button>`;
        if (hasScale) {
            btns += `<button class="btn-banner btn-banner-weigh" onclick="weighBannerItem()">${t('dashboard.banner_review_action_weigh')}</button>`;
        }
        actionsEl.innerHTML = btns;

    } else if (entry.type === 'prediction') {
        const pred = entry.data;
        const dir = pred.direction || 'less';
        const dailyRate = parseFloat(pred.daily_rate) || 0;
        const daysSince = parseInt(pred.days_since_restock) || 0;
        banner.className = 'alert-banner banner-prediction';
        iconEl.textContent = '📊';
        titleEl.textContent = `${t('dashboard.banner_prediction_title')}: ${pred.name}${pred.brand ? ' (' + pred.brand + ')' : ''}`;
        let rateText = '';
        if (dailyRate > 0) {
            rateText = dailyRate >= 1
                ? t('dashboard.banner_prediction_rate_day', { n: Math.round(dailyRate), unit: pred.unit })
                : t('dashboard.banner_prediction_rate_week', { n: Math.round(dailyRate * 7), unit: pred.unit });
        }
        const timeText = daysSince > 0 ? ` — ${t('dashboard.banner_prediction_days_ago', { n: daysSince })}` : '';
        let diffText;
        if (dir === 'more') {
            diffText = t('dashboard.banner_prediction_more', { expected: pred.expected_qty, unit: pred.unit, time: timeText, actual: pred.actual_qty });
        } else {
            diffText = t('dashboard.banner_prediction_less', { expected: pred.expected_qty, unit: pred.unit, time: timeText, actual: pred.actual_qty });
        }
        detailEl.innerHTML = rateText ? `${rateText}: ${diffText}` : diffText.charAt(0).toUpperCase() + diffText.slice(1);
        let btns = `<button class="btn-banner btn-banner-confirm" onclick="confirmBannerPrediction()">${t('dashboard.banner_prediction_action_confirm', { qty: pred.actual_qty, unit: pred.unit })}</button>`;
        btns += `<button class="btn-banner btn-banner-edit" onclick="editBannerPrediction()">${t('dashboard.banner_prediction_action_edit')}</button>`;
        if (hasScale) {
            btns += `<button class="btn-banner btn-banner-weigh" onclick="weighBannerItem()">${t('dashboard.banner_prediction_action_weigh')}</button>`;
        }
        actionsEl.innerHTML = btns;

    } else if (entry.type === 'finished') {
        const fin = entry.data;
        banner.className = 'alert-banner banner-finished';
        iconEl.textContent = '📦';
        const barcodeSuffix = fin.barcode && fin.barcode.length >= 3
            ? ` <span style="font-family:monospace;font-size:0.7em;opacity:0.6">…${escapeHtml(fin.barcode.slice(-3))}</span>`
            : '';
        titleEl.innerHTML = `${escapeHtml(fin.name)}${fin.brand ? ' (' + escapeHtml(fin.brand) + ')' : ''}${barcodeSuffix} — ${escapeHtml(t('dashboard.banner_finished_title'))}`;
        const expectedText = fin.expected_qty ? ' ' + t('dashboard.banner_finished_expected', { qty: fin.expected_qty, unit: fin.unit }) : '';
        detailEl.innerHTML = t('dashboard.banner_finished_zero') + expectedText + ' ' + t('dashboard.banner_finished_check');
        let btns = `<button class="btn-banner btn-banner-ok" onclick="confirmBannerFinished()">${t('dashboard.banner_finished_action_yes')}</button>`;
        btns += `<button class="btn-banner btn-banner-edit" onclick="notFinishedBannerAction()">${t('dashboard.banner_finished_action_no')}</button>`;
        actionsEl.innerHTML = btns;

    } else if (entry.type === 'anomaly') {
        const an = entry.data;
        const isPhantom = an.direction === 'phantom';
        const isUntracked = an.direction === 'untracked';
        banner.className = 'alert-banner banner-anomaly';
        iconEl.textContent = '🔍';
        if (isUntracked) {
            // More consumption recorded than entries — initial stock was never registered
            titleEl.textContent = `${an.name} — ${t('dashboard.banner_anomaly_untracked_title')}`;
            detailEl.innerHTML = t('dashboard.banner_anomaly_untracked_detail', { inv_qty: an.inv_qty, unit: an.unit });
        } else if (isPhantom) {
            titleEl.textContent = `${an.name} — ${t('dashboard.banner_anomaly_phantom_title')}`;
            detailEl.innerHTML = t('dashboard.banner_anomaly_phantom_detail', { inv_qty: an.inv_qty, unit: an.unit, expected_qty: an.expected_qty });
        } else {
            titleEl.textContent = `${an.name} — ${t('dashboard.banner_anomaly_ghost_title')}`;
            detailEl.innerHTML = t('dashboard.banner_anomaly_ghost_detail', { expected_qty: an.expected_qty, unit: an.unit, name: an.name, inv_qty: an.inv_qty });
        }
        let btns = `<button class="btn-banner btn-banner-edit" onclick="editBannerAnomaly()">${t('dashboard.banner_anomaly_action_edit')}</button>`;
        btns += `<button class="btn-banner btn-banner-ok" onclick="dismissBannerAnomaly()">${t('dashboard.banner_anomaly_action_dismiss')} (${an.inv_qty} ${an.unit})</button>`;
        if (_geminiAvailable) {
            btns += `<button class="btn-banner btn-banner-ai" onclick="explainBannerAnomaly()" title="Chiedi a Gemini una spiegazione">\ud83e\udd16 Spiega</button>`;
        }
        actionsEl.innerHTML = btns;
    }

    if (_bannerQueue.length > 1) {
        let dots = `<span class="banner-nav-arrow" onclick="bannerPrev()">‹</span>`;
        dots += _bannerQueue.map((_, i) =>
            `<span class="banner-dot${i === _bannerIndex ? ' active' : ''}" onclick="_bannerIndex=${i};renderBannerItem()"></span>`
        ).join('');
        dots += `<span class="banner-nav-arrow" onclick="bannerNext()">›</span>`;
        counterEl.innerHTML = dots;
    } else {
        counterEl.innerHTML = '';
    }
    banner.style.display = '';
}

function dismissBannerItem() {
    _bannerQueue.splice(_bannerIndex, 1);
    if (_bannerQueue.length === 0) {
        document.getElementById('alert-banner').style.display = 'none';
        return;
    }
    if (_bannerIndex >= _bannerQueue.length) _bannerIndex = 0;
    renderBannerItem();
}

function confirmBannerReview() {
    const entry = _bannerQueue[_bannerIndex];
    if (!entry || entry.type !== 'review') return;
    setReviewConfirmed(entry.data.id);
    showToast(t('toast.quantity_confirmed'), 'success');
    dismissBannerItem();
}

function editBannerReview() {
    const entry = _bannerQueue[_bannerIndex];
    if (!entry || entry.type !== 'review') return;
    _bannerEditPending = true;
    editReviewItem(entry.data.id, entry.data.product_id);
}

function confirmBannerPrediction() {
    const entry = _bannerQueue[_bannerIndex];
    if (!entry || entry.type !== 'prediction') return;
    setReviewConfirmed('pred_' + entry.data.inventory_id);
    showToast('✅ Confermato — il sistema ricalcolerà le previsioni dalle prossime registrazioni', 'success');
    dismissBannerItem();
}

function editBannerPrediction() {
    const entry = _bannerQueue[_bannerIndex];
    if (!entry || entry.type !== 'prediction') return;
    _bannerEditPending = true;
    editReviewItem(entry.data.inventory_id, entry.data.product_id);
}

async function explainBannerAnomaly() {
    if (!_requireGemini()) return;
    const entry = _bannerQueue[_bannerIndex];
    if (!entry || entry.type !== 'anomaly') return;
    const an = entry.data;

    // Show loading inline in the banner detail area
    const detailEl = document.getElementById('alert-banner-detail');
    if (!detailEl) return;
    const originalHtml = detailEl.innerHTML;
    detailEl.innerHTML = '<em style="opacity:0.7">\ud83e\udd16 Analizzo\u2026</em>';

    // Disable the Spiega button to prevent double calls
    const explainBtn = document.querySelector('#alert-banner .btn-banner-ai');
    if (explainBtn) explainBtn.disabled = true;

    try {
        const result = await api('gemini_anomaly_explain', {}, 'POST', {
            name:         an.name,
            inv_qty:      an.inv_qty,
            expected_qty: an.expected_qty,
            diff:         an.diff,
            direction:    an.direction,
            unit:         an.unit,
            lang:         _currentLang,
        });

        if (result.success && result.explanation) {
            detailEl.innerHTML = `<span style="font-size:0.85rem">\ud83e\udd16 ${escapeHtml(result.explanation)}</span>`;
        } else {
            detailEl.innerHTML = originalHtml;
            showToast('Impossibile ottenere spiegazione AI', 'error');
        }
    } catch (e) {
        detailEl.innerHTML = originalHtml;
        showToast('Errore AI', 'error');
    }
}

function editBannerAnomaly() {
    const entry = _bannerQueue[_bannerIndex];
    if (!entry || entry.type !== 'anomaly') return;
    _bannerEditPending = true;
    editReviewItem(entry.data.inventory_id, entry.data.product_id);
}

function dismissBannerAnomaly() {
    const entry = _bannerQueue[_bannerIndex];
    if (!entry || entry.type !== 'anomaly') return;
    const key = entry.data.dismiss_key;
    setReviewConfirmed('an_' + key);
    api('dismiss_anomaly', {}, 'POST', { dismiss_key: key }).catch(() => {});
    showToast('Anomalia ignorata', 'info');
    dismissBannerItem();
}

function weighBannerItem() {
    const entry = _bannerQueue[_bannerIndex];
    if (!entry) return;
    _bannerEditPending = true;
    const item = entry.data;
    const targetId = entry.type === 'prediction' ? item.inventory_id : item.id;
    // Navigate to edit form and auto-start scale reading
    api('inventory_list').then(data => {
        currentInventory = data.inventory || [];
        editInventoryItem(targetId);
        setTimeout(() => readScaleForEdit(), 200);
    });
}

function editReviewItem(inventoryId, productId) {
    api('inventory_list').then(data => {
        currentInventory = data.inventory || [];
        editInventoryItem(inventoryId);
    });
}

// --- Banner handlers for expired & expiring ---
function bannerQuickUse() {
    const entry = _bannerQueue[_bannerIndex];
    if (!entry) return;
    const item = entry.data;
    quickUse(item.product_id, item.location);
    dismissBannerItem();
}

function bannerThrowAway() {
    const entry = _bannerQueue[_bannerIndex];
    if (!entry) return;
    const item = entry.data;
    api('inventory_use', {}, 'POST', {
        product_id: item.product_id,
        quantity: item.quantity,
        location: item.location,
        use_all: true,
        notes: 'Buttato'
    }).then(res => {
        if (res.success) {
            showToast(t('toast.thrown_away', { name: item.name }), 'success');
            loadDashboard();
        }
    }).catch(() => showToast(t('error.connection'), 'error'));
    dismissBannerItem();
}

function bannerFinishAll() {
    const entry = _bannerQueue[_bannerIndex];
    if (!entry) return;
    const item = entry.data;
    dismissBannerItem();
    api('inventory_use', {}, 'POST', {
        product_id: item.product_id,
        use_all: true,
        location: '__all__',
    }).then(res => {
        if (res.success) {
            showToast(`📤 ${item.name} terminato!`, 'success');
            showLowStockBringPrompt(res, () => loadDashboard());
        } else {
            showToast(res.error || 'Errore', 'error');
        }
    }).catch(() => showToast(t('error.connection'), 'error'));
}

function editBannerExpiry() {
    const entry = _bannerQueue[_bannerIndex];
    if (!entry || (entry.type !== 'expired' && entry.type !== 'expiring')) return;
    _bannerEditPending = true;
    editReviewItem(entry.data.id, entry.data.product_id);
}

function dismissBannerExpired() {
    const entry = _bannerQueue[_bannerIndex];
    if (!entry || entry.type !== 'expired') return;
    setReviewConfirmed('exp_' + entry.data.id);
    dismissBannerItem();
}

function dismissBannerExpiring() {
    const entry = _bannerQueue[_bannerIndex];
    if (!entry || entry.type !== 'expiring') return;
    setReviewConfirmed('exps_' + entry.data.id);
    dismissBannerItem();
}

async function confirmBannerFinished() {
    const entry = _bannerQueue[_bannerIndex];
    if (!entry || entry.type !== 'finished') return;
    const productId = entry.data.product_id;
    try {
        await api('inventory_confirm_finished', {}, 'POST', { product_id: productId });
    } catch(e) {}
    setReviewConfirmed('fin_' + productId);
    showToast(t('toast.product_finished_confirmed'), 'success');
    dismissBannerItem();
}

async function notFinishedBannerAction() {
    const entry = _bannerQueue[_bannerIndex];
    if (!entry || entry.type !== 'finished') return;
    const productId = entry.data.product_id;
    // Remove from this session's queue (will re-appear next load if still at qty=0)
    dismissBannerItem();
    showLoading(true);
    try {
        const data = await api('product_get', { id: productId });
        showLoading(false);
        if (data.product) {
            currentProduct = data.product;
            showAddForm();
        } else {
            showToast(t('error.not_found'), 'error');
        }
    } catch(e) {
        showLoading(false);
        showToast(t('error.connection'), 'error');
    }
}

// --- Banner swipe navigation ---
let _bannerTouchStartX = 0;
let _bannerTouchStartY = 0;
let _bannerSwiping = false;

function initBannerSwipe() {
    const banner = document.getElementById('alert-banner');
    if (!banner || banner._swipeInit) return;
    banner._swipeInit = true;

    banner.addEventListener('touchstart', e => {
        if (_bannerQueue.length <= 1) return;
        const touch = e.touches[0];
        _bannerTouchStartX = touch.clientX;
        _bannerTouchStartY = touch.clientY;
        _bannerSwiping = true;
    }, { passive: true });

    banner.addEventListener('touchend', e => {
        if (!_bannerSwiping || _bannerQueue.length <= 1) return;
        _bannerSwiping = false;
        const touch = e.changedTouches[0];
        const dx = touch.clientX - _bannerTouchStartX;
        const dy = touch.clientY - _bannerTouchStartY;
        // Only horizontal swipes (at least 40px, and more horizontal than vertical)
        if (Math.abs(dx) < 40 || Math.abs(dy) > Math.abs(dx)) return;
        if (dx < 0) bannerNext();
        else bannerPrev();
    }, { passive: true });
}

function bannerNext() {
    if (_bannerQueue.length <= 1) return;
    const banner = document.getElementById('alert-banner');
    banner.classList.remove('banner-slide-left', 'banner-slide-right');
    void banner.offsetWidth; // force reflow
    _bannerIndex = (_bannerIndex + 1) % _bannerQueue.length;
    banner.classList.add('banner-slide-left');
    renderBannerItem();
}

function bannerPrev() {
    if (_bannerQueue.length <= 1) return;
    const banner = document.getElementById('alert-banner');
    banner.classList.remove('banner-slide-left', 'banner-slide-right');
    void banner.offsetWidth;
    _bannerIndex = (_bannerIndex - 1 + _bannerQueue.length) % _bannerQueue.length;
    banner.classList.add('banner-slide-right');
    renderBannerItem();
}

// Group items by local category and render with category headers
function renderGroupedByCategory(items, compact = false) {
    const catGroups = {};
    items.forEach(item => {
        const localCat = mapToLocalCategory(item.category, item.name);
        if (!catGroups[localCat]) catGroups[localCat] = [];
        catGroups[localCat].push(item);
    });
    
    // Sort categories: use CATEGORY_ICONS key order
    const catOrder = Object.keys(CATEGORY_ICONS);
    const sortedCats = Object.keys(catGroups).sort((a, b) => {
        const ia = catOrder.indexOf(a);
        const ib = catOrder.indexOf(b);
        return (ia === -1 ? 999 : ia) - (ib === -1 ? 999 : ib);
    });
    
    let html = '';
    for (const cat of sortedCats) {
        const catItems = catGroups[cat];
        const label = CATEGORY_LABELS[cat] || '📦 Altro';
        html += `<div class="cat-group-header">${label} <span class="cat-group-count">${catItems.length}</span></div>`;
        html += catItems.map(item => compact ? renderDashItem(item) : renderInventoryItem(item)).join('');
    }
    return html;
}

function renderDashItem(item) {
    const catIcon = CATEGORY_ICONS[mapToLocalCategory(item.category, item.name)] || '📦';
    const days = daysUntilExpiry(item.expiry_date);
    const isExpired = days < 0;
    const isExpiring = !isExpired && days <= 7;
    const parts = formatQuantityParts(item.quantity, item.unit, item.default_quantity, item.package_unit);
    
    let expiryLabel = '';
    if (item.expiry_date) {
        if (days < 0) expiryLabel = t('expiry.badge_expired_ago').replace('{n}', Math.abs(days));
        else if (days === 0) expiryLabel = t('expiry.badge_today');
        else if (days === 1) expiryLabel = t('expiry.badge_tomorrow_long');
        else if (days <= 7) expiryLabel = t('expiry.badge_days').replace('{n}', days);
        else expiryLabel = formatDate(item.expiry_date);
    }
    
    return `
    <div class="inventory-item compact-item" onclick="dashItemTap(${item.id}, ${item.product_id})">
        <div class="inv-image">
            ${item.image_url ? `<img src="${escapeHtml(item.image_url)}" alt="" onerror="this.parentElement.innerHTML='${catIcon}'">` : catIcon}
        </div>
        <div class="inv-info">
            <div class="inv-name">${escapeHtml(item.name)}</div>
            ${item.brand ? `<div class="inv-brand">${escapeHtml(item.brand)}</div>` : ''}
        </div>
        <div class="inv-qty-right">
            <span class="inv-qty-value">${parts.mainQty} <small>${parts.unitLabel}</small></span>
            ${parts.packageDetail ? `<span class="inv-qty-pkg-detail">${parts.packageDetail}</span>` : ''}
            ${expiryLabel ? `<span class="inv-expiry-small ${isExpired ? 'expired' : isExpiring ? 'expiring' : ''}">${expiryLabel}</span>` : ''}
        </div>
    </div>`;
}

function dashItemTap(inventoryId, productId) {
    // Load full inventory so modal works
    api('inventory_list').then(data => {
        currentInventory = data.inventory || [];
        showItemDetail(inventoryId, productId);
    });
}

function showAlertItemDetail(inventoryId, productId) {
    // Load full inventory so modal works (same pattern as dashItemTap)
    api('inventory_list').then(data => {
        currentInventory = data.inventory || [];
        showItemDetail(inventoryId, productId);
    });
}

function formatSubRemainder(amt, pkgUnit) {
    const uL = { 'g': 'g', 'ml': 'ml' };
    if (pkgUnit === 'ml' || pkgUnit === 'g') return `${Math.round(amt)}${uL[pkgUnit] || pkgUnit}`;
    return `${Math.round(amt * 10) / 10}${uL[pkgUnit] || pkgUnit}`;
}

function _pzFractionLabel(n) {
    const whole = Math.floor(n);
    const frac = Math.round((n - whole) * 4) / 4; // nearest quarter
    const fracMap = { 0.25: '¼', 0.5: '½', 0.75: '¾' };
    const fracStr = fracMap[frac] || '';
    if (whole === 0) return fracStr || '0';
    return `${whole}${fracStr}`;
}

function formatQuantity(qty, unit, defaultQty, packageUnit) {
    if (!qty && qty !== 0) return '';
    const n = parseFloat(qty);
    const unitLabels = { 'pz': 'pz', 'g': 'g', 'ml': 'ml', 'conf': 'conf' };
    const label = unitLabels[unit] || unit || 'pz';

    // Special handling for conf with partial packages
    if (unit === 'conf' && packageUnit && defaultQty > 0) {
        const pkgLabel = unitLabels[packageUnit] || packageUnit;
        const wholeConf = Math.floor(n + 0.001);
        const fractionalConf = Math.round((n - wholeConf) * 1000) / 1000;

        if (fractionalConf < 0.01) {
            return `${wholeConf} conf <span class="conf-size-info">(da ${defaultQty}${pkgLabel})</span>`;
        }
        const remainderText = formatSubRemainder(fractionalConf * defaultQty, packageUnit);
        if (wholeConf > 0) {
            return `${wholeConf} conf <span class="conf-size-info">(da ${defaultQty}${pkgLabel})</span> + ${remainderText}`;
        }
        return remainderText;
    }

    let result;
    if (n === Math.floor(n)) result = `${Math.floor(n)} ${label}`;
    else if (unit === 'pz') result = `${_pzFractionLabel(n)} ${label}`;
    else result = `${n.toFixed(1)} ${label}`;
    return result;
}

// Structured quantity display for inventory cards.
// Returns { mainQty: '10', unitLabel: 'conf', packageDetail: 'da 36g', fraction: '¼' }
function formatQuantityParts(qty, unit, defaultQty, packageUnit) {
    const n = parseFloat(qty) || 0;
    const unitLabels = { 'pz': 'pz', 'g': 'g', 'ml': 'ml', 'conf': 'conf' };
    const label = unitLabels[unit] || unit || 'pz';

    // Special handling for conf with partial packages
    if (unit === 'conf' && packageUnit && defaultQty > 0) {
        const pkgLabel = unitLabels[packageUnit] || packageUnit;
        const wholeConf = Math.floor(n + 0.001);
        const fractionalConf = Math.round((n - wholeConf) * 1000) / 1000;

        if (fractionalConf < 0.01) {
            return { mainQty: `${wholeConf}`, unitLabel: 'conf', packageDetail: `da ${defaultQty}${pkgLabel}`, fraction: '' };
        }
        const remainderText = formatSubRemainder(fractionalConf * defaultQty, packageUnit);
        if (wholeConf > 0) {
            return { mainQty: `${wholeConf}`, unitLabel: 'conf', packageDetail: `da ${defaultQty}${pkgLabel}`, fraction: `+ ${remainderText}` };
        }
        return { mainQty: remainderText, unitLabel: '', packageDetail: '', fraction: '' };
    }

    let mainQty;
    if (n === Math.floor(n)) mainQty = `${Math.floor(n)}`;
    else if (unit === 'pz') mainQty = _pzFractionLabel(n);
    else mainQty = `${n.toFixed(1)}`;
    
    let packageDetail = '';
    let fraction = '';
    if (unit !== 'conf' && defaultQty && defaultQty > 1) {
        const d = parseFloat(defaultQty);
        const ratio = n / d;
        const remainder = ratio - Math.floor(ratio);
        if (remainder >= 0.1 && remainder <= 0.9) {
            if (remainder < 0.38) fraction = '¼';
            else if (remainder < 0.62) fraction = '½';
            else fraction = '¾';
        }
    }
    
    return { mainQty, unitLabel: label, packageDetail, fraction };
}

// Show package fraction: only ¼, ½, ¾ when there's a partial package.
// Returns '' if quantity maps to whole packages or fraction is not meaningful.
function formatPackageFraction(qty, defaultQty) {
    if (!defaultQty || defaultQty <= 0) return '';
    const n = parseFloat(qty);
    const d = parseFloat(defaultQty);
    if (isNaN(n) || isNaN(d) || d <= 0 || d === 1) return '';
    
    const ratio = n / d;
    const remainder = ratio - Math.floor(ratio);
    
    // Only show if there IS a fractional part
    if (remainder < 0.1 || remainder > 0.9) return '';
    
    let frac = '';
    if (remainder < 0.38) frac = '¼';
    else if (remainder < 0.62) frac = '½';
    else frac = '¾';
    
    return `<span class="pkg-fraction">${frac}</span>`;
}

// ===== INVENTORY =====
async function loadInventory() {
    try {
        const data = await api('inventory_list', currentLocation ? { location: currentLocation } : {});
        currentInventory = data.inventory || [];
        renderInventory(currentInventory);
        loadQuickAccess();
    } catch (err) {
        console.error('Inventory load error:', err);
    }
}

function renderInventoryItem(item) {
    const catIcon = CATEGORY_ICONS[mapToLocalCategory(item.category, item.name)] || '📦';
    const locInfo = LOCATIONS[item.location] || { icon: '📦', label: item.location };
    const days = daysUntilExpiry(item.expiry_date);
    const isExpired = days < 0;
    const isExpiring = !isExpired && days <= 7;
    const parts = formatQuantityParts(item.quantity, item.unit, item.default_quantity, item.package_unit);
    
    let expiryBadge = '';
    if (item.expiry_date) {
        let expiryText;
        if (isExpired) expiryText = t('expiry.badge_expired_ago').replace('{n}', Math.abs(days));
        else if (days === 0) expiryText = t('expiry.badge_today');
        else if (days === 1) expiryText = t('expiry.badge_tomorrow');
        else if (days <= 7) expiryText = t('expiry.badge_days').replace('{n}', days);
        else expiryText = formatDate(item.expiry_date);
        expiryBadge = `<span class="inv-badge ${isExpired ? 'badge-expired' : isExpiring ? 'badge-expiry' : ''}">${expiryText}</span>`;
    }
    
    const vacuumBadge = item.vacuum_sealed ? `<span class="vacuum-badge">${t('inventory.vacuum_badge')}</span>` : '';
    const openedBadge = item.opened_at ? `<span class="opened-badge">${t('inventory.opened_badge')}</span>` : '';
    
    return `
    <div class="inventory-item" onclick="showItemDetail(${item.id}, ${item.product_id})">
        <div class="inv-image">
            ${item.image_url ? `<img src="${escapeHtml(item.image_url)}" alt="" onerror="this.parentElement.innerHTML='${catIcon}'">` : catIcon}
        </div>
        <div class="inv-info">
            <div class="inv-name">${escapeHtml(item.name)}</div>
            ${item.brand ? `<div class="inv-brand">${escapeHtml(item.brand)}</div>` : ''}
            <div class="inv-meta">
                <span class="inv-badge badge-location">${locInfo.icon} ${locInfo.label}</span>
                ${expiryBadge}
                ${openedBadge}
                ${vacuumBadge}
            </div>
        </div>
        <div class="inv-qty-col">
            <span class="inv-qty-number">${parts.mainQty}</span>
            <span class="inv-qty-unit">${parts.unitLabel}${parts.packageDetail ? ` <span class="inv-qty-pkg">${parts.packageDetail}</span>` : ''}</span>
            ${parts.fraction ? `<span class="inv-qty-frac">${parts.fraction}</span>` : ''}
        </div>
    </div>`;
}

function renderInventory(items) {
    const container = document.getElementById('inventory-list');
    if (items.length === 0) {
        container.innerHTML = `<div class="empty-state"><div class="empty-state-icon">📭</div><p>${t('inventory.empty_text')}</p></div>`;
        return;
    }
    container.innerHTML = renderGroupedByCategory(items, false);
}

function filterLocation(loc) {
    currentLocation = loc;
    document.querySelectorAll('.location-tabs .tab').forEach(t => {
        t.classList.toggle('active', t.dataset.loc === loc);
    });
    loadInventory();
}

function filterInventory() {
    const q = document.getElementById('inventory-search').value.toLowerCase().trim();
    const qas = document.getElementById('quick-access-section');
    if (!q) {
        if (qas) qas.style.display = '';
        renderInventory(currentInventory);
        return;
    }
    if (qas) qas.style.display = 'none';
    const filtered = currentInventory.filter(i =>
        i.name.toLowerCase().includes(q) ||
        (i.brand && i.brand.toLowerCase().includes(q)) ||
        (i.barcode && i.barcode.includes(q))
    );
    renderInventory(filtered);
}

// ===== QUICK ACCESS: RECENT & POPULAR =====
async function loadQuickAccess() {
    const section = document.getElementById('quick-access-section');
    if (!section) return;
    try {
        const data = await api('recent_popular_products');
        const recent = data.recent || [];
        const popular = data.popular || [];
        const recentIds = data.recent_ids || [];

        const recentGroup = document.getElementById('quick-recent-group');
        const popularGroup = document.getElementById('quick-popular-group');
        const recentGrid = document.getElementById('quick-recent-grid');
        const popularGrid = document.getElementById('quick-popular-grid');

        // Render recent (max 4)
        if (recent.length > 0) {
            recentGrid.innerHTML = recent.slice(0, 4).map(p => renderQuickAccessBtn(p)).join('');
            recentGroup.style.display = '';
        } else {
            recentGroup.style.display = 'none';
        }

        // Render popular (max 8), excluding products already in recent
        const filteredPopular = popular.filter(p => !recentIds.includes(parseInt(p.product_id)));
        if (filteredPopular.length > 0) {
            popularGrid.innerHTML = filteredPopular.slice(0, 8).map(p => renderQuickAccessBtn(p)).join('');
            popularGroup.style.display = '';
        } else {
            popularGroup.style.display = 'none';
        }

        section.style.display = (recent.length > 0 || filteredPopular.length > 0) ? '' : 'none';
    } catch (e) {
        console.warn('[QuickAccess] load failed:', e);
        section.style.display = 'none';
    }
}

function renderQuickAccessBtn(product) {
    const catIcon = CATEGORY_ICONS[mapToLocalCategory(product.category, product.name)] || '📦';
    const imgHtml = product.image_url
        ? `<img src="${escapeHtml(product.image_url)}" alt="" onerror="this.parentElement.innerHTML='${catIcon}'">`
        : catIcon;
    const brandHtml = product.brand ? `<span class="qa-brand">(${escapeHtml(product.brand)})</span>` : '';
    return `
    <button class="quick-access-btn" onclick="quickAccessSelect(${product.product_id})">
        <div class="qa-img">${imgHtml}</div>
        <div class="qa-name">${escapeHtml(product.name)}</div>
        ${brandHtml}
    </button>`;
}

function quickAccessSelect(productId) {
    // Find the product in current inventory and show its detail
    const item = currentInventory.find(i => i.product_id === productId);
    if (item) {
        showItemDetail(item.id, item.product_id);
    } else {
        // Product not in current view (maybe different location), navigate to it
        quickUse(productId, currentLocation || 'dispensa');
    }
}

// ===== ITEM DETAIL MODAL =====
function showItemDetail(inventoryId, productId) {
    const item = currentInventory.find(i => i.id === inventoryId);
    if (!item) return;
    
    const locInfo = LOCATIONS[item.location] || { icon: '📦', label: item.location };
    const catIcon = CATEGORY_ICONS[mapToLocalCategory(item.category, item.name)] || '📦';
    
    document.getElementById('modal-content').innerHTML = `
        <div class="modal-header">
            <h3>${escapeHtml(item.name)}</h3>
            <button class="modal-close" onclick="closeModal()">✕</button>
        </div>
        <div class="product-preview-small" style="margin-bottom:12px">
            ${item.image_url ?
                `<img src="${escapeHtml(item.image_url)}" alt="" style="width:60px;height:60px;border-radius:10px;object-fit:cover">` :
                `<span style="font-size:2.5rem">${catIcon}</span>`
            }
            <div class="product-preview-info">
                <h3>${escapeHtml(item.name)}</h3>
                <p>${item.brand ? escapeHtml(item.brand) : ''}</p>
            </div>
        </div>
        <div class="modal-detail">
            <div class="modal-detail-row">
                <span class="modal-detail-label">${t('inventory.label_position')}</span>
                <span class="modal-detail-value">${locInfo.icon} ${locInfo.label}</span>
            </div>
            <div class="modal-detail-row">
                <span class="modal-detail-label">${t('inventory.label_quantity')}</span>
                <span class="modal-detail-value">${formatQuantity(item.quantity, item.unit, item.default_quantity, item.package_unit)}</span>
            </div>
            ${item.expiry_date ? `
            <div class="modal-detail-row">
                <span class="modal-detail-label">${t('inventory.label_expiry')}</span>
                <span class="modal-detail-value">${formatDate(item.expiry_date)}</span>
            </div>` : ''}
            ${item.vacuum_sealed ? `
            <div class="modal-detail-row">
                <span class="modal-detail-label">${t('inventory.label_storage')}</span>
                <span class="modal-detail-value">${t('inventory.vacuum_badge')}</span>
            </div>` : ''}
            ${item.opened_at ? `
            <div class="modal-detail-row">
                <span class="modal-detail-label">${t('inventory.label_status')}</span>
                <span class="modal-detail-value">${t('inventory.opened_since').replace('{date}', formatDateTime(item.opened_at))}</span>
            </div>` : ''}
            ${item.barcode ? `
            <div class="modal-detail-row">
                <span class="modal-detail-label">🔖 Barcode</span>
                <span class="modal-detail-value">${item.barcode}</span>
            </div>` : ''}
            <div class="modal-detail-row">
                <span class="modal-detail-label">${t('inventory.label_added')}</span>
                <span class="modal-detail-value">${formatDateTime(item.added_at)}</span>
            </div>
        </div>
        <div class="modal-actions">
            <button class="btn btn-danger flex-1" onclick="quickUse(${item.product_id}, '${item.location}')">📤 Usa</button>
            <button class="btn btn-primary flex-1" onclick="editInventoryItem(${inventoryId})">✏️ Modifica</button>
            <button class="btn btn-secondary" onclick="deleteInventoryItem(${inventoryId})" style="padding:12px">🗑️</button>
        </div>
    `;
    document.getElementById('modal-overlay').style.display = 'flex';
}

function closeModal() {
    document.getElementById('modal-overlay').style.display = 'none';
    _cancelScaleAutoConfirm(false);
    _scaleRecipeAutoFillPaused = false;
    _scaleUserDismissed = false;
    _scaleWeightCallback = null;
    _bannerEditPending = false;
}

async function quickUse(productId, location) {
    closeModal();
    showLoading(true);
    try {
        currentProduct = { id: productId };
        // Get product info
        const data = await api('product_get', { id: productId });
        if (data.product) {
            currentProduct = data.product;
            // Extract weight_info from notes if available
            if (!currentProduct.weight_info && currentProduct.notes) {
                const pesoMatch = currentProduct.notes.match(/Peso:\s*([^·]+)/);
                if (pesoMatch) currentProduct.weight_info = pesoMatch[1].trim();
            }
        }
        document.getElementById('use-location').value = location;
        // Mark active location button
        document.querySelectorAll('#page-use .loc-btn').forEach(b => b.classList.remove('active'));
        const locBtns = document.querySelectorAll('#page-use .loc-btn');
        locBtns.forEach(b => {
            if (b.textContent.toLowerCase().includes(location)) b.classList.add('active');
        });
        
        renderUsePreview();

        // Reset scale state so the stale weight already on the scale doesn't
        // immediately trigger an auto-fill. Only a weight *change* (≥10 g) after
        // the page opens should be treated as a new product being placed.
        _cancelScaleAutoConfirm(false); // stops timers, clears _scaleStabilityVal & _scaleLastConfirmedGrams
        if (_scaleLatestWeight) {
            const _baselineG = _scaleToGrams(parseFloat(_scaleLatestWeight.value), _scaleLatestWeight.unit);
            if (_baselineG !== null && _baselineG >= 10) _scaleLastConfirmedGrams = _baselineG;
            _scaleLatestWeight = null; // prevent immediate call inside loadUseInventoryInfo
        }

        loadUseInventoryInfo();
        showLoading(false);
        showPage('use');
    } catch (err) {
        showLoading(false);
        console.error('quickUse error:', err);
        showToast(t('error.loading'), 'error');
    }
}

async function deleteInventoryItem(id) {
    if (confirm(t('confirm.remove_item'))) {
        await api('inventory_delete', {}, 'POST', { id });
        closeModal();
        showToast(t('toast.product_removed'), 'success');
        refreshCurrentPage();
    }
}

function recalcEditExpiry(locInputId, vacuumInputId, expiryInputId) {
    const product = window._editingProduct;
    if (!product) return;
    const loc = document.getElementById(locInputId)?.value || '';
    const isVacuum = document.getElementById(vacuumInputId)?.checked;
    // Use opened shelf life if item is already opened
    let days = product._isOpened
        ? estimateOpenedExpiryDays(product, loc)
        : estimateExpiryDays(product, loc);
    if (isVacuum) days = getVacuumExpiryDays(days);
    const newDate = addDays(days);
    const expiryInput = document.getElementById(expiryInputId);
    if (expiryInput) expiryInput.value = newDate;
}

function editInventoryItem(id) {
    const item = currentInventory.find(i => i.id === id);
    if (!item) {
        closeModal();
        showToast(t('error.not_found'), 'error');
        return;
    }
    
    const isConf = (item.unit || 'pz') === 'conf';
    const confSizeVal = (isConf && item.default_quantity > 0) ? item.default_quantity : '';
    const confUnitVal = (isConf && item.package_unit) ? item.package_unit : 'g';
    
    // Determine if scale is available for this item's unit
    const s = getSettings();
    const effectiveUnit = isConf ? (item.package_unit || 'g') : (item.unit || 'pz');
    const scaleEditReady = s.scale_enabled && s.scale_gateway_url && _scaleConnected &&
        (effectiveUnit === 'g' || effectiveUnit === 'ml');
    
    window._editingProduct = { name: item.name, category: item.category || '', _isOpened: !!item.opened_at };
    
    // Rebuild modal content for editing (don't close and reopen - just replace content)
    document.getElementById('modal-content').innerHTML = `
        <div class="modal-header">
            <h3>Modifica ${escapeHtml(item.name)}</h3>
            <button class="modal-close" onclick="closeModal()">✕</button>
        </div>
        <form class="form" onsubmit="submitEditInventory(event, ${id}, ${item.product_id})">
            <div class="form-group">
                <label>📦 ${t('inventory.label_quantity').replace('📦 ', '')}</label>
                <div class="qty-control">
                    <button type="button" class="qty-btn" onclick="adjustQty('edit-qty', -1)">−</button>
                    <input type="number" id="edit-qty" value="${item.quantity}" min="0" step="any" class="qty-input">
                    <button type="button" class="qty-btn" onclick="adjustQty('edit-qty', 1)">+</button>
                </div>
                ${scaleEditReady ? `
                <div id="edit-scale-section" style="display:none;text-align:center;padding:10px;background:linear-gradient(135deg,#f3e8ff,#ede9fe);border-radius:10px;margin-top:8px">
                    <div style="font-size:1.8rem;font-weight:bold;color:#5b21b6" id="edit-scale-reading">— — —</div>
                    <div style="font-size:0.78rem;color:#7c6cb0;margin-top:2px">${t('scale.place_on_scale')}</div>
                </div>
                <button type="button" id="btn-scale-edit" class="btn btn-secondary scale-read-btn" style="margin-top:8px;width:100%"
                    onclick="readScaleForEdit()">⚖️ ${t('scale.read_btn')}</button>
                ` : ''}
            </div>
            <div class="form-group">
                <label>📏 Unità di misura</label>
                <select id="edit-unit" class="form-input" onchange="onEditUnitChange()">
                    ${['pz','g','ml','conf'].map(u => `<option value="${u}" ${(item.unit||'pz') === u ? 'selected' : ''}>${u === 'pz' ? 'pz (pezzi)' : u === 'g' ? 'g (grammi)' : u === 'ml' ? 'ml (millilitri)' : u === 'conf' ? 'conf (confezioni)' : u}</option>`).join('')}
                </select>
            </div>
            <div class="form-group" id="edit-conf-size-group" style="display:${isConf ? 'block' : 'none'}">
                <label>📦 Ogni confezione contiene:</label>
                <div class="conf-size-inputs">
                    <input type="number" id="edit-conf-size" class="form-input conf-size-input" min="1" step="any" value="${confSizeVal}" placeholder="es. 300">
                    <select id="edit-conf-unit" class="form-input conf-size-unit">
                        ${['g','ml'].map(u => `<option value="${u}" ${confUnitVal === u ? 'selected' : ''}>${u}</option>`).join('')}
                    </select>
                </div>
            </div>
            <div class="form-group">
                <label>${t('inventory.label_position')}</label>
                <div class="location-selector">
                    ${Object.entries(LOCATIONS).map(([k, v]) => `
                        <button type="button" class="loc-btn ${item.location === k ? 'active' : ''}" 
                            onclick="this.parentElement.querySelectorAll('.loc-btn').forEach(b=>b.classList.remove('active'));this.classList.add('active');document.getElementById('edit-loc').value='${k}';recalcEditExpiry('edit-loc','edit-vacuum','edit-expiry')">${v.icon} ${v.label}</button>
                    `).join('')}
                </div>
                <input type="hidden" id="edit-loc" value="${item.location}">
            </div>
            <div class="form-group">
                <label>${t('inventory.label_expiry')}</label>
                <input type="date" id="edit-expiry" value="${item.expiry_date || ''}" class="form-input">
            </div>
            <div class="form-group">
                <label class="toggle-row">
                    ${t('add.vacuum_label')}
                    <span class="toggle-switch">
                        <input type="checkbox" id="edit-vacuum" ${item.vacuum_sealed ? 'checked' : ''} onchange="recalcEditExpiry('edit-loc','edit-vacuum','edit-expiry')">
                        <span class="toggle-slider"></span>
                    </span>
                </label>
            </div>
            <button type="submit" class="btn btn-large btn-primary full-width">${t('btn.save')}</button>
        </form>
    `;
    document.getElementById('modal-overlay').style.display = 'flex';
}

function onEditUnitChange() {
    const unit = document.getElementById('edit-unit').value;
    const confGroup = document.getElementById('edit-conf-size-group');
    if (confGroup) confGroup.style.display = unit === 'conf' ? 'block' : 'none';
}

async function submitEditInventory(e, id, productId) {
    e.preventDefault();
    const qty = parseFloat(document.getElementById('edit-qty').value);
    const loc = document.getElementById('edit-loc').value;
    const expiry = document.getElementById('edit-expiry').value || null;
    const unit = document.getElementById('edit-unit').value;
    
    const payload = { id, quantity: qty, location: loc, expiry_date: expiry, unit, product_id: productId,
        vacuum_sealed: document.getElementById('edit-vacuum')?.checked ? 1 : 0 };
    
    // Add package info if conf
    if (unit === 'conf') {
        payload.package_unit = document.getElementById('edit-conf-unit')?.value || '';
        payload.package_size = parseFloat(document.getElementById('edit-conf-size')?.value) || 0;
    } else {
        // Clear package info if not conf
        payload.package_unit = '';
        payload.package_size = 0;
    }
    
    await api('inventory_update', {}, 'POST', payload);
    closeModal();
    showToast('Aggiornato!', 'success');
    if (_bannerEditPending) {
        _bannerEditPending = false;
        // Mark the item as confirmed so it does NOT reappear in the banner
        const entry = _bannerQueue[_bannerIndex];
        if (entry) {
            if (entry.type === 'review') setReviewConfirmed(entry.data.id);
            else if (entry.type === 'prediction') setReviewConfirmed('pred_' + entry.data.inventory_id);
            else if (entry.type === 'expired') setReviewConfirmed('exp_' + entry.data.id);
            else if (entry.type === 'expiring') setReviewConfirmed('exps_' + entry.data.id);
        }
        dismissBannerItem();
    }
    refreshCurrentPage();
}

// ===== SCAN DEBUG LOG =====
let _scanDebugVisible = false;
let _scanLogBuffer = [];
let _scanLogTimer = null;

function scanLog(msg) {
    const el = document.getElementById('scan-debug-log');
    if (el) {
        const _scanLocale = _currentLang === 'de' ? 'de-DE' : _currentLang === 'en' ? 'en-GB' : 'it-IT';
        const ts = new Date().toLocaleTimeString(_scanLocale, {hour:'2-digit',minute:'2-digit',second:'2-digit',fractionalSecondDigits:1});
        el.textContent += `[${ts}] ${msg}\n`;
        el.scrollTop = el.scrollHeight;
    }
    console.log('[ScanDebug]', msg);
    // Buffer for remote send
    _scanLogBuffer.push(msg);
    if (!_scanLogTimer) {
        _scanLogTimer = setTimeout(flushScanLog, 2000);
    }
}

function flushScanLog() {
    _scanLogTimer = null;
    if (_scanLogBuffer.length === 0) return;
    const msgs = _scanLogBuffer.splice(0).map(m => `[SCAN] ${m}`);
    _remoteLogBuffer.push(...msgs);
    if (!_remoteLogTimer) {
        _remoteLogTimer = setTimeout(flushRemoteLog, 2000);
    }
}

function toggleScanDebug() {
    const el = document.getElementById('scan-debug-log');
    if (!el) return;
    _scanDebugVisible = !_scanDebugVisible;
    el.style.display = _scanDebugVisible ? 'block' : 'none';
}

// ===== BARCODE SCANNER =====
let _useBarcodeDetector = ('BarcodeDetector' in window);

async function initScanner() {
    const video = document.getElementById('scanner-video');
    const viewport = document.getElementById('scanner-viewport');
    const logEl = document.getElementById('scan-debug-log');
    if (logEl) logEl.textContent = '';
    
    const constraints = getCameraConstraints();
    scanLog(`Camera mode: ${getSettings().camera_facing || 'environment'}`);
    scanLog(`BarcodeDetector: ${_useBarcodeDetector ? 'YES (native)' : 'NO (Quagga fallback)'}`);
    scanLog(`Constraints: ${JSON.stringify(constraints.video)}`);
    
    try {
        stopScanner();
        
        const stream = await navigator.mediaDevices.getUserMedia(constraints);
        const track = stream.getVideoTracks()[0];
        const caps = track.getSettings ? track.getSettings() : {};
        scanLog(`Stream OK — track: ${track.label}`);
        scanLog(`Resolution: ${caps.width||'?'}x${caps.height||'?'}, facing: ${caps.facingMode||'N/A'}`);
        
        scannerStream = stream;
        video.srcObject = stream;
        await video.play();
        scanLog(`Video playing — videoWidth: ${video.videoWidth}, videoHeight: ${video.videoHeight}`);
        
        if (_useBarcodeDetector) {
            startNativeScanner(video);
        } else {
            startQuaggaScanner(video);
        }
        
    } catch (err) {
        scanLog(`CAMERA ERROR: ${err.name}: ${err.message}`);
        console.error('Camera error:', err);
        document.getElementById('scan-result').style.display = 'block';
        document.getElementById('scan-result').innerHTML = `
            <p style="color: var(--danger)">${t('error.camera')}</p>
            <p style="font-size:0.85rem; color: var(--text-light); margin-top:8px">${t('scanner.camera_error_hint')}</p>
        `;
    }
}

// ===== NATIVE BarcodeDetector SCANNER =====
async function startNativeScanner(videoEl) {
    if (quaggaRunning) return;
    
    const scannerLine = document.querySelector('.scanner-line');
    const detector = new BarcodeDetector({
        formats: ['ean_13', 'ean_8', 'code_128', 'code_39', 'upc_a', 'upc_e']
    });
    
    let scanning = true;
    quaggaRunning = true;
    let frameCount = 0;
    let partialCount = 0;
    let lastDetected = '';
    let detectCount = 0;
    let detectionHistory = {};
    
    scanLog('Native BarcodeDetector started');
    
    function updateFeedback(state) {
        if (!scannerLine) return;
        scannerLine.classList.remove('scanning', 'detecting');
        if (state) scannerLine.classList.add(state);
    }
    
    async function scanFrame() {
        if (!scanning || !scannerStream) return;
        frameCount++;
        
        if (frameCount === 1) updateFeedback('scanning');
        
        try {
            const barcodes = await detector.detect(videoEl);
            
            if (barcodes.length > 0) {
                const code = barcodes[0].rawValue;
                const format = barcodes[0].format;
                partialCount++;
                scanLog(`Native detect #${partialCount} [f${frameCount}]: ${code} (${format})`);
                updateFeedback('detecting');
                
                if (!detectionHistory[code]) detectionHistory[code] = { count: 0 };
                detectionHistory[code].count++;
                
                if (code === lastDetected) {
                    detectCount++;
                } else {
                    lastDetected = code;
                    detectCount = 1;
                }
                
                if (detectCount >= 2 || detectionHistory[code].count >= 2) {
                    scanning = false;
                    quaggaRunning = false;
                    updateFeedback(null);
                    scanLog(`CONFIRMED: ${code} after ${frameCount} frames`);
                    onBarcodeDetected(code);
                    return;
                }
            } else {
                updateFeedback('scanning');
            }
        } catch (e) {
            scanLog(`Native detect error: ${e.message}`);
        }
        
        if (scanning) {
            if (frameCount % 30 === 0) {
                scanLog(`Native scanning... f${frameCount}, partials: ${partialCount}`);
            }
            requestAnimationFrame(scanFrame);
        }
    }
    
    requestAnimationFrame(scanFrame);
}

// ===== QUAGGA FALLBACK SCANNER =====
function startQuaggaScanner(videoEl) {
    if (quaggaRunning) return;
    
    const canvas = document.getElementById('scanner-canvas');
    const ctx = canvas.getContext('2d');
    const frontCam = isFrontCamera();
    const scannerLine = document.querySelector('.scanner-line');
    let frameCount = 0;
    let partialCount = 0;
    
    scanLog(`Quagga starting — frontCam: ${frontCam}`);
    
    let scanning = true;
    quaggaRunning = true;
    let lastDetected = '';
    let detectCount = 0;
    let detectionHistory = {};
    
    // Alternate between full frame and center-cropped for better detection
    let scanPass = 0; // 0=full, 1=center-crop, 2=full-enhanced, 3=center-enhanced
    
    function updateScannerFeedback(state) {
        if (!scannerLine) return;
        scannerLine.classList.remove('scanning', 'detecting');
        if (state) scannerLine.classList.add(state);
    }
    
    function getFrameDataUrl(pass) {
        const vw = videoEl.videoWidth;
        const vh = videoEl.videoHeight;
        
        if (pass % 2 === 0) {
            // Full frame
            canvas.width = vw;
            canvas.height = vh;
            ctx.drawImage(videoEl, 0, 0);
        } else {
            // Center crop: 60% of frame, focused on barcode area
            const cropW = Math.round(vw * 0.7);
            const cropH = Math.round(vh * 0.4);
            const sx = Math.round((vw - cropW) / 2);
            const sy = Math.round((vh - cropH) / 2);
            canvas.width = cropW;
            canvas.height = cropH;
            ctx.drawImage(videoEl, sx, sy, cropW, cropH, 0, 0, cropW, cropH);
        }
        
        // Apply enhancement on passes 2,3 or always for front cam
        if (frontCam || pass >= 2) {
            enhanceCanvasForBarcode(ctx, canvas.width, canvas.height);
        }
        
        return canvas.toDataURL('image/jpeg', 0.95);
    }
    
    function scanFrame() {
        if (!scanning || !scannerStream) return;
        frameCount++;
        scanPass = (scanPass + 1) % 4;
        
        const dataUrl = getFrameDataUrl(scanPass);
        
        if (frameCount === 1) {
            scanLog(`Frame #1 — video: ${videoEl.videoWidth}x${videoEl.videoHeight}`);
            updateScannerFeedback('scanning');
        }
        
        let callbackCalled = false;
        const safetyTimer = setTimeout(() => {
            if (!callbackCalled && scanning) {
                scanLog(`Quagga timeout on f${frameCount}, retrying...`);
                setTimeout(scanFrame, 100);
            }
        }, 5000);
        
        try {
            const imgSize = Math.max(canvas.width, canvas.height);
            Quagga.decodeSingle({
                src: dataUrl,
                numOfWorkers: 0,
                inputStream: { size: Math.min(imgSize, 800) },
                decoder: {
                    readers: [
                        'ean_reader',
                        'ean_8_reader',
                        'code_128_reader',
                        'code_39_reader',
                        'upc_reader',
                        'upc_e_reader'
                    ],
                    multiple: false
                },
                locate: true,
                locator: { patchSize: 'large', halfSample: false }
            }, function(result) {
                callbackCalled = true;
                clearTimeout(safetyTimer);
                if (result && result.codeResult) {
                    const code = result.codeResult.code;
                    const format = result.codeResult.format;
                    partialCount++;
                    const passName = ['full','crop','full+enh','crop+enh'][scanPass];
                    scanLog(`Partial #${partialCount} [f${frameCount} ${passName}]: ${code} (${format})`);
                    updateScannerFeedback('detecting');
                    
                    if (!detectionHistory[code]) detectionHistory[code] = { count: 0, lastFrame: 0 };
                    detectionHistory[code].count++;
                    detectionHistory[code].lastFrame = frameCount;
                    
                    if (code === lastDetected) {
                        detectCount++;
                    } else {
                        lastDetected = code;
                        detectCount = 1;
                    }
                    
                    const dominated = detectionHistory[code];
                    if (detectCount >= 2 || dominated.count >= 2) {
                        scanning = false;
                        quaggaRunning = false;
                        updateScannerFeedback(null);
                        scanLog(`CONFIRMED: ${code} after ${frameCount} frames (consec:${detectCount}, total:${dominated.count})`);
                        onBarcodeDetected(code);
                        return;
                    }
                } else {
                    updateScannerFeedback('scanning');
                }
                if (scanning) {
                    if (frameCount % 20 === 0) {
                        scanLog(`Scanning... f${frameCount}, partials: ${partialCount}, pass: ${scanPass}`);
                    }
                    setTimeout(scanFrame, 150);
                }
            });
        } catch (e) {
            callbackCalled = true;
            clearTimeout(safetyTimer);
            scanLog(`Quagga error: ${e.message}`);
            if (scanning) setTimeout(scanFrame, 500);
        }
    }
    
    setTimeout(scanFrame, 500);
}

// Enhance low-quality camera frames for better barcode recognition
function enhanceCanvasForBarcode(ctx, w, h) {
    const imageData = ctx.getImageData(0, 0, w, h);
    const d = imageData.data;
    // Convert to high-contrast grayscale
    for (let i = 0; i < d.length; i += 4) {
        // Luminance
        let gray = 0.299 * d[i] + 0.587 * d[i+1] + 0.114 * d[i+2];
        // Increase contrast
        gray = ((gray - 128) * 1.5) + 128;
        gray = gray < 0 ? 0 : gray > 255 ? 255 : gray;
        // Threshold to make bars more distinct
        gray = gray < 140 ? 0 : 255;
        d[i] = d[i+1] = d[i+2] = gray;
    }
    ctx.putImageData(imageData, 0, 0);
}

function stopScanner() {
    quaggaRunning = false;
    _scanZoomLevel = 1;
    if (scannerStream) {
        scannerStream.getTracks().forEach(t => t.stop());
        scannerStream = null;
    }
    const video = document.getElementById('scanner-video');
    if (video) video.srcObject = null;
    const zoomBtn = document.getElementById('scan-zoom-btn');
    if (zoomBtn) zoomBtn.textContent = 'x1';
    
    // Also stop AI camera
    if (aiStream) {
        aiStream.getTracks().forEach(t => t.stop());
        aiStream = null;
    }
    const aiVideo = document.getElementById('ai-video');
    if (aiVideo) aiVideo.srcObject = null;
}

async function onBarcodeDetected(barcode) {
    showLoading(true);
    
    // Vibrate if available
    if (navigator.vibrate) navigator.vibrate(100);
    
    try {
        // First check local DB
        const localResult = await api('search_barcode', { barcode });
        if (localResult.found) {
            currentProduct = localResult.product;
            // If product was saved with 'pz' but has weight info in notes, fix defaults
            if (currentProduct.unit === 'pz' && currentProduct.default_quantity <= 1 && currentProduct.notes) {
                const pesoMatch = currentProduct.notes.match(/Peso:\s*([^·]+)/);
                if (pesoMatch) {
                    const weightStr = pesoMatch[1].trim();
                    const detected = detectUnitAndQuantity(weightStr);
                    if (detected.unit !== 'pz') {
                        currentProduct.unit = detected.unit;
                        currentProduct.default_quantity = detected.quantity;
                        currentProduct.weight_info = weightStr;
                        if (detected.packageUnit) currentProduct.package_unit = detected.packageUnit;
                        if (detected.confCount) currentProduct._confCount = detected.confCount;
                        // Update product in DB for future scans
                        api('product_save', {}, 'POST', {
                            id: currentProduct.id,
                            barcode: currentProduct.barcode,
                            name: currentProduct.name,
                            brand: currentProduct.brand || '',
                            category: currentProduct.category || '',
                            image_url: currentProduct.image_url || '',
                            unit: detected.unit,
                            default_quantity: detected.quantity,
                            package_unit: detected.packageUnit || '',
                            notes: currentProduct.notes,
                        });
                    }
                }
            }
            // Extract weight_info from notes if available (stored as "Peso: 500 g · ...")
            if (!currentProduct.weight_info && currentProduct.notes) {
                const pesoMatch = currentProduct.notes.match(/Peso:\s*([^·]+)/);
                if (pesoMatch) currentProduct.weight_info = pesoMatch[1].trim();
            }
            // Detect confCount from weight_info for multipack pre-fill
            if (currentProduct.weight_info && currentProduct.unit === 'conf' && !currentProduct._confCount) {
                const detected = detectUnitAndQuantity(currentProduct.weight_info);
                if (detected.confCount) currentProduct._confCount = detected.confCount;
            }
            showLoading(false);
            stopScanner();
            showProductAction();
            return;
        }
        
        // Lookup in external DB
        const lookupResult = await api('lookup_barcode', { barcode });
        if (lookupResult.found && lookupResult.product) {
            const p = lookupResult.product;
            // Detect unit and quantity from quantity_info
            const detected = detectUnitAndQuantity(p.quantity_info);
            
            // Build rich notes with all available info
            const notesParts = [];
            if (p.quantity_info) notesParts.push(`Peso: ${p.quantity_info}`);
            if (p.nutriscore) notesParts.push(`Nutriscore: ${p.nutriscore.toUpperCase()}`);
            if (p.nova_group) notesParts.push(`NOVA: ${p.nova_group}`);
            if (p.ecoscore) notesParts.push(`Ecoscore: ${p.ecoscore.toUpperCase()}`);
            if (p.origin) notesParts.push(`Origine: ${p.origin}`);
            if (p.labels) notesParts.push(`Etichette: ${p.labels}`);
            
            // Save to local DB
            const saveResult = await api('product_save', {}, 'POST', {
                barcode: barcode,
                name: p.name || t('product.not_recognized'),
                brand: p.brand || '',
                category: p.category || '',
                image_url: p.image_url || '',
                unit: detected.unit,
                default_quantity: detected.quantity,
                package_unit: detected.packageUnit || '',
                notes: notesParts.join(' · '),
            });
            
            if (saveResult.id) {
                currentProduct = {
                    id: saveResult.id,
                    barcode: barcode,
                    name: p.name || t('product.not_recognized'),
                    brand: p.brand || '',
                    category: p.category || '',
                    image_url: p.image_url || '',
                    unit: detected.unit,
                    default_quantity: detected.quantity,
                    package_unit: detected.packageUnit || '',
                    _confCount: detected.confCount || 0,
                    weight_info: p.quantity_info || '',
                    nutriscore: p.nutriscore || '',
                    ingredients: p.ingredients || '',
                    allergens: p.allergens || '',
                    conservation: p.conservation || '',
                    origin: p.origin || '',
                    nova_group: p.nova_group || '',
                    ecoscore: p.ecoscore || '',
                    labels: p.labels || '',
                    stores: p.stores || '',
                };
                showLoading(false);
                stopScanner();
                showProductAction();
                return;
            }
        }
        
        // Not found - ask user to add manually
        showLoading(false);
        stopScanner();
        showToast(t('error.not_found_manual'), 'error');
        startManualEntry(barcode);
        
    } catch (err) {
        showLoading(false);
        console.error('Barcode lookup error:', err);
        showToast(t('error.search'), 'error');
    }
}

function submitManualBarcode() {
    const input = document.getElementById('manual-barcode-input');
    const barcode = (input.value || '').trim();
    if (!barcode) {
        showToast(t('error.barcode_empty'), 'error');
        input.focus();
        return;
    }
    if (!/^\d{4,14}$/.test(barcode)) {
        showToast(t('error.barcode_format'), 'error');
        input.focus();
        return;
    }
    stopScanner();
    onBarcodeDetected(barcode);
}

// ===== QUICK NAME ENTRY (for loose/unpackaged products) =====
async function submitQuickName() {
    const input = document.getElementById('quick-product-name');
    const name = (input.value || '').trim();
    if (!name || name.length < 2) {
        showToast(t('error.min_chars'), 'error');
        input.focus();
        return;
    }
    
    stopScanner();
    showLoading(true);
    
    try {
        // Search local products DB
        const localData = await api('products_search', { q: name });
        const localProducts = (localData.products || []).slice(0, 5);
        
        showLoading(false);
        
        if (localProducts.length > 0) {
            // Show results to pick from + option to create new
            showQuickNameResults(name, localProducts);
        } else {
            // No local results — create new product directly
            await createQuickProduct(name);
        }
    } catch (err) {
        showLoading(false);
        console.error('Quick name search error:', err);
        showToast(t('error.search_short'), 'error');
    }
}

function showQuickNameResults(searchName, products) {
    const container = document.querySelector('.quick-name-entry');
    
    // Remove any previous results
    const oldResults = container.querySelector('.quick-name-results');
    if (oldResults) oldResults.remove();
    
    const resultsDiv = document.createElement('div');
    resultsDiv.className = 'quick-name-results';
    
    // Existing products
    products.forEach(p => {
        const catIcon = CATEGORY_ICONS[mapToLocalCategory(p.category, p.name)] || '📦';
        const item = document.createElement('div');
        item.className = 'quick-name-result-item';
        item.innerHTML = `
            <span class="qnr-icon">${catIcon}</span>
            <div class="qnr-info">
                <div class="qnr-name">${escapeHtml(p.name)}</div>
                <div class="qnr-detail">${p.brand ? escapeHtml(p.brand) + ' · ' : ''}${p.barcode ? '📊 ' + p.barcode : t('product.no_barcode')}</div>
            </div>
        `;
        item.onclick = () => selectQuickProduct(p);
        resultsDiv.appendChild(item);
    });
    
    // "Create new" button
    const newItem = document.createElement('div');
    newItem.className = 'quick-name-result-item qnr-new';
    newItem.innerHTML = `
        <span class="qnr-icon">➕</span>
        <div class="qnr-info">
            <div class="qnr-name">${t('scan.create_named').replace('{name}', '"' + escapeHtml(searchName) + '"')}</div>
            <div class="qnr-detail">${t('scan.new_without_barcode')}</div>
        </div>
    `;
    newItem.onclick = () => createQuickProduct(searchName);
    resultsDiv.appendChild(newItem);
    
    container.appendChild(resultsDiv);
}

function selectQuickProduct(product) {
    currentProduct = {
        id: product.id,
        barcode: product.barcode || '',
        name: product.name,
        brand: product.brand || '',
        category: product.category || '',
        image_url: product.image_url || '',
        unit: product.unit || 'pz',
        default_quantity: product.default_quantity || 1,
    };
    // Extract weight_info from notes if available
    if (product.notes) {
        const pesoMatch = product.notes.match(/Peso:\s*([^·]+)/);
        if (pesoMatch) currentProduct.weight_info = pesoMatch[1].trim();
    }
    clearQuickNameResults();
    // Clear the search input
    const qInput = document.getElementById('quick-product-name');
    if (qInput) qInput.value = '';
    showProductAction();
}

async function createQuickProduct(name) {
    showLoading(true);
    
    // Auto-detect category from name (sync regex first)
    const category = guessCategoryFromName(name);
    
    try {
        const result = await api('product_save', {}, 'POST', {
            name: name,
            brand: '',
            category: category,
            unit: 'pz',
            default_quantity: 1,
        });
        
        if (result.success || result.id) {
            currentProduct = {
                id: result.id,
                name: name,
                brand: '',
                category: category,
                unit: 'pz',
                default_quantity: 1,
            };
            showLoading(false);
            clearQuickNameResults();
            showToast('Prodotto creato!', 'success');

            // If regex gave 'altro', try embedding in background and silently update
            if (category === 'altro' && typeof classifyCategoryByEmbedding === 'function') {
                classifyCategoryByEmbedding(name).then(async embCat => {
                    if (!embCat || !result.id) return;
                    try {
                        await api('product_save', {}, 'POST', {
                            id: result.id,
                            name: name,
                            brand: '',
                            category: embCat,
                            unit: 'pz',
                            default_quantity: 1,
                        });
                        if (currentProduct && currentProduct.id === result.id) {
                            currentProduct.category = embCat;
                        }
                    } catch (_) { /* silent */ }
                });
            }

            showProductAction();
        } else {
            showLoading(false);
            showToast(result.error || t('error.save'), 'error');
        }
    } catch (err) {
        showLoading(false);
        console.error('Quick product creation error:', err);
        showToast(t('error.connection'), 'error');
    }
}

function clearQuickNameResults() {
    const container = document.querySelector('.quick-name-entry');
    if (container) {
        const results = container.querySelector('.quick-name-results');
        if (results) results.remove();
    }
    const input = document.getElementById('quick-product-name');
    if (input) input.value = '';
}

function startManualEntry(barcode = '') {
    stopScanner();
    // Reset form
    document.getElementById('pf-id').value = '';
    document.getElementById('pf-name').value = '';
    document.getElementById('pf-brand').value = '';
    document.getElementById('pf-category').value = '';
    document.getElementById('pf-unit').value = 'pz';
    document.getElementById('pf-defqty').value = '1';
    document.getElementById('pf-notes').value = '';
    document.getElementById('pf-barcode').value = barcode || '';
    document.getElementById('pf-image').value = '';
    document.getElementById('pf-image-preview').style.display = 'none';
    document.getElementById('product-form-title').textContent = 'Nuovo Prodotto';
    const pfAiRow = document.getElementById('pf-ai-fill-row');
    if (pfAiRow) pfAiRow.style.display = 'block';

    // Show barcode hint when no barcode was passed
    _updateBarcodeHint();
    document.getElementById('pf-barcode').addEventListener('input', _updateBarcodeHint);
    
    // Remove datalist/autocomplete suggestions for new products (they cause confusion)
    document.getElementById('pf-name').removeAttribute('list');
    document.getElementById('pf-brand').removeAttribute('list');
    
    // Reset conf-size-row visibility
    const pfConfRow = document.getElementById('pf-conf-size-row');
    if (pfConfRow) pfConfRow.style.display = 'none';
    document.getElementById('pf-conf-size').value = '';
    document.getElementById('pf-conf-unit').value = 'g';
    
    // Reset manual-edit tracking flags
    document.getElementById('pf-category').dataset.manuallySet = 'false';
    document.getElementById('pf-defqty').dataset.manuallySet = 'false';
    
    // Track if user manually changes the quantity field
    const qtyInput = document.getElementById('pf-defqty');
    qtyInput.removeEventListener('input', markQtyManuallySet);
    qtyInput.addEventListener('input', markQtyManuallySet);
    
    // Auto-detect name → category when typing
    const nameInput = document.getElementById('pf-name');
    nameInput.removeEventListener('input', autoDetectCategory);
    nameInput.addEventListener('input', autoDetectCategory);
    
    showPage('product-form');
}

function markQtyManuallySet() {
    document.getElementById('pf-defqty').dataset.manuallySet = 'true';
}

function autoDetectCategory() {
    const name = document.getElementById('pf-name').value.toLowerCase();
    if (name.length < 3) return;
    
    const catSelect = document.getElementById('pf-category');
    // Don't override if user already manually selected something
    if (catSelect.dataset.manuallySet === 'true') return;
    
    // Keywords → category mapping
    const keyword2cat = {
        'latte': 'latticini', 'yogurt': 'latticini', 'formaggio': 'latticini', 'mozzarella': 'latticini',
        'burro': 'latticini', 'panna': 'latticini', 'ricotta': 'latticini', 'mascarpone': 'latticini',
        'gorgonzola': 'latticini', 'parmigiano': 'latticini', 'grana': 'latticini', 'burrata': 'latticini',
        'stracchino': 'latticini', 'uova': 'latticini',
        'pollo': 'carne', 'manzo': 'carne', 'maiale': 'carne', 'vitello': 'carne', 'tacchino': 'carne',
        'prosciutto': 'carne', 'salame': 'carne', 'bresaola': 'carne', 'mortadella': 'carne',
        'wurstel': 'carne', 'macinato': 'carne', 'speck': 'carne',
        'salmone': 'pesce', 'tonno': 'pesce', 'sgombro': 'pesce', 'pesce': 'pesce', 'merluzzo': 'pesce',
        'mela': 'frutta', 'mele': 'frutta', 'banana': 'frutta', 'arancia': 'frutta', 'pera': 'frutta',
        'fragola': 'frutta', 'uva': 'frutta', 'kiwi': 'frutta', 'limone': 'frutta',
        'insalata': 'verdura', 'pomodor': 'verdura', 'zucchin': 'verdura', 'patat': 'verdura',
        'cipoll': 'verdura', 'carota': 'verdura', 'spinaci': 'verdura', 'rucola': 'verdura',
        'peperoni': 'verdura', 'melanzane': 'verdura', 'broccoli': 'verdura',
        'pasta': 'pasta', 'spaghetti': 'pasta', 'penne': 'pasta', 'fusilli': 'pasta', 'riso': 'pasta',
        'farina': 'pasta', 'rigatoni': 'pasta', 'farfalle': 'pasta',
        'pane': 'pane', 'fette biscottate': 'pane', 'pancarrè': 'pane', 'pan carrè': 'pane',
        'grissini': 'pane', 'crackers': 'pane', 'cracker': 'pane',
        'surgelat': 'surgelati', 'findus': 'surgelati', 'gelato': 'surgelati',
        'acqua': 'bevande', 'succo': 'bevande', 'birra': 'bevande', 'vino': 'bevande',
        'coca cola': 'bevande', 'aranciata': 'bevande', 'tè': 'bevande', 'caffè': 'bevande',
        'olio': 'condimenti', 'aceto': 'condimenti', 'sale': 'condimenti', 'pepe': 'condimenti',
        'maionese': 'condimenti', 'ketchup': 'condimenti', 'senape': 'condimenti', 'zucchero': 'condimenti',
        'biscott': 'snack', 'cioccolat': 'snack', 'nutella': 'snack', 'merendine': 'snack',
        'patatine': 'snack', 'caramelle': 'snack',
        'pelati': 'conserve', 'passata': 'conserve', 'legumi': 'conserve', 'ceci': 'conserve',
        'fagioli': 'conserve', 'lenticchie': 'conserve', 'marmellata': 'conserve', 'miele': 'conserve',
        'cereali': 'cereali', 'muesli': 'cereali', 'fiocchi': 'cereali',
    };
    
    for (const [keyword, cat] of Object.entries(keyword2cat)) {
        if (name.includes(keyword)) {
            catSelect.value = cat;
            onCategoryChange(true);
            return;
        }
    }

    // ── Embedding fallback: async, only when keywords didn't match ──────────
    // Kick off model load (no-op if already loaded/loading) and update the
    // select once the result is ready.  Only runs when pipeline is available.
    if (typeof classifyCategoryByEmbedding === 'function') {
        classifyCategoryByEmbedding(document.getElementById('pf-name').value).then(embCat => {
            if (!embCat) return;
            // Re-check manuallySet — user might have picked something while awaiting
            const sel = document.getElementById('pf-category');
            if (!sel || sel.dataset.manuallySet === 'true') return;
            sel.value = embCat;
            onCategoryChange(true);
        });
    }
}

function onCategoryChange(fromAutoDetect = false) {
    const cat = document.getElementById('pf-category').value;
    const unitSelect = document.getElementById('pf-unit');
    const qtyInput = document.getElementById('pf-defqty');
    
    // If user manually changed category via dropdown, don't auto-fill qty/unit
    if (!fromAutoDetect) {
        // Mark qty as "set" so future auto-detects won't overwrite either
        qtyInput.dataset.manuallySet = 'true';
        return;
    }
    
    // Auto-detect from name: suggest default unit/qty based on category
    // BUT only if user hasn't manually changed the quantity field
    const catDefaults = {
        'latticini': { unit: 'pz', qty: 1 },
        'carne': { unit: 'g', qty: 500 },
        'pesce': { unit: 'g', qty: 300 },
        'frutta': { unit: 'g', qty: 1000 },
        'verdura': { unit: 'g', qty: 500 },
        'pasta': { unit: 'g', qty: 500 },
        'pane': { unit: 'pz', qty: 1 },
        'surgelati': { unit: 'g', qty: 450 },
        'bevande': { unit: 'ml', qty: 1000 },
        'condimenti': { unit: 'pz', qty: 1 },
        'snack': { unit: 'g', qty: 250 },
        'conserve': { unit: 'g', qty: 400 },
        'cereali': { unit: 'g', qty: 500 },
        'igiene': { unit: 'pz', qty: 1 },
        'pulizia': { unit: 'pz', qty: 1 },
    };
    
    if (catDefaults[cat]) {
        // Only auto-fill unit/qty if user hasn't manually touched them
        if (qtyInput.dataset.manuallySet !== 'true') {
            unitSelect.value = catDefaults[cat].unit;
            qtyInput.value = catDefaults[cat].qty;
        }
    }
}

function onPfUnitChange() {
    const unit = document.getElementById('pf-unit').value;
    const confRow = document.getElementById('pf-conf-size-row');
    if (confRow) confRow.style.display = unit === 'conf' ? 'block' : 'none';
}

function _updateBarcodeHint() {
    const hint = document.getElementById('pf-barcode-hint');
    const val = (document.getElementById('pf-barcode')?.value || '').trim();
    if (hint) hint.style.display = val ? 'none' : 'block';
}

/**
 * Open a temporary camera modal to scan a barcode and fill the pf-barcode field.
 * Uses BarcodeDetector if available, otherwise shows manual-input fallback.
 */
async function scanBarcodeForForm() {
    const overlayEl = document.getElementById('modal-overlay');
    const contentEl = document.getElementById('modal-content');

    let stream = null;
    let scanning = true;

    const stopStream = () => {
        scanning = false;
        if (stream) stream.getTracks().forEach(t => t.stop());
        stream = null;
    };

    const closeScanner = () => {
        stopStream();
        overlayEl.style.display = 'none';
    };

    contentEl.innerHTML = `
        <div class="modal-header">
            <h3>${t('scanner.title_barcode')}</h3>
            <button class="modal-close" onclick="document.getElementById('modal-overlay').style.display='none'">✕</button>
        </div>
        <div style="position:relative;width:100%;background:#000;border-radius:10px;overflow:hidden;aspect-ratio:4/3">
            <video id="pf-bc-video" autoplay playsinline muted style="width:100%;height:100%;object-fit:cover"></video>
            <div class="scanner-line scanning" style="position:absolute;left:0;right:0;top:50%;transform:translateY(-50%);height:2px;background:rgba(59,130,246,0.8)"></div>
        </div>
        <p style="text-align:center;margin-top:12px;color:var(--text-muted);font-size:0.88rem">${t('scanner.barcode_hint')}</p>
        <div style="margin-top:10px;text-align:center">
            <input type="text" id="pf-bc-manual" class="form-input" placeholder="${t('scanner.barcode_manual_placeholder')}" inputmode="numeric" style="max-width:260px;display:inline-block">
            <button class="btn btn-primary" style="margin-top:8px;width:100%" onclick="
                const v = document.getElementById('pf-bc-manual').value.trim();
                if(v){ document.getElementById('pf-barcode').value=v; _updateBarcodeHint(); document.getElementById('modal-overlay').style.display='none'; }
            ">${t('scanner.barcode_use_btn')}</button>
        </div>
    `;
    overlayEl.style.display = 'flex';

    // Attach close handler (clicking backdrop)
    overlayEl.onclick = (e) => { if (e.target === overlayEl) { stopStream(); overlayEl.style.display = 'none'; overlayEl.onclick = null; } };

    try {
        stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } });
        const video = document.getElementById('pf-bc-video');
        video.srcObject = stream;
        await video.play();

        if (!('BarcodeDetector' in window)) {
            // No native API — just let user type manually
            return;
        }

        const detector = new BarcodeDetector({ formats: ['ean_13','ean_8','code_128','code_39','upc_a','upc_e'] });
        const detectionHistory = {};

        const scanFrame = async () => {
            if (!scanning || !stream) return;
            try {
                const barcodes = await detector.detect(video);
                if (barcodes.length > 0) {
                    const code = barcodes[0].rawValue;
                    detectionHistory[code] = (detectionHistory[code] || 0) + 1;
                    if (detectionHistory[code] >= 2) {
                        scanning = false;
                        stopStream();
                        overlayEl.style.display = 'none';
                        overlayEl.onclick = null;
                        document.getElementById('pf-barcode').value = code;
                        _updateBarcodeHint();
                        if (navigator.vibrate) navigator.vibrate(80);
                        showToast(`🔖 Barcode acquisito: ${code}`, 'success');
                        return;
                    }
                }
            } catch (_) {}
            if (scanning) requestAnimationFrame(scanFrame);
        };
        requestAnimationFrame(scanFrame);

    } catch (err) {
        // Camera not available — user can still type manually
        const videoEl = document.getElementById('pf-bc-video');
        if (videoEl) videoEl.style.display = 'none';
    }
}

async function submitProduct(e) {
    e.preventDefault();
    showLoading(true);
    
    const pfUnit = document.getElementById('pf-unit').value;
    const productData = {
        id: document.getElementById('pf-id').value || null,
        name: document.getElementById('pf-name').value,
        brand: document.getElementById('pf-brand').value,
        category: document.getElementById('pf-category').value,
        unit: pfUnit,
        default_quantity: pfUnit === 'conf' ? (parseFloat(document.getElementById('pf-conf-size')?.value) || 1) : (parseFloat(document.getElementById('pf-defqty').value) || 1),
        package_unit: pfUnit === 'conf' ? (document.getElementById('pf-conf-unit')?.value || '') : '',
        notes: document.getElementById('pf-notes').value,
        barcode: document.getElementById('pf-barcode').value || null,
        image_url: document.getElementById('pf-image').value || '',
    };
    
    try {
        const result = await api('product_save', {}, 'POST', productData);
        if (result.success) {
            currentProduct = { ...productData, id: result.id };
            showLoading(false);
            showToast('Prodotto salvato!', 'success');
            showProductAction();
        } else {
            showLoading(false);
            showToast(result.error || t('error.save'), 'error');
        }
    } catch (err) {
        showLoading(false);
        showToast(t('error.connection'), 'error');
    }
}

// ===== PRODUCT ACTION (IN/OUT) =====
function showProductAction() {
    if (!currentProduct) return;
    
    const catIcon = CATEGORY_ICONS[mapToLocalCategory(currentProduct.category, currentProduct.name)] || '📦';
    const nutriscoreColors = { a: '#1e8f4e', b: '#60ac0e', c: '#eeae0e', d: '#ff6f1e', e: '#e63e11' };
    
    let detailsHtml = '';
    
    // Weight / quantity info
    if (currentProduct.weight_info) {
        detailsHtml += `<div class="product-detail-tag">⚖️ ${escapeHtml(currentProduct.weight_info)}</div>`;
    }
    
    // Nutriscore badge
    if (currentProduct.nutriscore) {
        const ns = currentProduct.nutriscore.toLowerCase();
        const nsColor = nutriscoreColors[ns] || '#999';
        detailsHtml += `<div class="product-detail-tag" style="background:${nsColor};color:#fff;font-weight:600">Nutri-Score ${ns.toUpperCase()}</div>`;
    }
    
    // NOVA group
    if (currentProduct.nova_group) {
        const novaLabels = { '1': t('nova.1'), '2': t('nova.2'), '3': t('nova.3'), '4': t('nova.4') };
        detailsHtml += `<div class="product-detail-tag">🏭 NOVA ${currentProduct.nova_group}${novaLabels[currentProduct.nova_group] ? ' - ' + novaLabels[currentProduct.nova_group] : ''}</div>`;
    }
    
    // Ecoscore
    if (currentProduct.ecoscore) {
        const es = currentProduct.ecoscore.toLowerCase();
        const esColor = nutriscoreColors[es] || '#999';
        detailsHtml += `<div class="product-detail-tag" style="background:${esColor};color:#fff;font-weight:600">🌍 Eco-Score ${es.toUpperCase()}</div>`;
    }
    
    // Origin
    if (currentProduct.origin) {
        detailsHtml += `<div class="product-detail-tag">📍 ${escapeHtml(currentProduct.origin)}</div>`;
    }
    
    // Labels (bio, DOP, etc.)
    if (currentProduct.labels) {
        detailsHtml += `<div class="product-detail-tag">🏷️ ${escapeHtml(currentProduct.labels)}</div>`;
    }
    
    // Allergens
    let allergensHtml = '';
    if (currentProduct.allergens) {
        allergensHtml = `<div class="product-allergens">⚠️ <strong>Allergeni:</strong> ${escapeHtml(currentProduct.allergens)}</div>`;
    }
    
    // Ingredients (collapsible)
    let ingredientsHtml = '';
    if (currentProduct.ingredients) {
        ingredientsHtml = `
            <details class="product-ingredients">
                <summary>📋 Ingredienti</summary>
                <p>${escapeHtml(currentProduct.ingredients)}</p>
            </details>
        `;
    }
    
    // Conservation
    let conservationHtml = '';
    if (currentProduct.conservation) {
        conservationHtml = `<div class="product-conservation">🧊 ${escapeHtml(currentProduct.conservation)}</div>`;
    }
    
    // LARGER product preview
    document.getElementById('action-product-preview').innerHTML = `
        ${currentProduct.image_url ?
            `<img src="${escapeHtml(currentProduct.image_url)}" alt="">` :
            `<span class="product-preview-emoji">${catIcon}</span>`
        }
        <div class="product-preview-info">
            <h3>${escapeHtml(currentProduct.name)}</h3>
            <p>${currentProduct.brand ? `<strong>${escapeHtml(currentProduct.brand)}</strong>` : ''}</p>
            ${currentProduct.weight_info ? `<p style="font-size:0.85rem;color:var(--text-light)">⚖️ ${escapeHtml(currentProduct.weight_info)}</p>` : ''}
            ${currentProduct.barcode ? `<p style="font-size:0.75rem;color:var(--text-muted)">📊 ${currentProduct.barcode}</p>` : ''}
        </div>
        <button type="button" class="btn-edit-inline" onclick="toggleActionEdit()" title="Modifica nome/marca">✏️</button>
    `;
    
    // Check if product needs editing (unknown name, missing info)
    const isUnknown = !currentProduct.name || 
        /sconosciuto|unknown|^$/i.test(currentProduct.name.trim()) ||
        currentProduct.name.trim().length < 2;
    
    // Edit product info section
    let editInfoEl = document.getElementById('action-edit-info');
    if (!editInfoEl) {
        editInfoEl = document.createElement('div');
        editInfoEl.id = 'action-edit-info';
        const preview = document.getElementById('action-product-preview');
        preview.parentElement.insertBefore(editInfoEl, preview.nextSibling);
    }
    
    // Always build the edit form, but only show it auto-opened for unknown products
    const categoryOptions = Object.entries(CATEGORY_LABELS).map(([key, label]) => 
        `<option value="${key}" ${mapToLocalCategory(currentProduct.category, currentProduct.name) === key ? 'selected' : ''}>${label}</option>`
    ).join('');
    
    editInfoEl.innerHTML = `
        <div class="edit-unknown-card ${isUnknown ? 'highlight' : ''}">
            <h4>${isUnknown ? '⚠️ Prodotto non riconosciuto' : '✏️ Modifica informazioni'}</h4>
            ${isUnknown ? '<p class="edit-unknown-hint">Inserisci il nome e le informazioni del prodotto</p>' : ''}
            <div class="edit-unknown-form">
                <div class="form-group">
                    <label>${t('edit.label_name')}</label>
                    <input type="text" id="edit-action-name" class="form-input" value="${escapeHtml(isUnknown ? '' : currentProduct.name)}" placeholder="Es: Latte intero, Pasta penne..." required>
                </div>
                <div class="form-group">
                    <label>🏪 Marca</label>
                    <input type="text" id="edit-action-brand" class="form-input" value="${escapeHtml(currentProduct.brand || '')}" placeholder="Es: Barilla, Mulino Bianco...">
                </div>
                <div class="form-group">
                    <label>📂 Categoria</label>
                    <select id="edit-action-category" class="form-input">
                        <option value="">-- Seleziona --</option>
                        ${categoryOptions}
                    </select>
                </div>
                <button type="button" class="btn btn-primary full-width" onclick="saveEditedProductInfo()">${t('btn.save_info')}</button>
            </div>
        </div>
    `;
    editInfoEl.style.display = isUnknown ? 'block' : 'none';
    if (isUnknown) {
        setTimeout(() => document.getElementById('edit-action-name')?.focus(), 100);
    }
    
    // Show extra product info section below preview
    let extraInfoEl = document.getElementById('action-product-details');
    if (!extraInfoEl) {
        const container = document.getElementById('action-product-preview').parentElement;
        extraInfoEl = document.createElement('div');
        extraInfoEl.id = 'action-product-details';
        const actionBtns = document.getElementById('action-buttons-container');
        actionBtns.parentElement.insertBefore(extraInfoEl, actionBtns);
    }
    
    if (detailsHtml || allergensHtml || ingredientsHtml || conservationHtml) {
        extraInfoEl.innerHTML = `
            <div class="product-details-card">
                ${detailsHtml ? `<div class="product-detail-tags">${detailsHtml}</div>` : ''}
                ${allergensHtml}
                ${ingredientsHtml}
                ${conservationHtml}
            </div>
        `;
        extraInfoEl.style.display = 'block';
    } else {
        extraInfoEl.style.display = 'none';
        extraInfoEl.innerHTML = '';
    }
    
    // === CHECK INVENTORY FOR THIS PRODUCT ===
    checkInventoryForProduct(currentProduct.id).then(inventoryItems => {
        _actionInventoryItems = inventoryItems;
        const statusBar = document.getElementById('action-inventory-status');
        const btnsContainer = document.getElementById('action-buttons-container');
        
        if (inventoryItems.length > 0) {
            // Product IS in inventory - show status and 3 buttons
            statusBar.style.display = 'block';
            let totalQty = 0;
            const unit = inventoryItems[0].unit || 'pz';
            const defQty = inventoryItems[0].default_quantity || 0;
            const pkgUnit = inventoryItems[0].package_unit || '';
            const invHtml = inventoryItems.map(inv => {
                const locInfo = LOCATIONS[inv.location] || { icon: '📦', label: inv.location };
                const qtyStr = formatQuantity(inv.quantity, inv.unit, inv.default_quantity, inv.package_unit);
                const pkgF = formatPackageFraction(inv.quantity, inv.default_quantity);
                totalQty += parseFloat(inv.quantity);
                let expiryStr = '';
                if (inv.expiry_date) {
                    const d = daysUntilExpiry(inv.expiry_date);
                if (d < 0) expiryStr = ` · ${t('expiry.badge_expired_ago').replace('{n}', Math.abs(d))}`;
                    else if (d <= 3) expiryStr = ` · ${t('expiry.badge_expires_red').replace('{n}', d)}`;
                    else if (d <= 7) expiryStr = ` · ${t('expiry.badge_expires_yellow').replace('{n}', d)}`;
                    else expiryStr = ` · 📅 ${formatDate(inv.expiry_date)}`;
                }
                const vacuumIcon = inv.vacuum_sealed ? ' 🫙' : '';
                return `<div class="inv-status-item inv-status-item-clickable" onclick="editActionInventoryItem(${inv.id})"><span>${locInfo.icon} ${locInfo.label}${vacuumIcon}${expiryStr}</span><span class="inv-status-qty">${qtyStr}${pkgF ? ' ' + pkgF : ''} ✏️</span></div>`;
            }).join('');
            
            const totalStr = formatQuantity(totalQty, unit, defQty, pkgUnit);
            const totalFrac = formatPackageFraction(totalQty, defQty);
            
            statusBar.innerHTML = `
                <div class="inv-status-header">
                    <span class="inv-status-title">${t('action.have_title')}</span>
                    <div class="inv-status-total-col">
                        <span class="inv-status-total">${totalStr}</span>
                        ${totalFrac ? `<span class="inv-status-total-frac">${totalFrac}</span>` : ''}
                    </div>
                </div>
                <div class="inv-status-items">${invHtml}</div>
            `;
            
            btnsContainer.className = 'action-buttons-4col';
            btnsContainer.innerHTML = `
                <button class="btn btn-huge btn-success" onclick="showAddForm()">
                    <span class="btn-icon">📥</span>
                    <span class="btn-text">${t('action.add_btn')}<br><small>${t('action.add_more_sub')}</small></span>
                </button>
                <button class="btn btn-huge btn-danger" onclick="showUseForm()">
                    <span class="btn-icon">📤</span>
                    <span class="btn-text">${t('action.use_btn')}<br><small>${t('action.use_qty_sub')}</small></span>
                </button>
                <button class="btn btn-huge btn-throw" onclick="showThrowForm()">
                    <span class="btn-icon">🗑️</span>
                    <span class="btn-text">${t('action.throw_btn')}<br><small>${t('action.throw_sub')}</small></span>
                </button>
                <button class="btn btn-huge btn-edit" onclick="openInventoryEdit()">
                    <span class="btn-icon">✏️</span>
                    <span class="btn-text">${t('product.modify_details')}<br><small>${t('action.edit_sub')}</small></span>
                </button>
            `;
            // Secondary: catalog edit link below the buttons (one instance only)
            let catalogLink = document.getElementById('catalog-edit-link');
            if (!catalogLink) {
                catalogLink = document.createElement('div');
                catalogLink.id = 'catalog-edit-link';
                catalogLink.style.cssText = 'text-align:center;margin-top:6px';
                btnsContainer.after(catalogLink);
            }
            catalogLink.innerHTML = `<button type="button" class="btn-link-small" onclick="editProductFromAction()">⚙️ Modifica scheda prodotto (nome, marca, categoria…)</button>`;
        } else {
            // Product NOT in inventory - show only AGGIUNGI
            statusBar.style.display = 'none';
            btnsContainer.className = 'action-buttons';
            btnsContainer.innerHTML = `
                <button class="btn btn-huge btn-success" onclick="showAddForm()" style="flex:1">
                    <span class="btn-icon">📥</span>
                    <span class="btn-text">${t('action.add_btn')}<br><small>${t('action.add_sub')}</small></span>
                </button>
            `;
            // Remove catalog-edit link if left over from a previous product
            const orphan = document.getElementById('catalog-edit-link');
            if (orphan) orphan.remove();
        }
    });
    
    // Update back button: go back to shopping if came from shopping list scan
    const backBtn = document.getElementById('action-back-btn');
    if (backBtn) backBtn.onclick = _spesaScanTarget ? () => { _spesaScanTarget = null; showPage('shopping'); } : () => showPage('scan');

    // Show "shopping target" banner if we came from the shopping list
    const banner = document.getElementById('shopping-scan-target-banner');
    if (banner && _spesaScanTarget) {
        const targetName = _spesaScanTarget.name;
        banner.style.display = 'block';
        banner.innerHTML = `
            <div class="shopping-scan-target-info">
                <span class="stb-label">🛒 ${t('shopping.scan_target_label')}</span>
                <span class="stb-name">${escapeHtml(targetName)}</span>
            </div>
            <div class="shopping-scan-target-actions">
                <button class="btn btn-success stb-btn" onclick="confirmShoppingItemFound()">✅ ${t('shopping.scan_target_found')}</button>
                <button class="btn btn-secondary stb-btn" onclick="_spesaScanTarget=null; document.getElementById('shopping-scan-target-banner').style.display='none'; document.getElementById('action-back-btn').onclick=()=>showPage('scan')">✕ ${t('btn.cancel')}</button>
            </div>
        `;
    } else if (banner) {
        banner.style.display = 'none';
    }

    showPage('action');
}

// Check if product exists in inventory
async function checkInventoryForProduct(productId) {
    try {
        const data = await api('inventory_list');
        return (data.inventory || []).filter(i => i.product_id == productId);
    } catch(e) {
        return [];
    }
}

// === EDIT PRODUCT FROM ACTION PAGE ===
function editProductFromAction() {
    if (!currentProduct) return;
    // Pre-fill the product form with current product data
    document.getElementById('pf-id').value = currentProduct.id || '';
    document.getElementById('pf-name').value = currentProduct.name || '';
    document.getElementById('pf-brand').value = currentProduct.brand || '';
    document.getElementById('pf-barcode').value = currentProduct.barcode || '';
    document.getElementById('pf-image').value = '';
    document.getElementById('pf-notes').value = currentProduct.notes || '';
    document.getElementById('pf-unit').value = currentProduct.unit || 'pz';
    document.getElementById('pf-defqty').value = currentProduct.default_quantity || 1;
    document.getElementById('product-form-title').textContent = t('product.title_edit');
    const pfAiRow = document.getElementById('pf-ai-fill-row');
    if (pfAiRow) pfAiRow.style.display = 'none';
    // Keep barcode hint hidden in edit mode
    const pfBcHint = document.getElementById('pf-barcode-hint');
    if (pfBcHint) pfBcHint.style.display = 'none';

    // Restore datalist for editing (was removed for new products)
    document.getElementById('pf-name').setAttribute('list', 'common-products');
    document.getElementById('pf-brand').setAttribute('list', 'common-brands');

    // Set category
    const cat = mapToLocalCategory(currentProduct.category, currentProduct.name);
    document.getElementById('pf-category').value = cat;
    document.getElementById('pf-category').dataset.manuallySet = 'true';
    document.getElementById('pf-defqty').dataset.manuallySet = 'true';

    // Image preview - not shown in edit mode
    const preview = document.getElementById('pf-image-preview');
    preview.style.display = 'none';

    // Conf size row
    const pfConfRow = document.getElementById('pf-conf-size-row');
    if (currentProduct.unit === 'conf' && pfConfRow) {
        pfConfRow.style.display = 'block';
        document.getElementById('pf-conf-size').value = currentProduct.default_quantity || '';
        document.getElementById('pf-conf-unit').value = currentProduct.package_unit || 'g';
    } else if (pfConfRow) {
        pfConfRow.style.display = 'none';
    }

    showPage('product-form');
}

// === EDIT INVENTORY ITEM FROM ACTION PAGE ===
// === OPEN INVENTORY EDIT — picks item or shows location picker ===
function openInventoryEdit() {
    const items = _actionInventoryItems;
    if (!items || items.length === 0) {
        showToast('Nessuna voce di inventario trovata', 'error');
        return;
    }
    if (items.length === 1) {
        editActionInventoryItem(items[0].id);
        return;
    }
    // Multiple locations → let user pick which one to edit
    const contentEl = document.getElementById('modal-content');
    contentEl.innerHTML = `
        <div class="modal-header">
            <h3>✏️ Quale modifica?</h3>
            <button class="modal-close" onclick="closeModal()">✕</button>
        </div>
        <p style="font-size:0.9rem;color:var(--text-muted);margin:0 0 12px">Scegli la posizione da modificare:</p>
        <div style="display:flex;flex-direction:column;gap:8px">
            ${items.map(inv => {
                const locInfo = LOCATIONS[inv.location] || { icon: '📦', label: inv.location };
                const qtyStr = formatQuantity(inv.quantity, inv.unit, inv.default_quantity, inv.package_unit);
                let expiryStr = '';
                if (inv.expiry_date) {
                    const d = daysUntilExpiry(inv.expiry_date);
                    expiryStr = ` · ${d < 0 ? t('expiry.badge_expired_bare') : '📅 ' + formatDate(inv.expiry_date)}`;
                }
                const vacuumStr = inv.vacuum_sealed ? ' 🫙' : '';
                return `<button class="btn btn-secondary full-width" style="justify-content:flex-start;gap:10px;text-align:left"
                    onclick="editActionInventoryItem(${inv.id})">
                    <span style="font-size:1.3rem">${locInfo.icon}</span>
                    <span><strong>${locInfo.label}</strong>${vacuumStr}<br>
                    <small style="color:var(--text-muted)">${qtyStr}${expiryStr}</small></span>
                </button>`;
            }).join('')}
        </div>
    `;
    document.getElementById('modal-overlay').style.display = 'flex';
}

function editActionInventoryItem(inventoryId) {
    const item = _actionInventoryItems.find(i => i.id === inventoryId);
    if (!item) return;
    
    const isConf = (item.unit || 'pz') === 'conf';
    const confSizeVal = (isConf && item.default_quantity > 0) ? item.default_quantity : '';
    const confUnitVal = (isConf && item.package_unit) ? item.package_unit : 'g';
    
    window._editingProduct = { name: item.name || currentProduct.name, category: item.category || currentProduct.category || '' };
    
    document.getElementById('modal-content').innerHTML = `
        <div class="modal-header">
            <h3>Modifica ${escapeHtml(item.name || currentProduct.name)}</h3>
            <button class="modal-close" onclick="closeModal()">✕</button>
        </div>
        <form class="form" onsubmit="submitActionEditInventory(event, ${inventoryId}, ${item.product_id})">
            <div class="form-group">
                <label>${t('add.quantity_label')}</label>
                <div class="qty-control">
                    <button type="button" class="qty-btn" onclick="adjustQty('action-edit-qty', -1)">−</button>
                    <input type="number" id="action-edit-qty" value="${item.quantity}" min="0" step="any" class="qty-input">
                    <button type="button" class="qty-btn" onclick="adjustQty('action-edit-qty', 1)">+</button>
                </div>
            </div>
            <div class="form-group">
                <label>${t('product.unit_label')}</label>
                <select id="action-edit-unit" class="form-input" onchange="onActionEditUnitChange()">
                    ${['pz','g','ml','conf'].map(u => `<option value="${u}" ${(item.unit||'pz') === u ? 'selected' : ''}>${u === 'pz' ? 'pz (pezzi)' : u === 'g' ? 'g (grammi)' : u === 'ml' ? 'ml (millilitri)' : u === 'conf' ? 'conf (confezioni)' : u}</option>`).join('')}
                </select>
            </div>
            <div class="form-group" id="action-edit-conf-group" style="display:${isConf ? 'block' : 'none'}">
                <label>📦 Ogni confezione contiene:</label>
                <div class="conf-size-inputs">
                    <input type="number" id="action-edit-conf-size" class="form-input conf-size-input" min="1" step="any" value="${confSizeVal}" placeholder="es. 300">
                    <select id="action-edit-conf-unit" class="form-input conf-size-unit">
                        ${['g','ml'].map(u => `<option value="${u}" ${confUnitVal === u ? 'selected' : ''}>${u}</option>`).join('')}
                    </select>
                </div>
            </div>
            <div class="form-group">
                <label>${t('inventory.label_position')}</label>
                <div class="location-selector">
                    ${Object.entries(LOCATIONS).map(([k, v]) => `
                        <button type="button" class="loc-btn ${item.location === k ? 'active' : ''}" 
                            onclick="this.parentElement.querySelectorAll('.loc-btn').forEach(b=>b.classList.remove('active'));this.classList.add('active');document.getElementById('action-edit-loc').value='${k}';recalcEditExpiry('action-edit-loc','action-edit-vacuum','action-edit-expiry')">${v.icon} ${v.label}</button>
                    `).join('')}
                </div>
                <input type="hidden" id="action-edit-loc" value="${item.location}">
            </div>
            <div class="form-group">
                <label>${t('inventory.label_expiry')}</label>
                <input type="date" id="action-edit-expiry" value="${item.expiry_date || ''}" class="form-input">
            </div>
            <div class="form-group">
                <label class="toggle-row">
                    ${t('add.vacuum_label')}
                    <span class="toggle-switch">
                        <input type="checkbox" id="action-edit-vacuum" ${item.vacuum_sealed ? 'checked' : ''} onchange="recalcEditExpiry('action-edit-loc','action-edit-vacuum','action-edit-expiry')">
                        <span class="toggle-slider"></span>
                    </span>
                </label>
            </div>
            <div class="modal-actions" style="margin-top:12px">
                <button type="submit" class="btn btn-large btn-primary flex-1">${t('btn.save')}</button>
                <button type="button" class="btn btn-secondary" onclick="deleteActionInventoryItem(${inventoryId})" style="padding:12px">🗑️</button>
            </div>
        </form>
    `;
    document.getElementById('modal-overlay').style.display = 'flex';
}

function onActionEditUnitChange() {
    const unit = document.getElementById('action-edit-unit').value;
    const confGroup = document.getElementById('action-edit-conf-group');
    if (confGroup) confGroup.style.display = unit === 'conf' ? 'block' : 'none';
}

async function submitActionEditInventory(e, id, productId) {
    e.preventDefault();
    const qty = parseFloat(document.getElementById('action-edit-qty').value);
    const loc = document.getElementById('action-edit-loc').value;
    const expiry = document.getElementById('action-edit-expiry').value || null;
    const unit = document.getElementById('action-edit-unit').value;
    
    const payload = { id, quantity: qty, location: loc, expiry_date: expiry, unit, product_id: productId,
        vacuum_sealed: document.getElementById('action-edit-vacuum')?.checked ? 1 : 0 };
    
    if (unit === 'conf') {
        payload.package_unit = document.getElementById('action-edit-conf-unit')?.value || '';
        payload.package_size = parseFloat(document.getElementById('action-edit-conf-size')?.value) || 0;
    } else {
        payload.package_unit = '';
        payload.package_size = 0;
    }
    
    await api('inventory_update', {}, 'POST', payload);
    closeModal();
    showToast('Aggiornato!', 'success');
    showProductAction(); // Refresh the action page
}

async function deleteActionInventoryItem(id) {
    if (confirm(t('confirm.remove_item'))) {
        await api('inventory_delete', {}, 'POST', { id });
        closeModal();
        showToast(t('toast.product_removed'), 'success');
        showProductAction(); // Refresh the action page
    }
}

// === THROW AWAY FORM ===
function showThrowForm() {
    // Open a modal to ask how much to throw away
    api('inventory_list').then(data => {
        const items = (data.inventory || []).filter(i => i.product_id == currentProduct.id);
        if (items.length === 0) {
            showToast('Prodotto non nell\'inventario', 'error');
            return;
        }
        
        const totalQty = items.reduce((sum, i) => sum + parseFloat(i.quantity), 0);
        const unit = items[0].unit || 'pz';
        const defQty = items[0].default_quantity || 0;
        const pkgUnit = items[0].package_unit || '';
        const qtyDisplay = formatQuantity(totalQty, unit, defQty, pkgUnit);
        
        let locOptionsHtml = items.map(inv => {
            const locInfo = LOCATIONS[inv.location] || { icon: '📦', label: inv.location };
            return `<div class="inv-status-item"><span>${locInfo.icon} ${locInfo.label}</span><span class="inv-status-qty">${formatQuantity(inv.quantity, inv.unit, inv.default_quantity, inv.package_unit)}</span></div>`;
        }).join('');
        
        document.getElementById('modal-content').innerHTML = `
            <div class="modal-header">
                <h3>${t('use.throw_title')}</h3>
                <button class="modal-close" onclick="closeModal()">✕</button>
            </div>
            <div class="product-preview-small" style="margin-bottom:12px">
                ${currentProduct.image_url ?
                    `<img src="${escapeHtml(currentProduct.image_url)}" alt="" style="width:50px;height:50px;border-radius:10px;object-fit:cover">` :
                    `<span style="font-size:2rem">${CATEGORY_ICONS[mapToLocalCategory(currentProduct.category, currentProduct.name)] || '📦'}</span>`
                }
                <div class="product-preview-info">
                    <h3>${escapeHtml(currentProduct.name)}</h3>
                    <p>Disponibile: <strong>${qtyDisplay}</strong></p>
                </div>
            </div>
            <div class="inventory-status-bar" style="margin-bottom:16px">
                <div class="inv-status-items">${locOptionsHtml}</div>
            </div>
            <div style="display:flex;flex-direction:column;gap:10px">
                <button class="btn btn-large btn-danger full-width" onclick="throwAll()">
                    ${t('use.throw_all', { qty: qtyDisplay })}
                </button>
                <div style="text-align:center;color:var(--text-muted);font-size:0.85rem">${t('use.throw_qty_hint')}</div>
                <div class="form-group">
                    <label>📍 Da dove?</label>
                    <div class="location-selector" id="throw-location-selector">
                        ${items.map((inv, idx) => {
                            const locInfo = LOCATIONS[inv.location] || { icon: '📦', label: inv.location };
                            return `<button type="button" class="loc-btn ${idx === 0 ? 'active' : ''}" onclick="selectThrowLocation(this, '${inv.location}')">${locInfo.icon} ${locInfo.label} (${formatQuantity(inv.quantity, inv.unit, inv.default_quantity, inv.package_unit)})</button>`;
                        }).join('')}
                    </div>
                    <input type="hidden" id="throw-location" value="${items[0].location}">
                </div>
                <div class="form-group">
                    <label>${t('use.throw_qty_label')}</label>
                    <div class="qty-control">
                        <button type="button" class="qty-btn" onclick="adjustQty('throw-quantity', -1)">−</button>
                        <input type="number" id="throw-quantity" value="1" min="0.1" step="any" class="qty-input">
                        <button type="button" class="qty-btn" onclick="adjustQty('throw-quantity', 1)">+</button>
                    </div>
                </div>
                <button class="btn btn-large btn-warning full-width" onclick="throwPartial()">
                    ${t('use.throw_partial_btn')}
                </button>
            </div>
        `;
        document.getElementById('modal-overlay').style.display = 'flex';
    });
}

function selectThrowLocation(btn, loc) {
    btn.parentElement.querySelectorAll('.loc-btn').forEach(b => b.classList.remove('active'));
    btn.classList.add('active');
    document.getElementById('throw-location').value = loc;
}

/**
 * Show a destructive-action confirmation modal with a 5-second auto-confirm countdown.
 * The user can tap "Annulla" to cancel or "Conferma" (or wait) to proceed.
 * @param {string}   title     — Modal title
 * @param {string}   msg       — Explanatory text
 * @param {Function} onConfirm — Called when confirmed (by user or countdown)
 * @param {string}   [confirmLabel] — Override confirm button label
 */
function _showDestructiveConfirm(title, msg, onConfirm, confirmLabel) {
    const DURATION = 5000;
    const btnLabel = confirmLabel || t('confirm.proceed') || 'Conferma';
    const cancelLabel = t('confirm.cancel') || 'Annulla';
    let rafHandle = null;
    let timerHandle = null;
    let resolved = false;

    const overlayEl = document.getElementById('modal-overlay');
    const contentEl = document.getElementById('modal-content');
    const confirmBtnId = '_destConfirmBtn_' + Date.now();
    const barId       = '_destConfirmBar_' + Date.now();

    contentEl.innerHTML = `
        <div class="modal-header">
            <h3>${escapeHtml(title)}</h3>
        </div>
        <p style="margin:12px 0 18px;color:var(--text-muted);font-size:0.95rem">${escapeHtml(msg)}</p>
        <div style="height:4px;background:var(--border);border-radius:2px;overflow:hidden;margin-bottom:16px">
            <div id="${barId}" style="height:100%;width:100%;background:var(--danger);transition:none"></div>
        </div>
        <div style="display:flex;gap:10px">
            <button class="btn btn-secondary" style="flex:1" id="_destCancelBtn">${escapeHtml(cancelLabel)}</button>
            <button class="btn btn-danger" style="flex:1" id="${confirmBtnId}">${escapeHtml(btnLabel)}</button>
        </div>
    `;
    overlayEl.style.display = 'flex';

    function cleanup() {
        if (rafHandle)   cancelAnimationFrame(rafHandle);
        if (timerHandle) clearTimeout(timerHandle);
        rafHandle = timerHandle = null;
    }
    function doConfirm() {
        if (resolved) return;
        resolved = true;
        cleanup();
        closeModal();
        onConfirm();
    }
    function doCancel() {
        if (resolved) return;
        resolved = true;
        cleanup();
        closeModal();
    }

    document.getElementById(confirmBtnId).addEventListener('click', doConfirm);
    document.getElementById('_destCancelBtn').addEventListener('click', doCancel);

    // Countdown animation
    const barEl = document.getElementById(barId);
    const start = performance.now();
    function tick() {
        const pct = Math.min(100, (performance.now() - start) / DURATION * 100);
        if (barEl) barEl.style.width = (100 - pct) + '%';
        if (pct < 100) { rafHandle = requestAnimationFrame(tick); }
    }
    rafHandle = requestAnimationFrame(tick);
    timerHandle = setTimeout(doConfirm, DURATION);
}

async function throwAll() {
    const name = currentProduct ? currentProduct.name : '';
    _showDestructiveConfirm(
        t('use.throw_all_confirm_title') || '🗑️ Butta tutto',
        (t('use.throw_all_confirm_msg') || 'Vuoi davvero buttare via tutto il prodotto?') + (name ? `\n"${name}"` : ''),
        async () => {
            showLoading(true);
            try {
                const result = await api('inventory_use', {}, 'POST', {
                    product_id: currentProduct.id,
                    use_all: true,
                    location: '__all__',
                    notes: 'Buttato'
                });
                showLoading(false);
                if (result.success) {
                    showToast(t('toast.thrown_away', { name: currentProduct.name }), 'success');
                    showPage('dashboard');
                } else {
                    showToast(result.error || 'Errore', 'error');
                }
            } catch(e) {
                showLoading(false);
                showToast(t('error.connection'), 'error');
            }
        },
        t('use.throw_all_confirm_btn') || '🗑️ Sì, butta'
    );
}

async function throwPartial() {
    const qty = parseFloat(document.getElementById('throw-quantity').value) || 1;
    const loc = document.getElementById('throw-location').value;
    closeModal();
    showLoading(true);
    try {
        const result = await api('inventory_use', {}, 'POST', {
            product_id: currentProduct.id,
            quantity: qty,
            location: loc,
            notes: 'Buttato'
        });
        showLoading(false);
        if (result.success) {
            showToast(t('toast.thrown_away_partial', { qty, unit: currentProduct.unit || 'pz', name: currentProduct.name }), 'success');
            showPage('dashboard');
        } else {
            showToast(result.error || 'Errore', 'error');
        }
    } catch(e) {
        showLoading(false);
        showToast(t('error.connection'), 'error');
    }
}

function toggleActionEdit() {
    const el = document.getElementById('action-edit-info');
    if (!el) return;
    el.style.display = el.style.display === 'none' ? 'block' : 'none';
    if (el.style.display === 'block') {
        setTimeout(() => document.getElementById('edit-action-name')?.focus(), 100);
    }
}

async function saveEditedProductInfo() {
    const name = (document.getElementById('edit-action-name')?.value || '').trim();
    if (!name) {
        showToast(t('product.name_required'), 'error');
        document.getElementById('edit-action-name')?.focus();
        return;
    }
    const brand = (document.getElementById('edit-action-brand')?.value || '').trim();
    const category = document.getElementById('edit-action-category')?.value || '';
    
    showLoading(true);
    try {
        const result = await api('product_save', {}, 'POST', {
            id: currentProduct.id,
            barcode: currentProduct.barcode || null,
            name: name,
            brand: brand,
            category: category || currentProduct.category || '',
            image_url: currentProduct.image_url || '',
            unit: currentProduct.unit || 'pz',
            default_quantity: currentProduct.default_quantity || 1,
            notes: currentProduct.notes || '',
        });
        showLoading(false);
        if (result.success) {
            // Update current product in memory
            currentProduct.name = name;
            currentProduct.brand = brand;
            if (category) currentProduct.category = category;
            showToast(t('toast.product_updated'), 'success');
            // Refresh the action page with updated data
            showProductAction();
        } else {
            showToast(result.error || t('error.save'), 'error');
        }
    } catch (err) {
        showLoading(false);
        showToast(t('error.connection'), 'error');
    }
}

// ===== ADD TO INVENTORY =====
function showAddForm() {
    const catIcon = CATEGORY_ICONS[mapToLocalCategory(currentProduct.category, currentProduct.name)] || '📦';
    document.getElementById('add-product-preview').innerHTML = `
        ${currentProduct.image_url ?
            `<img src="${escapeHtml(currentProduct.image_url)}" alt="">` :
            `<span style="font-size:2rem">${catIcon}</span>`
        }
        <div class="product-preview-info">
            <h3>${escapeHtml(currentProduct.name)}</h3>
            <p>${currentProduct.brand ? escapeHtml(currentProduct.brand) : ''}</p>
            ${currentProduct.weight_info ? `<p style="font-size:0.8rem;color:var(--text-light)">${escapeHtml(currentProduct.weight_info)}</p>` : ''}
        </div>
    `;
    
    // Set unit selector
    const unit = currentProduct.unit || 'pz';
    const unitSelect = document.getElementById('add-unit');
    unitSelect.value = unit;
    
    document.getElementById('add-quantity').value = unit === 'conf' ? (currentProduct._confCount || currentProduct.last_qty || 1) : (currentProduct.default_quantity || 1);
    document.getElementById('add-quantity').dataset.manuallySet = 'false';
    
    // Show/hide conf size row and pre-fill
    const confRow = document.getElementById('add-conf-size-row');
    if (confRow) {
        confRow.style.display = unit === 'conf' ? 'block' : 'none';
        if (unit === 'conf' && currentProduct.package_unit && currentProduct.default_quantity > 0) {
            document.getElementById('add-conf-size').value = currentProduct.default_quantity;
            document.getElementById('add-conf-unit').value = currentProduct.package_unit;
        } else if (unit === 'conf' && ['g', 'ml', 'kg', 'l'].includes(currentProduct.unit) && currentProduct.default_quantity > 0) {
            // Product was defined in weight/volume — that quantity IS the package size
            document.getElementById('add-conf-size').value = currentProduct.default_quantity;
            document.getElementById('add-conf-unit').value = currentProduct.unit;
        } else if (unit === 'conf') {
            document.getElementById('add-conf-size').value = '';
            document.getElementById('add-conf-unit').value = 'g';
        }
    }
    
    // Track manual edits to quantity in add form
    const addQtyInput = document.getElementById('add-quantity');
    addQtyInput.removeEventListener('input', markAddQtyManuallySet);
    addQtyInput.addEventListener('input', markAddQtyManuallySet);
    
    // Show weight info if product has it
    const weightInfoEl = document.getElementById('add-weight-info');
    if (currentProduct.weight_info) {
        weightInfoEl.textContent = `📦 Confezione: ${currentProduct.weight_info}`;
        weightInfoEl.style.display = 'block';
    } else {
        weightInfoEl.style.display = 'none';
    }
    
    // Set qty step based on selected unit
    updateAddQtyStep();
    
    // Auto-detect location
    const autoLoc = guessLocation(currentProduct);
    document.getElementById('add-location').value = autoLoc;
    
    // Highlight correct location button
    document.querySelectorAll('#page-add .loc-btn').forEach(b => b.classList.remove('active'));
    document.querySelectorAll('#page-add .loc-btn').forEach(b => {
        const btnText = b.textContent.toLowerCase();
        if (btnText.includes(autoLoc)) b.classList.add('active');
    });
    
    // Show the purchase-type selector  
    const expirySection = document.getElementById('add-expiry-section');
    const estimatedDays = estimateExpiryDays(currentProduct, autoLoc);
    const estimatedDate = addDays(estimatedDays);
    const estimateLabel = formatEstimatedExpiry(estimatedDays);
    
    let expirySuffix = autoLoc === 'freezer' ? ' (freezer)' : '';
    
    // Reset vacuum sealed toggle
    const vacuumCb = document.getElementById('add-vacuum-sealed');
    if (vacuumCb) {
        vacuumCb.checked = false;
        document.getElementById('add-vacuum-hint').style.display = 'none';
    }
    // Reset historical expiry for this product; will be fetched async
    window._historyExpiryDays = null;
    window._historyExpiryCount = 0;
    // Reset extra batches from previous add
    window._addExtraBatches = [];
    // Store base expiry for vacuum recalculation
    window._addBaseExpiryDays = estimatedDays;
    
    expirySection.innerHTML = `
        <label>${t('add.purchase_type_label')}</label>
        <div class="purchase-type-selector">
            <button type="button" class="purchase-type-btn active" onclick="selectPurchaseType(this, 'new')">
                ${t('add.new_btn')}
            </button>
            <button type="button" class="purchase-type-btn" onclick="selectPurchaseType(this, 'existing')">
                ${t('add.existing_btn')}
            </button>
        </div>
        <div id="expiry-detail" class="expiry-detail">
            <div class="expiry-estimate">
                <span class="expiry-estimate-label">${t('add.estimated_expiry')} <strong>${estimateLabel}${expirySuffix}</strong></span>
                <span class="expiry-estimate-date">${formatDate(estimatedDate)}</span>
            </div>
            <div class="expiry-input-row">
                <input type="date" id="add-expiry" class="form-input" value="${estimatedDate}">
                <button type="button" class="btn btn-accent btn-scan-expiry" onclick="scanExpiryWithAI()" title="${t('add.scan_expiry_title')}">📷</button>
            </div>
            <p class="form-hint">${t('add.hint_modify')}</p>
        </div>
        <div id="multi-batch-section" style="display:${unit === 'conf' ? 'block' : 'none'}">
            <div id="multi-batch-container"></div>
            <button type="button" class="btn btn-outline btn-small full-width" style="margin-top:8px" onclick="addExpiryBatch()">
                📦 + Lotto con scadenza diversa
            </button>
        </div>
    `;
    
    showPage('add');
    updateScaleReadButtons();
    // After rendering, fetch history-based expiry prediction
    if (currentProduct && currentProduct.id) {
        _fetchExpiryHistoryAndUpdate(currentProduct.id);
    }
    // If Gemini is available and product was just created (no history), ask for AI hint
    if (_geminiAvailable && currentProduct && !currentProduct._aiHintFetched) {
        _applyAIProductHint();
    }
}

function toggleVacuumSealed() {
    const cb = document.getElementById('add-vacuum-sealed');
    if (cb) cb.checked = !cb.checked;
    onVacuumSealedChange();
}

function onVacuumSealedChange() {
    const hint = document.getElementById('add-vacuum-hint');
    if (hint) hint.style.display = document.getElementById('add-vacuum-sealed')?.checked ? 'block' : 'none';
    recalculateAddExpiry();
}

function recalculateAddExpiry() {
    if (!currentProduct) return;
    const loc = document.getElementById('add-location')?.value || '';
    const isVacuum = document.getElementById('add-vacuum-sealed')?.checked;
    
    const baseDays = window._historyExpiryDays ?? estimateExpiryDays(currentProduct, loc);
    let days = isVacuum ? getVacuumExpiryDays(baseDays) : baseDays;
    
    window._addBaseExpiryDays = baseDays;
    
    const newDate = addDays(days);
    const newLabel = formatEstimatedExpiry(days);
    
    let suffix = '';
    if (window._historyExpiryDays) suffix = ' (da storico)';
    else if (loc === 'freezer' && isVacuum) suffix = ' ' + t('add.suffix_freezer_vacuum');
    else if (loc === 'freezer') suffix = ' (freezer)';
    else if (isVacuum) suffix = ' (sotto vuoto)';
    
    const expiryInput = document.getElementById('add-expiry');
    const estimateEl = document.querySelector('.expiry-estimate-label');
    const dateEl = document.querySelector('.expiry-estimate-date');
    if (expiryInput) expiryInput.value = newDate;
    if (estimateEl) estimateEl.innerHTML = `${t('add.estimated_expiry')} <strong>${newLabel}${suffix}</strong>`;
    if (dateEl) dateEl.textContent = formatDate(newDate);
}

async function _fetchExpiryHistoryAndUpdate(productId) {
    try {
        const res = await fetch(`api/index.php?action=expiry_history&product_id=${encodeURIComponent(productId)}`);
        const data = await res.json();
        if (data.avg_days && data.avg_days > 0 && data.count >= 1) {
            window._historyExpiryDays = data.avg_days;
            window._historyExpiryCount = data.count;
            // Update the displayed date and label
            const loc = document.getElementById('add-location')?.value || '';
            const isVacuum = document.getElementById('add-vacuum-sealed')?.checked;
            let days = isVacuum ? getVacuumExpiryDays(data.avg_days) : data.avg_days;
            const newDate = addDays(days);
            const newLabel = formatEstimatedExpiry(days);
            const suffix = ` <span class="history-badge" title="Media da ${data.count} insertiment${data.count === 1 ? 'o' : 'i'} precedent${data.count === 1 ? 'e' : 'i'}">📊 storico</span>`;
            const expiryInput = document.getElementById('add-expiry');
            const estimateEl = document.querySelector('.expiry-estimate-label');
            const dateEl = document.querySelector('.expiry-estimate-date');
            if (expiryInput) expiryInput.value = newDate;
            if (estimateEl) estimateEl.innerHTML = `${t('add.estimated_expiry')} <strong>${newLabel}${suffix}</strong>`;
            if (dateEl) dateEl.textContent = formatDate(newDate);
            window._addBaseExpiryDays = data.avg_days;
        }
    } catch (e) {
        // silently fall back to rule-based estimate
    }
}

// ===== AI PRODUCT HINT: shelf-life + storage suggestion =====
let _aiProductHintController = null;
async function _applyAIProductHint() {
    if (!currentProduct) return;
    // Abort any in-flight request for a previous product
    if (_aiProductHintController) _aiProductHintController.abort();
    _aiProductHintController = new AbortController();

    // Show a subtle loading indicator near the estimate label
    const estimateEl = document.querySelector('.expiry-estimate-label');
    if (estimateEl) {
        const oldHtml = estimateEl.innerHTML;
        estimateEl.dataset.aiOriginal = oldHtml;
        estimateEl.innerHTML += ' <span id="ai-hint-loading" style="font-size:0.75rem;opacity:0.7">🤖…</span>';
    }

    try {
        const data = await api('gemini_product_hint', {}, 'POST', {
            name:     currentProduct.name,
            category: currentProduct.category || '',
            lang:     _currentLang,
        });

        // Remove loading indicator
        document.getElementById('ai-hint-loading')?.remove();

        if (!data.success || !data.location || !data.expiry_days) return;
        // Mark so we don't re-fetch on the same product
        currentProduct._aiHintFetched = true;

        const curLoc  = document.getElementById('add-location')?.value;
        const locChanged = data.location !== curLoc;

        // Update location if AI suggests a different one (and user hasn't manually picked)
        if (locChanged) {
            document.getElementById('add-location').value = data.location;
            // Update active loc-btn
            document.querySelectorAll('#page-add .loc-btn').forEach(b => {
                const onclick = b.getAttribute('onclick') || '';
                const locMatch = onclick.match(/'([^']+)'\s*\)/);
                if (locMatch) b.classList.toggle('active', locMatch[1] === data.location);
            });
        }

        // Update expiry only if we have no historical data (history takes priority)
        if (!window._historyExpiryDays) {
            window._addBaseExpiryDays = data.expiry_days;
            const newDate  = addDays(data.expiry_days);
            const newLabel = formatEstimatedExpiry(data.expiry_days);
            const expiryInput = document.getElementById('add-expiry');
            const dateEl      = document.querySelector('.expiry-estimate-date');
            if (expiryInput) expiryInput.value = newDate;
            if (dateEl) dateEl.textContent = formatDate(newDate);
            const aiSuffix = ` <span class="history-badge" style="background:rgba(99,102,241,0.15);color:#6366f1" title="${escapeHtml(data.reason || '')}">🤖 AI</span>`;
            if (estimateEl) estimateEl.innerHTML = `${t('add.estimated_expiry')} <strong>${newLabel}</strong>${aiSuffix}`;
        } else if (estimateEl && estimateEl.dataset.aiOriginal) {
            // Restore original if history already set
            estimateEl.innerHTML = estimateEl.dataset.aiOriginal;
        }

        // Show a toast only if location changed
        if (locChanged) {
            const locLabels = { dispensa: t('location.dispensa') || 'Dispensa', frigo: t('location.frigo') || 'Frigo', freezer: t('location.freezer') || 'Freezer' };
            showToast(`🤖 AI: conserva in ${locLabels[data.location] || data.location}`, 'info', 4000);
        }
    } catch (e) {
        document.getElementById('ai-hint-loading')?.remove();
        if (estimateEl && estimateEl.dataset.aiOriginal) estimateEl.innerHTML = estimateEl.dataset.aiOriginal;
        // silent — AI hint is best-effort
    }
}

function getVacuumExpiryDays(baseDays) {
    // Vacuum sealing extends shelf life significantly
    if (baseDays <= 7) return Math.round(baseDays * 3);       // very fresh: 3x (e.g., 3→9, 7→21)
    if (baseDays <= 14) return Math.round(baseDays * 3);       // fresh cheese/dairy: 3x (10→30)
    if (baseDays <= 30) return Math.round(baseDays * 2.5);     // short: 2.5x (e.g., 21→52)
    if (baseDays <= 90) return Math.round(baseDays * 2.5);     // medium (cheese ~60d): 2.5x (60→150)
    return Math.round(baseDays * 1.5);                         // long-lasting: 1.5x
}

function onAddUnitChange() {
    updateAddQtyStep();
    const unit = document.getElementById('add-unit').value;
    const qtyInput = document.getElementById('add-quantity');
    
    // Show/hide conf size row
    const confRow = document.getElementById('add-conf-size-row');
    if (confRow) {
        const isConf = unit === 'conf';
        confRow.style.display = isConf ? 'block' : 'none';
        // Pre-fill from currentProduct if available
        if (isConf && currentProduct) {
            const sizeInput = document.getElementById('add-conf-size');
            const unitSelect = document.getElementById('add-conf-unit');
            if (currentProduct.package_unit && currentProduct.default_quantity > 1) {
                sizeInput.value = currentProduct.default_quantity;
                unitSelect.value = currentProduct.package_unit;
            } else if (['g', 'ml', 'kg', 'l'].includes(currentProduct.unit) && currentProduct.default_quantity > 0) {
                // Product was defined in weight/volume — that quantity IS the package size
                sizeInput.value = currentProduct.default_quantity;
                unitSelect.value = currentProduct.unit;
            } else {
                sizeInput.value = '';
                unitSelect.value = 'g';
            }
        }
        // Scroll into view so the user sees the new field
        if (isConf) setTimeout(() => confRow.scrollIntoView({ behavior: 'smooth', block: 'nearest' }), 100);
    }

    // Show/hide multi-batch section (only for conf unit)
    const mbSection = document.getElementById('multi-batch-section');
    if (mbSection) mbSection.style.display = unit === 'conf' ? 'block' : 'none';
    
    // If switching units, suggest a sensible quantity
    // BUT only if the user hasn't manually changed the quantity in this form
    if (qtyInput.dataset.manuallySet === 'true') return; // User already edited qty, don't overwrite
    
    const currentQty = parseFloat(qtyInput.value) || 1;
    
    // Convert between related units if logical
    if (unit === 'g' && currentQty <= 10) qtyInput.value = currentProduct.weight_info ? parseFloat(currentProduct.weight_info) || 250 : 250;
    if (unit === 'ml' && currentQty <= 10) qtyInput.value = 500;
    if (unit === 'pz' && currentQty > 100) qtyInput.value = 1;
    if (unit === 'conf' && currentQty > 10) qtyInput.value = 1;

    // Show/hide scale read button based on new unit
    updateScaleReadButtons();
}

function updateAddQtyStep() {
    const qtyInput = document.getElementById('add-quantity');
    const unit = document.getElementById('add-unit').value;
    qtyInput.step = 'any';
    if (unit === 'g' || unit === 'ml') {
        qtyInput.min = '1';
    } else {
        qtyInput.min = '1';
    }
}

function markAddQtyManuallySet() {
    document.getElementById('add-quantity').dataset.manuallySet = 'true';
}

function adjustAddQty(delta) {
    const qtyInput = document.getElementById('add-quantity');
    qtyInput.dataset.manuallySet = 'true'; // +/- buttons count as manual edit
    const unit = document.getElementById('add-unit').value;
    let val = parseFloat(qtyInput.value) || 0;
    let step;
    if (unit === 'g' || unit === 'ml') {
        step = val < 50 ? 1 : (val < 500 ? 10 : 50);
    } else {
        step = 1;
    }
    val = Math.max(parseFloat(qtyInput.min) || 0.1, val + delta * step);
    // Round nicely
    if (step >= 1) val = Math.round(val);
    else val = Math.round(val * 10) / 10;
    qtyInput.value = val;
}

function selectPurchaseType(btn, type) {
    btn.parentElement.querySelectorAll('.purchase-type-btn').forEach(b => b.classList.remove('active'));
    btn.classList.add('active');
    
    // Reset extra batches when switching purchase type
    window._addExtraBatches = [];
    const mbContainer = document.getElementById('multi-batch-container');
    if (mbContainer) mbContainer.innerHTML = '';

    const detailDiv = document.getElementById('expiry-detail');
    
    // Save current quantity before switching, so we can preserve it
    const currentQty = document.getElementById('add-quantity').value;
    
    if (type === 'new') {
        // Recalculate fresh expiry based on current location/vacuum
        const loc = document.getElementById('add-location')?.value || '';
        const isVacuum = document.getElementById('add-vacuum-sealed')?.checked;
        const baseDays = window._historyExpiryDays ?? estimateExpiryDays(currentProduct, loc);
        let days = isVacuum ? getVacuumExpiryDays(baseDays) : baseDays;
        const estimatedDate = addDays(days);
        const estimateLabel = formatEstimatedExpiry(days);
        let suffix = '';
        if (window._historyExpiryDays) suffix = ` <span class="history-badge" title="Media da ${window._historyExpiryCount} inserimento/i precedente/i">📊 storico</span>`;
        else if (loc === 'freezer' && isVacuum) suffix = ' ' + t('add.suffix_freezer_vacuum');
        else if (loc === 'freezer') suffix = ' ' + t('add.suffix_freezer');
        else if (isVacuum) suffix = ' ' + t('add.suffix_vacuum');
        
        detailDiv.innerHTML = `
            <div class="expiry-estimate">
                <span class="expiry-estimate-label">${t('add.estimated_expiry')} <strong>${estimateLabel}${suffix}</strong></span>
                <span class="expiry-estimate-date">${formatDate(estimatedDate)}</span>
            </div>
            <div class="expiry-input-row">
                <input type="date" id="add-expiry" class="form-input" value="${estimatedDate}">
                <button type="button" class="btn btn-accent btn-scan-expiry" onclick="scanExpiryWithAI()" title="${t('add.scan_expiry_title')}">📷</button>
            </div>
            <p class="form-hint">${t('add.hint_modify')}</p>
        `;
        // Restore quantity - switching purchase type should NOT change it
        document.getElementById('add-quantity').value = currentQty;
        // Show multi-batch section only in "new" mode (and only for conf unit)
        const mbSection = document.getElementById('multi-batch-section');
        if (mbSection) mbSection.style.display = (document.getElementById('add-unit')?.value === 'conf') ? 'block' : 'none';
    } else {
        detailDiv.innerHTML = `
            <div class="form-group">
                <label>📅 Quando scade?</label>
                <div class="expiry-input-row">
                    <input type="date" id="add-expiry" class="form-input" value="">
                    <button type="button" class="btn btn-accent btn-scan-expiry" onclick="scanExpiryWithAI()" title="${t('add.scan_expiry_title')}">📷</button>
                </div>
                <p class="form-hint">Inserisci la data di scadenza o scansionala</p>
            </div>
            <div class="form-group">
                <label>${t('add.remaining_label')}</label>
                <p class="form-hint" style="margin-bottom:6px">${t('add.remaining_hint')}</p>
                <div class="remaining-options">
                    <button type="button" class="remaining-btn" onclick="setRemainingPct(1)">${t('add.remaining_full')}</button>
                    <button type="button" class="remaining-btn" onclick="setRemainingPct(0.75)">🟡 ¾</button>
                    <button type="button" class="remaining-btn" onclick="setRemainingPct(0.5)">${t('add.remaining_half')}</button>
                    <button type="button" class="remaining-btn" onclick="setRemainingPct(0.25)">🔴 ¼</button>
                </div>
            </div>
        `;
        // DON'T auto-set remaining percentage - keep the quantity the user already entered
        // Hide multi-batch section in "existing" mode
        const mbSection = document.getElementById('multi-batch-section');
        if (mbSection) mbSection.style.display = 'none';
    }
}

function setRemainingPct(pct) {
    document.querySelectorAll('.remaining-btn').forEach(b => b.classList.remove('active'));
    event.target.classList.add('active');
    const baseQty = currentProduct.default_quantity || 1;
    const unit = currentProduct.unit || 'pz';
    let adjustedQty;
    if (unit === 'pz' || unit === 'conf') {
        adjustedQty = Math.max(1, Math.round(baseQty * pct));
    } else {
        adjustedQty = Math.round(baseQty * pct * 10) / 10;
    }
    document.getElementById('add-quantity').value = adjustedQty;
}

// ===== MULTI-EXPIRY BATCHES (for conf products with different expiry dates) =====
window._addExtraBatches = [];

function addExpiryBatch() {
    const loc = document.getElementById('add-location')?.value || '';
    const baseDays = window._historyExpiryDays ?? estimateExpiryDays(currentProduct, loc);
    const estimatedDate = addDays(baseDays);
    window._addExtraBatches.push({ qty: 1, expiry: estimatedDate });
    _rebuildMultiBatchUI();
}

function removeExpiryBatch(i) {
    window._addExtraBatches.splice(i, 1);
    _rebuildMultiBatchUI();
}

function adjustBatchQty(i, delta) {
    window._addExtraBatches[i].qty = Math.max(1, (window._addExtraBatches[i].qty || 1) + delta);
    _rebuildMultiBatchUI();
}

function _rebuildMultiBatchUI() {
    const container = document.getElementById('multi-batch-container');
    if (!container) return;
    if (window._addExtraBatches.length === 0) {
        container.innerHTML = '';
        return;
    }
    container.innerHTML = window._addExtraBatches.map((b, i) => `
        <div class="multi-batch-row">
            <div class="multi-batch-qty">
                <button type="button" class="qty-btn" onclick="adjustBatchQty(${i}, -1)">−</button>
                <input type="number" class="qty-input" value="${b.qty}" min="1" step="1" style="width:60px"
                    onchange="window._addExtraBatches[${i}].qty = parseInt(this.value)||1">
                <button type="button" class="qty-btn" onclick="adjustBatchQty(${i}, 1)">+</button>
                <span class="multi-batch-unit">conf</span>
            </div>
            <input type="date" class="form-input multi-batch-date" value="${b.expiry}"
                onchange="window._addExtraBatches[${i}].expiry = this.value">
            <button type="button" class="btn-icon-sm" onclick="removeExpiryBatch(${i})" title="Rimuovi">✕</button>
        </div>
    `).join('');
}

function selectLocation(btn, loc) {
    btn.parentElement.querySelectorAll('.loc-btn').forEach(b => b.classList.remove('active'));
    btn.classList.add('active');
    document.getElementById('add-location').value = loc;
    recalculateAddExpiry();
}

async function submitAdd(e) {
    e.preventDefault();
    showLoading(true);
    
    try {
        const selectedUnit = document.getElementById('add-unit').value;
        const productUnit = currentProduct.unit || 'pz';
        
        // Validate conf fields
        if (selectedUnit === 'conf') {
            const confSize = parseFloat(document.getElementById('add-conf-size')?.value);
            if (!confSize || confSize <= 0) {
                showLoading(false);
                showToast('Specifica il contenuto di ogni confezione', 'error');
                document.getElementById('add-conf-size')?.focus();
                return;
            }
        }
        
        const result = await api('inventory_add', {}, 'POST', {
            product_id: currentProduct.id,
            quantity: parseFloat(document.getElementById('add-quantity').value) || 1,
            location: document.getElementById('add-location').value,
            expiry_date: document.getElementById('add-expiry').value || null,
            unit: selectedUnit !== productUnit ? selectedUnit : null,
            package_unit: selectedUnit === 'conf' ? (document.getElementById('add-conf-unit')?.value || null) : null,
            package_size: selectedUnit === 'conf' ? (parseFloat(document.getElementById('add-conf-size')?.value) || null) : null,
            vacuum_sealed: document.getElementById('add-vacuum-sealed')?.checked ? 1 : 0,
        });
        
        showLoading(false);
        if (result.success) {
            // Build quantity info for toast
            let qtyInfo = '';
            if (result.total_qty) {
                const u = result.unit || 'pz';
                const unitLabels = { 'pz': 'pz', 'g': 'g', 'ml': 'ml', 'conf': 'conf' };
                const uLabel = unitLabels[u] || u;
                if (u === 'conf' && result.package_unit && result.default_quantity > 0) {
                    const pkgLabel = unitLabels[result.package_unit] || result.package_unit;
                    qtyInfo = ` (totale: ${result.total_qty} ${uLabel} da ${result.default_quantity}${pkgLabel})`;
                } else {
                    qtyInfo = ` (totale: ${result.total_qty} ${uLabel})`;
                }
            }
            showToast(t('add.product_added').replace('{name}', currentProduct.name).replace('{qty}', qtyInfo), 'success');
            if (result.removed_from_bring) {
                setTimeout(() => showToast(t('toast.removed_from_shopping'), 'info'), 1500);
            } else if (shoppingItems.length > 0 && shoppingListUUID) {
                // PHP matching may have missed the item (custom name / no catalog match) —
                // try a client-side fuzzy remove using the already-loaded shoppingItems
                const match = _findSimilarItem(currentProduct.name, shoppingItems);
                if (match) {
                    api('bring_remove', {}, 'POST', {
                        name: match.name,
                        rawName: match.rawName || '',
                        listUUID: shoppingListUUID
                    }).then(r => {
                        if (r && r.success) {
                            shoppingItems = shoppingItems.filter(i => i !== match);
                            setTimeout(() => showToast(t('toast.removed_from_shopping'), 'info'), 1500);
                        }
                    }).catch(() => {});
                }
            }
            if (!spesaModeAfterAdd()) showPage('dashboard');

            // Submit extra batches (different expiry dates) in the background, silently
            if ((window._addExtraBatches || []).length > 0) {
                const loc = document.getElementById('add-location')?.value || result.location || 'dispensa';
                const selectedUnit = document.getElementById('add-unit').value;
                const productUnit = currentProduct.unit || 'pz';
                const confUnit = document.getElementById('add-conf-unit')?.value || null;
                const confSize = parseFloat(document.getElementById('add-conf-size')?.value) || null;
                for (const batch of window._addExtraBatches) {
                    if (!batch.qty || batch.qty <= 0) continue;
                    api('inventory_add', {}, 'POST', {
                        product_id: currentProduct.id,
                        quantity: batch.qty,
                        location: loc,
                        expiry_date: batch.expiry || null,
                        unit: selectedUnit !== productUnit ? selectedUnit : null,
                        package_unit: selectedUnit === 'conf' ? confUnit : null,
                        package_size: selectedUnit === 'conf' ? confSize : null,
                    }).catch(() => {});
                }
                window._addExtraBatches = [];
            }
        } else {
            showToast(result.error || 'Errore', 'error');
        }
    } catch (err) {
        showLoading(false);
        showToast(t('error.connection'), 'error');
    }
}

// ===== USE FROM INVENTORY =====
let _useSubmitting = false; // double-submit guard
function showUseForm() {
    renderUsePreview();
    _useConfMode = null; // reset
    _useSubmitting = false;
    _scaleUserDismissed = false;
    _scaleStabilityVal  = null;
    _scaleLatestWeight  = null; // clear stale weight from previous product
    _cancelScaleAutoConfirm(false);
    document.getElementById('use-quantity').value = 1;
    document.getElementById('use-location').value = 'dispensa';
    document.getElementById('use-unit-switch').style.display = 'none';

    
    // Reset location buttons
    document.querySelectorAll('#page-use .loc-btn').forEach(b => b.classList.remove('active'));
    document.querySelector('#page-use .loc-btn').classList.add('active');
    
    loadUseInventoryInfo();
    showPage('use');
    updateScaleReadButtons();
}

function renderUsePreview() {
    const catIcon = CATEGORY_ICONS[mapToLocalCategory(currentProduct?.category, currentProduct?.name)] || '📦';
    document.getElementById('use-product-preview').innerHTML = `
        ${currentProduct?.image_url ?
            `<img src="${escapeHtml(currentProduct.image_url)}" alt="">` :
            `<span style="font-size:2rem">${catIcon}</span>`
        }
        <div class="product-preview-info">
            <h3>${escapeHtml(currentProduct?.name || '')}</h3>
            <p>${currentProduct?.brand ? escapeHtml(currentProduct.brand) : ''}</p>
        </div>
    `;
}

// Conf-mode tracking for USE form
let _useConfMode = null; // null = normal, { packageSize, packageUnit, totalSub, unit } = conf mode active
let _useNormalUnit = 'pz'; // unit when not in conf mode
let _useCurrentItems = []; // cached inventory items for the current product on the use page

/**
 * Mostra un suggerimento giallo sotto le info inventario quando ci sono più
 * confezioni con scadenze diverse (o in posti diversi con scadenze diverse).
 * Es: "⚠️ Usa prima quella in Frigo — scade il 12/04 (tra 3 giorni)!"
 */
function _renderUseExpiryHint(items) {
    const hintEl = document.getElementById('use-expiry-hint');

    // Parse YYYY-MM-DD as local noon to avoid timezone edge cases on some engines.
    const parseLocalExpiryDate = (dateStr) => {
        if (!dateStr) return null;
        const m = String(dateStr).match(/^(\d{4})-(\d{2})-(\d{2})/);
        if (!m) return null;
        return new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]), 12, 0, 0, 0);
    };

    // Ignore tiny residual quantities to avoid misleading hints on near-zero leftovers.
    const withExpiry = items.filter(i => i.expiry_date && parseFloat(i.quantity) > 0.01);

    // Serve almeno 2 item con scadenze diverse (o locazioni diverse con scadenze)
    if (withExpiry.length < 2) { hintEl.style.display = 'none'; return; }

    const dates = withExpiry.map(i => i.expiry_date);
    const uniqueDates = new Set(dates);
    const uniqueLocs  = new Set(withExpiry.map(i => i.location));

    // Mostra hint se scadenze diverse OPPURE stessa scadenza ma luoghi diversi
    if (uniqueDates.size < 2 && uniqueLocs.size < 2) { hintEl.style.display = 'none'; return; }

    // Trova il più vicino alla scadenza
    withExpiry.sort((a, b) => {
        const da = parseLocalExpiryDate(a.expiry_date);
        const db = parseLocalExpiryDate(b.expiry_date);
        return (da ? da.getTime() : Infinity) - (db ? db.getTime() : Infinity);
    });
    const soonest = withExpiry[0];
    const expDate = parseLocalExpiryDate(soonest.expiry_date);
    if (!expDate || Number.isNaN(expDate.getTime())) { hintEl.style.display = 'none'; return; }

    const today = new Date(); today.setHours(0,0,0,0);
    const diffDays = Math.round((expDate - today) / 86400000);

    const locInfo  = LOCATIONS[soonest.location] || { icon: '📦', label: soonest.location };
    const dateStr  = expDate.toLocaleDateString(_currentLang === 'de' ? 'de-DE' : _currentLang === 'en' ? 'en-GB' : 'it-IT', { day: '2-digit', month: '2-digit' });

    let whenStr;
    if (diffDays < 0)       whenStr = t('use.when_expired').replace('{n}', -diffDays);
    else if (diffDays === 0) whenStr = t('use.when_today');
    else if (diffDays === 1) whenStr = t('use.when_tomorrow');
    else                     whenStr = t('use.when_days').replace('{n}', diffDays);

    const locLabel = uniqueLocs.size > 1
        ? ` (${locInfo.icon} ${locInfo.label})`
        : '';

    hintEl.innerHTML = t('use.expiry_warning').replace('{loc}', locLabel).replace('{date}', `<strong>${dateStr}</strong>`).replace('{when}', whenStr);
    hintEl.style.display = 'block';
}

function _isOpenedInventoryItem(item) {
    const q = parseFloat(item.quantity);
    const dq = parseFloat(item.default_quantity) || 0;
    if (item.unit === 'conf' && dq > 0) return q !== Math.floor(q);
    if (dq > 0) return Math.abs(q - Math.round(q / dq) * dq) > dq * 0.02;
    return false;
}

function _locationHasOpenedPackage(items, location) {
    return items.some(i => i.location === location && _isOpenedInventoryItem(i));
}

async function loadUseInventoryInfo() {
    try {
        const data = await api('inventory_list');
        const items = (data.inventory || []).filter(i => i.product_id == currentProduct.id);
        _useCurrentItems = items; // cache for submitUseAll context detection
        const infoEl = document.getElementById('use-inventory-info');
        const unitSwitch = document.getElementById('use-unit-switch');
        
        if (items.length === 0) {
            infoEl.innerHTML = t('use.not_in_inventory');
            unitSwitch.style.display = 'none';
            _useConfMode = null;
            document.getElementById('use-expiry-hint').style.display = 'none';
            return;
        }

        // ── Suggerisci quale confezione usare per prima ──────────────────
        _renderUseExpiryHint(items);
        // ─────────────────────────────────────────────────────────────────

        // Auto-select the location with an opened package first (use from opened before sealed)
        const openedItem = items.find(_isOpenedInventoryItem);
        const firstLoc = openedItem ? openedItem.location : items[0].location;
        
        // Build location buttons only for locations where the product exists
        const productLocations = [...new Set(items.map(i => i.location))];
        const locSelector = document.getElementById('use-location-selector');

        // Prefer the remembered location (if confirmed), else use the opened-package heuristic
        const prefLoc = _getPreferredUseLocation(currentProduct.id);
        const activeLoc = (prefLoc && productLocations.includes(prefLoc)) ? prefLoc : firstLoc;
        document.getElementById('use-location').value = activeLoc;

        // Builder for the full set of location buttons
        const buildLocButtons = (active) => productLocations.map(loc => {
            const locInfo = LOCATIONS[loc] || { icon: '📦', label: loc };
            const locItems = items.filter(i => i.location === loc);
            const locQty = locItems.reduce((s, i) => s + parseFloat(i.quantity), 0);
            const u = locItems[0].unit || 'pz';
            const qtyLabel = formatQuantity(locQty, u, locItems[0].default_quantity, locItems[0].package_unit);
            const openedBadge = _locationHasOpenedPackage(items, loc)
                ? ` <span class="loc-opened-badge">🔓 ${t('use.opened_badge')}</span>`
                : '';
            return `<button type="button" class="loc-btn ${loc === active ? 'active' : ''}${openedBadge ? ' loc-btn-opened' : ''}" onclick="selectUseLocation(this, '${loc}')">${locInfo.icon} ${locInfo.label}${openedBadge}<br><small>${qtyLabel}</small></button>`;
        }).join('');

        if (prefLoc && productLocations.includes(prefLoc) && productLocations.length > 1) {
            // Confirmed preference → show collapsed row + hidden full picker
            const locInfo = LOCATIONS[prefLoc] || { icon: '📦', label: prefLoc };
            locSelector.innerHTML = `
                <div class="pref-loc-info" id="pref-loc-info">
                    <span class="pref-loc-name">${locInfo.icon} ${locInfo.label}</span>
                    <button type="button" class="btn-link pref-loc-change" onclick="_expandUseLocationSelector()">${t('use.change')}</button>
                </div>
                <div id="pref-loc-full" style="display:none">${buildLocButtons(activeLoc)}</div>
            `;
        } else {
            locSelector.innerHTML = buildLocButtons(activeLoc);
        }


        const unit = items[0].unit || 'pz';
        const pkgSize = parseFloat(items[0].default_quantity) || 0;
        const pkgUnit = items[0].package_unit || '';
        const isConf = unit === 'conf' && pkgSize > 0 && pkgUnit;

        if (isConf) {
            // --- CONF MODE: show sub-unit controls ---
            const totalConf = items.reduce((s, i) => s + parseFloat(i.quantity), 0);
            const totalSub = totalConf * pkgSize;
            const unitLabels = { 'ml': 'ml', 'g': 'g', 'pz': 'pz' };
            const subLabel = unitLabels[pkgUnit] || pkgUnit;

            _useConfMode = { packageSize: pkgSize, packageUnit: pkgUnit, totalSub, totalConf, subLabel };

            // Show inventory info with sub-unit total
            infoEl.innerHTML = `<strong>${t('use.available')}</strong> ` + items.map(i => {
                const loc = LOCATIONS[i.location] || { icon: '📦', label: i.location };
                const confQty = parseFloat(i.quantity);
                const subQty = Math.round(confQty * pkgSize);
                const confDisplay = confQty === Math.floor(confQty) ? Math.floor(confQty) : confQty.toFixed(1);
                return `${loc.icon} ${loc.label}: ${confDisplay} conf (${subQty}${subLabel})`;
            }).join(' · ');

            // Show unit switch
            unitSwitch.style.display = 'flex';
            document.getElementById('use-unit-sub').textContent = subLabel;
            
            // Default to sub-unit mode
            switchUseUnit('sub');
            // Trigger a live-box refresh with the latest reading if on scale
            if (_scaleLatestWeight) _scaleAutoFillUse(_scaleLatestWeight);
        } else {
            // --- NORMAL MODE ---
            _useConfMode = null;
            _useNormalUnit = unit;
            unitSwitch.style.display = 'none';
            // Trigger a live-box refresh with the latest reading if on scale
            if (_scaleLatestWeight) _scaleAutoFillUse(_scaleLatestWeight);
            
            infoEl.innerHTML = `<strong>${t('use.available')}</strong> ` + items.map(i => {
                const loc = LOCATIONS[i.location] || { icon: '📦', label: i.location };
                const qLabel = formatQuantity(parseFloat(i.quantity), i.unit, i.default_quantity, i.package_unit);
                return `${loc.icon} ${loc.label}: ${qLabel}`;
            }).join(' · ');
            
            const qtyInput = document.getElementById('use-quantity');
            qtyInput.value = 1;
            qtyInput.step = 'any';
            qtyInput.min = '0.01';
            document.getElementById('use-partial-hint').textContent = t('use.partial_hint');

            // Fraction buttons for pz unit
            const existingFrac = document.getElementById('pz-fraction-btns');
            if (existingFrac) existingFrac.remove();
            if (unit === 'pz') {
                const fracDiv = document.createElement('div');
                fracDiv.id = 'pz-fraction-btns';
                fracDiv.className = 'pz-fraction-btns';
                fracDiv.innerHTML = `
                    <p class="form-hint">${t('use.partial_piece_hint')}</p>
                    <div class="fraction-btn-row">
                        <button type="button" class="frac-btn" data-frac="0.25" onclick="setPzFraction(0.25)">¼ ${t('use.piece')}</button>
                        <button type="button" class="frac-btn" data-frac="0.5" onclick="setPzFraction(0.5)">½ ${t('use.piece')}</button>
                        <button type="button" class="frac-btn" data-frac="0.75" onclick="setPzFraction(0.75)">¾ ${t('use.piece')}</button>
                        <button type="button" class="frac-btn active" data-frac="1" onclick="setPzFraction(1)">${t('use.one_whole')}</button>
                    </div>`;
                document.querySelector('#page-use .use-partial').appendChild(fracDiv);
            }
        }
    } catch(e) {
        console.error(e);
    }
}

function switchUseUnit(mode) {
    const subBtn = document.getElementById('use-unit-sub');
    const confBtn = document.getElementById('use-unit-conf');
    const qtyInput = document.getElementById('use-quantity');
    const hint = document.getElementById('use-partial-hint');

    if (mode === 'sub') {
        subBtn.classList.add('active');
        confBtn.classList.remove('active');
        _useConfMode._activeUnit = 'sub';
        const step = getSubUnitStep(_useConfMode.packageUnit);
        qtyInput.value = step;
        qtyInput.step = 'any';
        qtyInput.min = 1;
        hint.textContent = t('recipes.quantity_in_total', { unit: _useConfMode.subLabel, total: `${Math.round(_useConfMode.totalSub)}${_useConfMode.subLabel}` });
    } else {
        confBtn.classList.add('active');
        subBtn.classList.remove('active');
        _useConfMode._activeUnit = 'conf';
        qtyInput.value = 1;
        qtyInput.step = 'any';
        qtyInput.min = 0.1;
        hint.textContent = t('recipes.packs_of_have', { size: `${_useConfMode.packageSize}${_useConfMode.subLabel}`, count: _useConfMode.totalConf.toFixed(1) });
    }
}

function getSubUnitStep(pkgUnit) {
    switch (pkgUnit) {
        case 'ml': return 50;
        case 'g': return 10;
        default: return 1;
    }
}

function adjustUseQty(direction) {
    _scaleUserDismissed = true;
    _cancelScaleTimersOnly();
    const input = document.getElementById('use-quantity');
    let val = parseFloat(input.value) || 0;
    let step;
    if (_useConfMode && _useConfMode._activeUnit === 'sub') {
        step = getSubUnitStep(_useConfMode.packageUnit);
    } else if (_useConfMode && _useConfMode._activeUnit === 'conf') {
        step = 0.5;
    } else {
        // Unit-aware step for normal mode
        const u = _useNormalUnit || 'pz';
        if (u === 'g' || u === 'ml') {
            step = val < 50 ? 1 : (val < 500 ? 10 : 50);
        } else {
            step = 0.5; // pz: allow half-piece steps
        }
    }
    val = Math.max(step, val + direction * step);
    val = Math.round(val * 1000) / 1000;

    // Cap at max available at selected location (in current unit)
    const selectedLoc = document.getElementById('use-location')?.value;
    if (selectedLoc && _useCurrentItems.length > 0) {
        const locItems = _useCurrentItems.filter(i => i.location === selectedLoc);
        const maxQtyAtLoc = locItems.reduce((s, i) => s + parseFloat(i.quantity || 0), 0);
        if (maxQtyAtLoc > 0) {
            // Convert to sub-unit for comparison if needed
            const maxInCurrentUnit = (_useConfMode && _useConfMode._activeUnit === 'sub')
                ? maxQtyAtLoc * _useConfMode.packageSize
                : maxQtyAtLoc;
            val = Math.min(val, Math.round(maxInCurrentUnit * 1000) / 1000);
        }
    }

    input.value = val;
    // Sync fraction button highlight if visible
    const newVal = parseFloat(input.value);
    document.querySelectorAll('#pz-fraction-btns .frac-btn').forEach(b => {
        b.classList.toggle('active', parseFloat(b.dataset.frac) === newVal);
    });
}

function selectUseLocation(btn, loc) {
    btn.parentElement.querySelectorAll('.loc-btn').forEach(b => b.classList.remove('active'));
    btn.classList.add('active');
    document.getElementById('use-location').value = loc;
}

// ── PREFERRED USE LOCATION ───────────────────────────────────────────────
// After 3+ consistent choices from the same location for a product,
// auto-selects it and hides the location picker (user can still tap "cambia").
const _PREF_LOC_KEY = '_prefUseLoc';
const _PREF_LOC_NEEDED = 3; // choices needed to confirm a preference

function _getPrefLocHistory(productId) {
    try {
        const all = JSON.parse(localStorage.getItem(_PREF_LOC_KEY) || '{}');
        return all[String(productId)] || [];
    } catch { return []; }
}

function _recordUseLocationChoice(productId, loc) {
    try {
        const all = JSON.parse(localStorage.getItem(_PREF_LOC_KEY) || '{}');
        const key = String(productId);
        const hist = all[key] || [];
        hist.push(loc);
        if (hist.length > 8) hist.splice(0, hist.length - 8); // keep last 8
        all[key] = hist;
        localStorage.setItem(_PREF_LOC_KEY, JSON.stringify(all));
    } catch { }
}

function _getPreferredUseLocation(productId) {
    const hist = _getPrefLocHistory(productId);
    if (hist.length < _PREF_LOC_NEEDED) return null;
    const recent = hist.slice(-5); // look at last 5
    const counts = {};
    for (const loc of recent) counts[loc] = (counts[loc] || 0) + 1;
    const [topLoc, topCount] = Object.entries(counts).sort((a, b) => b[1] - a[1])[0];
    return topCount >= _PREF_LOC_NEEDED ? topLoc : null;
}

function _expandUseLocationSelector() {
    document.getElementById('pref-loc-info')?.style.setProperty('display', 'none');
    document.getElementById('pref-loc-full')?.style.removeProperty('display');
}
// ────────────────────────────────────────────────────────────────────────

function setPzFraction(frac) {
    document.getElementById('use-quantity').value = frac;
    document.querySelectorAll('#pz-fraction-btns .frac-btn').forEach(b => {
        b.classList.toggle('active', parseFloat(b.dataset.frac) === frac);
    });
}

// ===== LOW STOCK → BRING! PROMPT =====
function isLowStock(totalRemaining, unit, defaultQty) {
    if (totalRemaining <= 0) return true; // fully depleted → definitely needs restocking
    if (unit === 'pz') return totalRemaining <= 1; // only 1 piece left
    if (unit === 'conf') return totalRemaining < 1; // only warn when less than 1 full pack remains (opened/partial)
    // Weight/volume: use percentage of default_qty or fixed threshold
    if (defaultQty > 0) return totalRemaining <= defaultQty * 0.25;
    // Fallback fixed thresholds
    if (unit === 'g' || unit === 'ml') return totalRemaining <= 100;
    return false;
}

/**
 * Return the significant tokens of a product name for similarity matching.
 * Strips stopwords and short tokens.
 */
function _nameTokens(name) {
    const stop = new Set(['di','del','della','dei','degli','delle','da','in','con','per','su','a','e','il','lo','la','i','gli','le','un','uno','una','al','alle','agli','allo']);
    return (name || '').toLowerCase()
        .replace(/[^a-z\u00c0-\u024f\s]/gi, ' ')
        .split(/\s+/)
        .filter(t => t.length > 2 && !stop.has(t));
}

/**
 * Check whether `name` matches any item in `list` (array of {name}).
 * Returns the matching item or null.
 * A match = at least one significant token in common.
 * NOTE: intentionally loose — use _matchBringToSmart for display/urgency matching.
 */
function _findSimilarItem(name, list) {
    const tokens = _nameTokens(name);
    if (tokens.length === 0) return null;
    return (list || []).find(item => {
        const iTokens = _nameTokens(item.name || '');
        return tokens.some(t => iTokens.includes(t));
    }) || null;
}

/**
 * Strict matching: find the smart item that corresponds to a Bring item by name.
 * Rules (in order):
 *  1. Exact case-insensitive match.
 *  2. First significant token of both names must be identical
 *     ("Latte" → "Latte Parzialmente Scremato" ✓; "Frutta" ≠ "Muesli Frutta Secca" ✗).
 *  3. For multi-token Bring names: all Bring tokens appear in the smart item tokens.
 * This avoids false positives when a generic word ("frutta", "noci") appears as a
 * secondary word inside an unrelated long product name.
 */
function _matchBringToSmart(bringName, smartItems) {
    const bLower = bringName.toLowerCase();
    const exact = smartItems.find(sd => sd.name.toLowerCase() === bLower);
    if (exact) return exact;
    const bTokens = _nameTokens(bringName);
    if (bTokens.length === 0) return null;
    const bFirst = bTokens[0];
    // Rule 2: first token match
    const firstMatch = smartItems.find(sd => {
        const sdTokens = _nameTokens(sd.name);
        return sdTokens.length > 0 && sdTokens[0] === bFirst;
    });
    if (firstMatch) return firstMatch;
    // Rule 3: multi-token full subset
    if (bTokens.length >= 2) {
        const allMatch = smartItems.find(sd => {
            const sdTokens = _nameTokens(sd.name);
            return bTokens.every(t => sdTokens.includes(t));
        });
        if (allMatch) return allMatch;
    }
    return null;
}

function showLowStockBringPrompt(result, afterCallback) {
    const name = result.product_name || currentProduct?.name || '';
    // Generic shopping name (e.g. "Affettato" for "Mortadella IGP"). Falls back to
    // the specific name when shopping_name is not set (older API call), so behaviour
    // is unchanged for legacy callers.
    const shoppingName = result.product_shopping_name || name;
    const unit = result.product_unit || currentProduct?.unit || 'pz';
    const defaultQty = result.product_default_qty || parseFloat(currentProduct?.default_quantity) || 0;
    const totalRemaining = result.total_remaining;
    
    // ── Fully depleted: no need to ask — backend already added to Bring! ──
    // Skip the modal entirely and proceed to the next step (e.g. move modal).
    if (totalRemaining <= 0) {
        // Backend auto-adds to Bring! when fully depleted. If it failed (Bring not
        // configured, or product already on list), silently attempt it from JS.
        if (!result.added_to_bring && shoppingName) {
            // Fire-and-forget — don't block the callback
            // Use generic shopping name; specific name goes into specification.
            const spec = shoppingName !== name ? name + (result.product_brand ? ` · ${result.product_brand}` : '') : '';
            (async () => {
                try {
                    const payload = { items: [{ name: shoppingName, specification: spec }] };
                    if (shoppingListUUID) payload.listUUID = shoppingListUUID;
                    const data = await api('bring_add', {}, 'POST', payload);
                    if (data.success && data.added > 0) {
                        showToast('🛒 Prodotto finito → aggiunto a Bring!', 'info');
                    }
                } catch(_e) { /* silent */ }
            })();
        }
        if (afterCallback) afterCallback();
        return;
    }
    
    if (!isLowStock(totalRemaining, unit, defaultQty)) {
        if (afterCallback) afterCallback();
        return;
    }
    
    // Format remaining for display
    let remainLabel = '';
    if (unit === 'conf' && result.product_package_unit) {
        const subTotal = Math.round(totalRemaining * defaultQty);
        remainLabel = `${subTotal}${result.product_package_unit}`;
    } else {
        const unitLabels = { pz: 'pz', g: 'g', ml: 'ml', conf: 'conf' };
        remainLabel = `${Number.isInteger(totalRemaining) ? totalRemaining : totalRemaining.toFixed(1)} ${unitLabels[unit] || unit}`;
    }

    // --- Deduplication check ---
    // 1. Already on Bring! list (shoppingItems)?
    const alreadyOnBring = _findSimilarItem(shoppingName, shoppingItems) || _findSimilarItem(name, shoppingItems);
    if (alreadyOnBring) {
        // Already present (same or similar item). Just inform and continue.
        showToast(t('shopping.already_in_list', { name: escapeHtml(alreadyOnBring.name) }), 'info');
        if (afterCallback) afterCallback();
        return;
    }

    // 2. In smart shopping predictions?
    const smartMatch = _findSimilarItem(shoppingName, smartShoppingItems) || _findSimilarItem(name, smartShoppingItems);
    const smartUrgencyLabel = {
        critical: t('shopping.urgency_critical'), high: t('shopping.urgency_high'),
        medium: t('shopping.urgency_medium'), low: t('shopping.urgency_low')
    };
    let smartNote = '';
    if (smartMatch) {
        const lbl = smartUrgencyLabel[smartMatch.urgency] || '';
        const _smartMsg = t('shopping.smart_already_predicted').replace('{name}', escapeHtml(smartMatch.name)).replace('{urgency}', lbl ? ` (${lbl})` : '');
        smartNote = `<div style="margin-bottom:12px;padding:8px 10px;background:rgba(249,115,22,0.1);border-radius:8px;border-left:3px solid #f97316;font-size:0.85rem">
            ${_smartMsg}
        </div>`;
    }

    // _lowStockName = generic name that goes into Bring! (e.g. "Affettato")
    // _lowStockSpec = specific product name used as specification (e.g. "Mortadella IGP")
    window._lowStockAfterCallback = afterCallback;
    window._lowStockName = shoppingName;
    window._lowStockSpec = shoppingName !== name
        ? name + (result.product_brand ? ` · ${result.product_brand}` : '')
        : name;
    
    document.getElementById('modal-content').innerHTML = `
        <div class="modal-header">
            <h3>⚠️ Sta per finire!</h3>
            <button class="modal-close" onclick="closeLowStockPrompt()">✕</button>
        </div>
        <div style="padding:0 16px 16px">
            <p style="margin-bottom:12px">${t('lowstock.message').replace('{name}', `<strong>${escapeHtml(name)}</strong>`).replace('{qty}', `<strong>${remainLabel}</strong>`)}</p>
            ${smartNote}
            <p style="margin-bottom:16px">${t('lowstock.question')}</p>
            <button type="button" class="btn btn-large btn-success full-width" onclick="addLowStockToBring()">
                ${t('lowstock.yes')}
            </button>
            <button type="button" class="btn btn-secondary full-width" style="margin-top:8px" onclick="closeLowStockPrompt()">
                ${t('lowstock.no')}
            </button>
        </div>
    `;
    document.getElementById('modal-overlay').style.display = 'flex';
}

async function addLowStockToBring() {
    closeModal();
    try {
        // Use the generic shopping name (e.g. "Affettato") set by showLowStockBringPrompt.
        // _lowStockSpec holds the specific product name (e.g. "Mortadella IGP · Marca").
        const bringName = window._lowStockName || '';
        const spec = window._lowStockSpec || '';
        window._lowStockName = null;
        window._lowStockSpec = null;
        const payload = { items: [{ name: bringName, specification: spec }] };
        if (shoppingListUUID) payload.listUUID = shoppingListUUID;
        const data = await api('bring_add', {}, 'POST', payload);
        if (data.success && data.added > 0) {
            showToast(t('shopping.added_to_bring').replace('{n}', data.added), 'success');
        } else if (data.success && data.skipped > 0) {
            showToast(t('shopping.already_in_list_short'), 'info');
        }
    } catch (e) {
        showToast(t('error.bring_add'), 'error');
    }
    const cb = window._lowStockAfterCallback;
    window._lowStockAfterCallback = null;
    if (cb) cb();
}

function closeLowStockPrompt() {
    closeModal();
    const cb = window._lowStockAfterCallback;
    window._lowStockAfterCallback = null;
    if (cb) cb();
}

let _moveModalTimer = null;
let _moveModalRAF = null;

function clearMoveModalTimer() {
    if (_moveModalTimer) { clearTimeout(_moveModalTimer); _moveModalTimer = null; }
    if (_moveModalRAF) { cancelAnimationFrame(_moveModalRAF); _moveModalRAF = null; }
}

function startMoveModalCountdown(btnId, onExpire) {
    clearMoveModalTimer();
    const duration = 15000;
    const start = performance.now();
    const btn = document.getElementById(btnId);
    if (!btn) return;
    function tick() {
        const elapsed = performance.now() - start;
        const pct = Math.max(0, 100 - (elapsed / duration) * 100);
        btn.style.background = `linear-gradient(to right, rgba(45,80,22,0.2) ${pct}%, transparent ${pct}%)`;
        if (elapsed < duration) {
            _moveModalRAF = requestAnimationFrame(tick);
        }
    }
    _moveModalRAF = requestAnimationFrame(tick);
    _moveModalTimer = setTimeout(() => {
        clearMoveModalTimer();
        onExpire();
    }, duration);
}

function showMoveAfterUseModal(product, fromLoc, remaining, openedId) {
    const otherLocs = Object.entries(LOCATIONS).filter(([k]) => k !== fromLoc);
    const locButtons = otherLocs.map(([k, v]) =>
        `<button type="button" class="loc-btn" onclick="clearMoveModalTimer();confirmMoveAfterUse(${product.id}, '${fromLoc}', '${k}', ${openedId || 0})">${v.icon} ${v.label}</button>`
    ).join('');
    const wasVacuum = !!product.vacuum_sealed;
    const vacuumRow = wasVacuum ? `
        <label style="display:flex;align-items:center;gap:8px;margin-top:12px;cursor:pointer">
            <input type="checkbox" id="move-vacuum-check" checked>
            <span>${t('move.vacuum_restore')}</span>
        </label>` : '';
    document.getElementById('modal-content').innerHTML = `
        <div class="modal-header">
            <h3>${t('move.title')}</h3>
            <button class="modal-close" onclick="clearMoveModalTimer();closeModal();showPage('dashboard')">✕</button>
        </div>
        <div style="padding:0 16px 16px">
            <p style="margin-bottom:12px">${t('move.question').replace('{thing}', openedId ? t('move.thing_opened') : t('move.thing_rest')).replace('{name}', `<strong>${escapeHtml(product.name)}</strong>`)}</p>
            <div class="location-selector">${locButtons}</div>
            ${vacuumRow}
            <button type="button" id="btn-move-stay" class="btn btn-secondary full-width move-countdown-btn" style="margin-top:12px" onclick="clearMoveModalTimer();closeModal();showPage('dashboard')">${t('move.stay_btn').replace('{location}', LOCATIONS[fromLoc]?.label || fromLoc)}</button>
        </div>
    `;
    document.getElementById('modal-overlay').style.display = 'flex';
    startMoveModalCountdown('btn-move-stay', () => { closeModal(); showPage('dashboard'); });
}

async function confirmMoveAfterUse(productId, fromLoc, toLoc, openedId) {
    clearMoveModalTimer();
    const newVacuum = document.getElementById('move-vacuum-check')?.checked ? 1 : 0;
    closeModal();
    showLoading(true);
    try {
        if (openedId) {
            // Move only the specific opened row — use opened shelf life
            const product = { name: currentProduct?.name || '', category: currentProduct?.category || '' };
            let days = estimateOpenedExpiryDays(product, toLoc);
            await api('inventory_update', {}, 'POST', {
                id: openedId,
                location: toLoc,
                expiry_date: addDays(days),
                product_id: productId,
                vacuum_sealed: newVacuum,
            });
            showToast(t('move.moved_toast').replace('{location}', LOCATIONS[toLoc]?.label || toLoc), 'success');
        } else {
            // Legacy: move whatever is at fromLoc
            const data = await api('inventory_list');
            const item = (data.inventory || []).find(i => i.product_id == productId && i.location === fromLoc && parseFloat(i.quantity) > 0);
            if (item) {
                const product = { name: item.name || '', category: item.category || '' };
                let days = estimateExpiryDays(product, toLoc);
                if (newVacuum) days = getVacuumExpiryDays(days);
                await api('inventory_update', {}, 'POST', {
                    id: item.id,
                    location: toLoc,
                    expiry_date: addDays(days),
                    product_id: productId,
                    vacuum_sealed: newVacuum,
                });
                showToast(`📦 Spostato in ${LOCATIONS[toLoc]?.label || toLoc}`, 'success');
            }
        }
    } catch (e) {
        console.error('Move error:', e);
    }
    showLoading(false);
    showPage('dashboard');
}

async function submitUseAll() {
    // Gate: show a countdown-confirmation before the destructive use_all call
    const name = currentProduct ? currentProduct.name : '';
    const items0 = _useCurrentItems ? _useCurrentItems.filter(i => parseFloat(i.quantity) > 0) : [];
    const totalQty = items0.reduce((s, i) => s + parseFloat(i.quantity || 0), 0);
    const unit = items0[0]?.unit || 'pz';
    const qtyStr = formatQuantity(totalQty, unit, items0[0]?.default_quantity, items0[0]?.package_unit);
    _showDestructiveConfirm(
        t('use.use_all_confirm_title') || '✅ Finisci tutto',
        `${t('use.use_all_confirm_msg') || 'Conferma che hai finito tutto il prodotto:'} "${name}" (${qtyStr})`,
        _doSubmitUseAll,
        t('use.use_all_confirm_btn') || '✅ Sì, finito'
    );
}

async function _doSubmitUseAll() {
    showLoading(true);
    try {
        const currentLoc = document.getElementById('use-location')?.value || '__all__';
        const items = _useCurrentItems.filter(i => parseFloat(i.quantity) > 0);

        const openedAtCurrentLoc = items.find(i => i.location === currentLoc && _isOpenedInventoryItem(i));
        const allOpened = items.filter(_isOpenedInventoryItem);

        let useLocation;

        if (openedAtCurrentLoc) {
            // Opened package at the currently selected location → finish only the opened item.
            // The PHP backend fetches fractional (opened) rows first, so use_all on a specific
            // location will clear the opened row and leave sealed packages untouched.
            useLocation = currentLoc;
        } else if (allOpened.length === 1) {
            // One opened package somewhere else → almost certainly this is what the user means
            useLocation = allOpened[0].location;
        } else if (allOpened.length > 1) {
            // Multiple opened packages at different locations → ask the user
            showLoading(false);
            _showUseAllDisambiguation(allOpened, items);
            return;
        } else {
            // No opened packages anywhere → finish everything (original behaviour)
            useLocation = '__all__';
        }

        const isOpenedFinish = useLocation !== '__all__' && items.some(
            i => i.location === useLocation && _isOpenedInventoryItem(i)
        );

        const result = await api('inventory_use', {}, 'POST', {
            product_id: currentProduct.id,
            use_all: true,
            location: useLocation,
        });
        showLoading(false);
        if (result.success) {
            const toastMsg = isOpenedFinish
                ? `🔓 ${t('use.toast_opened_finished').replace('{name}', currentProduct.name)}`
                : `📤 ${currentProduct.name} terminato!`;
            showToast(toastMsg, 'success');
            if (result.added_to_bring) {
                setTimeout(() => showToast(t('use.toast_bring'), 'info'), 1500);
            }
            // Check low stock (product may exist at other locations)
            showLowStockBringPrompt(result, () => showPage('dashboard'));
        } else {
            showToast(result.error || 'Errore', 'error');
        }
    } catch (err) {
        showLoading(false);
        showToast(t('error.connection'), 'error');
    }
}

/**
 * Show a modal asking which opened package to mark as finished.
 * Called when multiple opened packages exist across different locations.
 */
function _showUseAllDisambiguation(openedItems, allItems) {
    const contentEl = document.getElementById('modal-content');
    const locButtons = openedItems.map(item => {
        const locInfo = LOCATIONS[item.location] || { icon: '📦', label: item.location };
        const qtyStr = formatQuantity(parseFloat(item.quantity), item.unit, item.default_quantity, item.package_unit);
        return `<button class="btn btn-warning full-width" style="justify-content:flex-start;gap:10px;text-align:left;margin-bottom:8px"
            onclick="closeModal(); _submitUseAllAt('${item.location}', true)">
            <span style="font-size:1.3rem">${locInfo.icon}</span>
            <span><strong>${locInfo.label}</strong> — 🔓 ${t('use.opened_badge')}<br>
            <small style="opacity:0.8">${qtyStr}</small></span>
        </button>`;
    }).join('');

    // Option to finish everything
    const totalQty = allItems.reduce((s, i) => s + parseFloat(i.quantity), 0);
    const unit = allItems[0]?.unit || 'pz';
    const defaultQty = allItems[0]?.default_quantity;
    const pkgUnit = allItems[0]?.package_unit;
    const totalStr = formatQuantity(totalQty, unit, defaultQty, pkgUnit);

    contentEl.innerHTML = `
        <div class="modal-header">
            <h3>${t('use.use_all')}</h3>
            <button class="modal-close" onclick="closeModal()">✕</button>
        </div>
        <p style="font-size:0.9rem;color:var(--text-muted);margin:0 0 14px">${t('use.disambiguation_hint')}</p>
        ${locButtons}
        <button class="btn btn-danger full-width" style="margin-top:4px"
            onclick="closeModal(); _submitUseAllAt('__all__', false)">
            🗑️ ${t('use.disambiguation_all').replace('{qty}', totalStr)}
        </button>
    `;
    document.getElementById('modal-overlay').style.display = 'flex';
}

async function _submitUseAllAt(location, isOpenedOnly) {
    showLoading(true);
    try {
        const result = await api('inventory_use', {}, 'POST', {
            product_id: currentProduct.id,
            use_all: true,
            location,
        });
        showLoading(false);
        if (result.success) {
            const toastMsg = isOpenedOnly
                ? `🔓 ${t('use.toast_opened_finished').replace('{name}', currentProduct.name)}`
                : `📤 ${currentProduct.name} terminato!`;
            showToast(toastMsg, 'success');
            if (result.added_to_bring) {
                setTimeout(() => showToast(t('use.toast_bring'), 'info'), 1500);
            }
            showLowStockBringPrompt(result, () => showPage('dashboard'));
        } else {
            showToast(result.error || 'Errore', 'error');
        }
    } catch (err) {
        showLoading(false);
        showToast(t('error.connection'), 'error');
    }
}

async function submitUse(e) {
    e.preventDefault();
    if (_useSubmitting) return; // prevent double-submit from scale auto-confirm
    _useSubmitting = true;
    _cancelScaleTimersOnly();
    _scaleStabilityVal = null;
    showLoading(true);
    try {
        let qty = parseFloat(document.getElementById('use-quantity').value) || 1;
        let displayQty = qty;
        let displayUnit = '';
        
        // Convert sub-unit to conf if needed
        if (_useConfMode && _useConfMode._activeUnit === 'sub') {
            displayUnit = _useConfMode.subLabel;
            qty = qty / _useConfMode.packageSize; // convert to conf
        } else if (_useConfMode && _useConfMode._activeUnit === 'conf') {
            displayUnit = 'conf';
        }

        // ── Validate: cannot use more than available at selected location ─────────
        const selectedLoc = document.getElementById('use-location').value;
        const locItems = _useCurrentItems.filter(i => i.location === selectedLoc);
        const maxQtyAtLoc = locItems.reduce((s, i) => s + parseFloat(i.quantity || 0), 0);
        if (maxQtyAtLoc > 0 && qty > maxQtyAtLoc + 0.001) {
            showLoading(false);
            _useSubmitting = false;
            showToast(t('use.error_exceeds_stock'), 'error');
            // Shake the input to make it obvious
            const inp = document.getElementById('use-quantity');
            inp.classList.add('input-shake');
            setTimeout(() => inp.classList.remove('input-shake'), 600);
            return;
        }
        // ─────────────────────────────────────────────────────────────────────────
        
        const result = await api('inventory_use', {}, 'POST', {
            product_id: currentProduct.id,
            quantity: qty,
            location: document.getElementById('use-location').value,
        });
        showLoading(false);
        _useSubmitting = false;
        if (result.success) {
            const usedText = displayUnit ? `${displayQty}${displayUnit}` : displayQty;
            showToast(t('use.toast_used').replace('{qty}', usedText).replace('{name}', currentProduct.name), 'success');
            if (result.added_to_bring) {
                setTimeout(() => showToast('🛒 Prodotto finito → aggiunto a Bring!', 'info'), 1500);
            }
            // If there's remaining quantity, offer to move to another location
            const usedFrom = document.getElementById('use-location').value;
            _recordUseLocationChoice(currentProduct.id, usedFrom); // track for preferred-location feature
            const moveCallback = result.remaining > 0
                ? () => showMoveAfterUseModal(currentProduct, usedFrom, result.remaining, result.opened_id)
                : () => showPage('dashboard');
            // Check low stock → Bring! prompt
            showLowStockBringPrompt(result, moveCallback);
        } else if (result.duplicate) {
            // Silently ignore: this was a scale double-trigger, not a real error
        } else {
            showToast(result.error || 'Errore', 'error');
        }
    } catch (err) {
        showLoading(false);
        _useSubmitting = false;
        showToast(t('error.connection'), 'error');
    }
}

// ===== AI IDENTIFICATION =====
async function captureForAI() {
    if (!_requireGemini()) return;
    stopScanner();
    showPage('ai');
}

async function initAICamera() {
    const video = document.getElementById('ai-video');
    const captureDiv = document.getElementById('ai-capture');
    const previewDiv = document.getElementById('ai-preview');
    const captureBtn = document.getElementById('ai-capture-btn');
    const retakeBtn = document.getElementById('ai-retake-btn');
    const resultDiv = document.getElementById('ai-result');
    
    captureDiv.style.display = 'block';
    previewDiv.style.display = 'none';
    captureBtn.style.display = 'block';
    retakeBtn.style.display = 'none';
    resultDiv.style.display = 'none';
    
    try {
        if (aiStream) {
            aiStream.getTracks().forEach(t => t.stop());
        }
        aiStream = await navigator.mediaDevices.getUserMedia(getCameraConstraints());
        video.srcObject = aiStream;
        await video.play();
    } catch (err) {
        console.error('AI Camera error:', err);
        showToast('Impossibile accedere alla fotocamera', 'error');
    }
}

function takePhotoForAI() {
    const video = document.getElementById('ai-video');
    const canvas = document.getElementById('ai-canvas');
    const img = document.getElementById('ai-image');
    
    canvas.width = video.videoWidth;
    canvas.height = video.videoHeight;
    const ctx = canvas.getContext('2d');
    ctx.drawImage(video, 0, 0);
    
    const dataUrl = canvas.toDataURL('image/jpeg', 0.85);
    img.src = dataUrl;
    
    // Stop camera
    if (aiStream) {
        aiStream.getTracks().forEach(t => t.stop());
        aiStream = null;
    }
    video.srcObject = null;
    
    document.getElementById('ai-capture').style.display = 'none';
    document.getElementById('ai-preview').style.display = 'block';
    document.getElementById('ai-capture-btn').style.display = 'none';
    document.getElementById('ai-retake-btn').style.display = 'block';

    // Immediately start analysis
    analyzeWithAI();
}

function retakePhotoAI() {
    document.getElementById('ai-result').style.display = 'none';
    initAICamera();
}

async function analyzeWithAI() {
    const resultDiv = document.getElementById('ai-result');
    resultDiv.style.display = 'block';
    resultDiv.innerHTML = `<div style="text-align:center;padding:20px"><div class="loading-spinner" style="margin:0 auto 12px"></div><p>${t('scanner.ai_identifying')}</p></div>`;

    const canvas = document.getElementById('ai-canvas');
    const base64 = canvas.toDataURL('image/jpeg', 0.7).split(',')[1];

    try {
        const result = await api('gemini_identify', {}, 'POST', { image: base64 });

        if (!result.success) {
            if (result.error === 'no_api_key') {
                resultDiv.innerHTML = `<p style="color:var(--warning)">⚠️ Chiave API Gemini non configurata.<br><small>Aggiungi GEMINI_API_KEY nel file .env sul server.</small></p>`;
            } else if (/resource.?exhaust|quota|rate.?limit/i.test(result.error || '')) {
                resultDiv.innerHTML = `<p style="color:var(--warning)">⏳ ${t('error.ai_quota')}</p>
                    <button class="btn btn-secondary full-width mt-2" onclick="retakePhotoAI()">${t('btn.retry')}</button>`;
            } else {
                resultDiv.innerHTML = `<p style="color:var(--danger)">❌ ${escapeHtml(result.error || t('error.identification'))}</p>
                    <button class="btn btn-secondary full-width mt-2" onclick="retakePhotoAI()">${t('btn.retry')}</button>`;
            }
            return;
        }

        const id = result.identified;
        const matches = result.off_matches || [];

        // Search local DB for existing products that match the AI identification
        let localMatches = [];
        try {
            const nameWords = (id.name || '').split(/\s+/).filter(w => w.length > 2);
            const searches = [api('products_search', { q: id.name })];
            if (id.brand) searches.push(api('products_search', { q: id.brand }));
            const results = await Promise.all(searches);
            const seen = new Set();
            results.forEach(r => {
                (r.products || []).forEach(p => {
                    if (!seen.has(p.id)) {
                        seen.add(p.id);
                        localMatches.push(p);
                    }
                });
            });
        } catch(e) { /* ignore search errors */ }

        let html = `<h4>🤖 Prodotto identificato</h4>`;
        html += `<div class="ai-identified-card">`;
        html += `<strong>${escapeHtml(id.name)}</strong>`;
        if (id.brand) html += ` <span style="color:var(--text-muted)">- ${escapeHtml(id.brand)}</span>`;
        if (id.description) html += `<p style="font-size:0.85rem;color:var(--text-light);margin:4px 0 0">${escapeHtml(id.description)}</p>`;
        html += `</div>`;

        // Show existing local products first
        if (localMatches.length > 0) {
            html += `<h4 style="margin-top:16px">${t('product.already_in_pantry')}</h4>`;
            html += `<div class="ai-matches-list">`;
            localMatches.forEach((p, idx) => {
                html += `<div class="ai-match-item" onclick="selectLocalMatch(${p.id})">`;
                if (p.image_url) {
                    html += `<img src="${escapeHtml(p.image_url)}" alt="" class="ai-match-img" onerror="this.style.display='none'">`;
                }
                html += `<div class="ai-match-info">`;
                html += `<strong>${escapeHtml(p.name)}</strong>`;
                if (p.brand) html += `<br><small>${escapeHtml(p.brand)}</small>`;
                if (p.default_quantity && p.unit) html += `<br><small style="color:var(--text-muted)">${p.default_quantity} ${p.unit}</small>`;
                html += `</div>`;
                if (p.barcode) html += `<span class="ai-match-barcode">${p.barcode}</span>`;
                html += `</div>`;
            });
            html += `</div>`;
        }

        if (matches.length > 0) {
            html += `<h4 style="margin-top:16px">📦 Prodotti corrispondenti</h4>`;
            html += `<div class="ai-matches-list">`;
            matches.forEach((m, idx) => {
                html += `<div class="ai-match-item" onclick="selectAIMatch(${idx})">`;
                if (m.image_url) {
                    html += `<img src="${m.image_url}" alt="" class="ai-match-img" onerror="this.style.display='none'">`;
                }
                html += `<div class="ai-match-info">`;
                html += `<strong>${escapeHtml(m.name)}</strong>`;
                if (m.brand) html += `<br><small>${escapeHtml(m.brand)}</small>`;
                if (m.quantity_info) html += `<br><small style="color:var(--text-muted)">${escapeHtml(m.quantity_info)}</small>`;
                html += `</div>`;
                html += `<span class="ai-match-barcode">${m.barcode}</span>`;
                html += `</div>`;
            });
            html += `</div>`;
        }

        // Option to save as-is without barcode
        html += `<div style="margin-top:16px; border-top: 1px solid var(--bg-light); padding-top: 12px">`;
        html += `<button class="btn btn-secondary full-width" onclick="saveAIProductDirect()">🆕 Non è nessuno di questi — salva come nuovo</button>`;
        html += `</div>`;

        resultDiv.innerHTML = html;

        // Store data for later use
        window._aiIdentified = id;
        window._aiMatches = matches;

    } catch (err) {
        console.error('AI identify error:', err);
        resultDiv.innerHTML = `<p style="color:var(--danger)">❌ ${t('error.connection')}</p>
            <button class="btn btn-secondary full-width mt-2" onclick="retakePhotoAI()">${t('btn.retry')}</button>`;
    }
}

async function selectLocalMatch(productId) {
    showLoading(true);
    try {
        const result = await api('product_get', { id: productId });
        if (result.product) {
            currentProduct = result.product;
            showLoading(false);
            showProductAction();
        } else {
            showLoading(false);
            showToast(t('error.not_found'), 'error');
        }
    } catch (err) {
        showLoading(false);
        showToast(t('error.connection'), 'error');
    }
}

async function selectAIMatch(idx) {
    const match = window._aiMatches[idx];
    if (!match) return;

    showLoading(true);

    try {
        // Use the barcode to do a full lookup (gets all details)
        const localResult = await api('search_barcode', { barcode: match.barcode });
        if (localResult.found) {
            currentProduct = localResult.product;
            showLoading(false);
            showProductAction();
            return;
        }

        // Full lookup via OpenFoodFacts
        const lookupResult = await api('lookup_barcode', { barcode: match.barcode });
        if (lookupResult.found && lookupResult.product) {
            const p = lookupResult.product;
            const detected = detectUnitAndQuantity(p.quantity_info);

            const notesParts = [];
            if (p.quantity_info) notesParts.push(`Peso: ${p.quantity_info}`);
            if (p.nutriscore) notesParts.push(`Nutriscore: ${p.nutriscore.toUpperCase()}`);
            if (p.nova_group) notesParts.push(`NOVA: ${p.nova_group}`);
            if (p.ecoscore) notesParts.push(`Ecoscore: ${p.ecoscore.toUpperCase()}`);
            if (p.origin) notesParts.push(`Origine: ${p.origin}`);

            const saveResult = await api('product_save', {}, 'POST', {
                barcode: match.barcode,
                name: p.name || match.name,
                brand: p.brand || match.brand || '',
                category: p.category || '',
                image_url: p.image_url || match.image_url || '',
                unit: detected.unit,
                default_quantity: detected.quantity,
                notes: notesParts.join(' · '),
            });

            if (saveResult.id) {
                currentProduct = {
                    id: saveResult.id,
                    barcode: match.barcode,
                    name: p.name || match.name,
                    brand: p.brand || match.brand || '',
                    category: p.category || '',
                    image_url: p.image_url || match.image_url || '',
                    unit: detected.unit,
                    default_quantity: detected.quantity,
                    weight_info: p.quantity_info || '',
                };
                showLoading(false);
                showProductAction();
                return;
            }
        }

        // Fallback: save with basic info from match
        const saveResult = await api('product_save', {}, 'POST', {
            barcode: match.barcode,
            name: match.name,
            brand: match.brand || '',
            category: match.category || '',
            image_url: match.image_url || '',
            unit: 'pz',
            default_quantity: 1,
        });

        if (saveResult.id) {
            currentProduct = { id: saveResult.id, barcode: match.barcode, name: match.name, brand: match.brand || '', category: match.category || '', image_url: match.image_url || '', unit: 'pz', default_quantity: 1 };
            showLoading(false);
            showProductAction();
        } else {
            showLoading(false);
            showToast(t('error.save'), 'error');
        }
    } catch (err) {
        showLoading(false);
        console.error('AI match select error:', err);
        showToast(t('error.connection'), 'error');
    }
}

async function saveAIProductDirect() {
    const id = window._aiIdentified;
    if (!id) return;

    showLoading(true);
    try {
        const result = await api('product_save', {}, 'POST', {
            name: id.name,
            brand: id.brand || '',
            category: id.category || '',
            unit: 'pz',
            default_quantity: 1,
        });

        if (result.success || result.id) {
            currentProduct = { id: result.id, name: id.name, brand: id.brand || '', category: id.category || '', unit: 'pz', default_quantity: 1 };
            showLoading(false);
            showToast('Prodotto salvato!', 'success');
            showProductAction();
        } else {
            showLoading(false);
            showToast(result.error || t('error.save'), 'error');
        }
    } catch (err) {
        showLoading(false);
        showToast(t('error.connection'), 'error');
    }
}

// ===== AI PHOTO FILL FOR PRODUCT FORM =====
let _pfAiStream = null;

async function captureForAIFormFill() {
    if (!_requireGemini()) return;
    document.getElementById('modal-content').innerHTML = `
        <div class="modal-header">
            <h3>📷 ${t('scan.ai_identify')}</h3>
            <button class="modal-close" onclick="closePfAiScanner()">✕</button>
        </div>
        <div class="expiry-scanner">
            <div id="pfai-cam-container" style="position:relative;border-radius:10px;overflow:hidden;background:#000;aspect-ratio:4/3">
                <video id="pfai-video" autoplay playsinline style="width:100%;height:100%;object-fit:cover"></video>
                <canvas id="pfai-canvas" style="display:none"></canvas>
                <div style="position:absolute;inset:0;border:2px dashed rgba(255,255,255,0.4);border-radius:10px;pointer-events:none"></div>
            </div>
            <div id="pfai-preview-container" style="display:none;border-radius:10px;overflow:hidden;aspect-ratio:4/3">
                <img id="pfai-preview-img" src="" alt="" style="width:100%;height:100%;object-fit:cover">
            </div>
            <div id="pfai-status" style="display:none;text-align:center;padding:12px">
                <div class="loading-spinner" style="margin:0 auto 8px"></div>
                <p>${t('scanner.ai_identifying')}</p>
            </div>
            <div id="pfai-result" style="display:none"></div>
            <p class="form-hint" style="text-align:center;margin:6px 0;font-size:0.8rem" id="pfai-hint">${t('scanner.product_label_hint')}</p>
            <div style="display:flex;gap:8px;margin-top:8px">
                <button class="btn btn-large btn-accent" style="flex:1" id="pfai-capture-btn" onclick="pfAiCapture()">${t('scanner.capture_btn')}</button>
                <button class="btn btn-large btn-secondary" style="flex:1;display:none" id="pfai-retake-btn" onclick="pfAiRetake()">${t('scanner.retake_btn')}</button>
            </div>
        </div>
    `;
    document.getElementById('modal-overlay').style.display = 'flex';

    try {
        _pfAiStream = await navigator.mediaDevices.getUserMedia(getCameraConstraints());
        const video = document.getElementById('pfai-video');
        video.srcObject = _pfAiStream;
        await video.play();
    } catch (err) {
        document.getElementById('pfai-cam-container').innerHTML =
            `<p style="color:var(--danger);text-align:center;padding:20px">${t('error.camera')}</p>`;
    }
}

function closePfAiScanner() {
    if (_pfAiStream) { _pfAiStream.getTracks().forEach(t => t.stop()); _pfAiStream = null; }
    closeModal();
}

function pfAiCapture() {
    const video = document.getElementById('pfai-video');
    const canvas = document.getElementById('pfai-canvas');
    canvas.width = video.videoWidth;
    canvas.height = video.videoHeight;
    canvas.getContext('2d').drawImage(video, 0, 0);
    const dataUrl = canvas.toDataURL('image/jpeg', 0.85);
    document.getElementById('pfai-preview-img').src = dataUrl;

    if (_pfAiStream) { _pfAiStream.getTracks().forEach(t => t.stop()); _pfAiStream = null; }
    video.srcObject = null;

    document.getElementById('pfai-cam-container').style.display = 'none';
    document.getElementById('pfai-preview-container').style.display = 'block';
    document.getElementById('pfai-capture-btn').style.display = 'none';
    document.getElementById('pfai-retake-btn').style.display = 'inline-flex';
    document.getElementById('pfai-hint').style.display = 'none';

    _pfAiAnalyze(canvas.toDataURL('image/jpeg', 0.7).split(',')[1]);
}

function pfAiRetake() {
    document.getElementById('pfai-cam-container').style.display = 'block';
    document.getElementById('pfai-preview-container').style.display = 'none';
    document.getElementById('pfai-capture-btn').style.display = 'inline-flex';
    document.getElementById('pfai-retake-btn').style.display = 'none';
    document.getElementById('pfai-status').style.display = 'none';
    document.getElementById('pfai-result').style.display = 'none';
    document.getElementById('pfai-hint').style.display = 'block';

    navigator.mediaDevices.getUserMedia(getCameraConstraints()).then(stream => {
        _pfAiStream = stream;
        const video = document.getElementById('pfai-video');
        video.srcObject = stream;
        video.play();
    });
}

async function _pfAiAnalyze(base64) {
    const statusEl = document.getElementById('pfai-status');
    const resultEl = document.getElementById('pfai-result');
    statusEl.style.display = 'block';
    resultEl.style.display = 'none';

    try {
        const result = await api('gemini_identify', {}, 'POST', { image: base64 });

        statusEl.style.display = 'none';
        resultEl.style.display = 'block';

        if (!result.success) {
            if (/resource.?exhaust|quota|rate.?limit/i.test(result.error || '')) {
                resultEl.innerHTML = `<p style="color:var(--warning);text-align:center">⏳ ${t('error.ai_quota')}</p>
                    <button class="btn btn-secondary full-width" onclick="pfAiRetake()">${t('btn.retry')}</button>`;
            } else {
                resultEl.innerHTML = `<p style="color:var(--danger);text-align:center">❌ ${escapeHtml(result.error || t('error.identification'))}</p>
                    <button class="btn btn-secondary full-width" onclick="pfAiRetake()">${t('btn.retry')}</button>`;
            }
            return;
        }

        const id = result.identified;
        const matches = result.off_matches || [];

        let html = `<div class="ai-identified-card" style="margin-bottom:10px">
            <strong>${escapeHtml(id.name)}</strong>`;
        if (id.brand) html += ` <span style="color:var(--text-muted)">— ${escapeHtml(id.brand)}</span>`;
        if (id.description) html += `<p style="font-size:0.82rem;color:var(--text-light);margin:4px 0 0">${escapeHtml(id.description)}</p>`;
        html += `</div>`;

        if (matches.length > 0) {
            html += `<p style="font-size:0.82rem;color:var(--text-muted);margin-bottom:6px">Seleziona la variante esatta o usa i dati AI:</p>`;
            html += `<div class="ai-matches-list" style="max-height:160px;overflow-y:auto;margin-bottom:10px">`;
            matches.forEach((m, idx) => {
                html += `<div class="ai-match-item" onclick="_pfAiFillFromMatch(${idx})">`;
                if (m.image_url) html += `<img src="${escapeHtml(m.image_url)}" alt="" class="ai-match-img" onerror="this.style.display='none'">`;
                html += `<div class="ai-match-info"><strong>${escapeHtml(m.name)}</strong>`;
                if (m.brand) html += `<br><small>${escapeHtml(m.brand)}</small>`;
                if (m.quantity_info) html += `<br><small style="color:var(--text-muted)">${escapeHtml(m.quantity_info)}</small>`;
                html += `</div><span class="ai-match-barcode">${escapeHtml(m.barcode)}</span></div>`;
            });
            html += `</div>`;
        }

        html += `<button class="btn btn-primary full-width" onclick="_pfAiFillFromAI()">${matches.length > 0 ? t('ai.use_data_no_barcode') : t('ai.use_data')}</button>`;
        resultEl.innerHTML = html;

        window._pfAiIdentified = id;
        window._pfAiMatches = matches;

    } catch (err) {
        statusEl.style.display = 'none';
        resultEl.style.display = 'block';
        resultEl.innerHTML = `<p style="color:var(--danger);text-align:center">❌ ${t('error.connection')}</p>
            <button class="btn btn-secondary full-width" onclick="pfAiRetake()">${t('btn.retry')}</button>`;
    }
}

function _pfAiFillFields(name, brand, category, barcode, imageUrl, quantityInfo) {
    if (name) document.getElementById('pf-name').value = name;
    if (brand) document.getElementById('pf-brand').value = brand;
    if (category) {
        const cat = mapToLocalCategory(category, name || '');
        document.getElementById('pf-category').value = cat;
        document.getElementById('pf-category').dataset.manuallySet = 'true';
        onCategoryChange(true);
    }
    if (barcode) document.getElementById('pf-barcode').value = barcode;
    if (imageUrl) {
        document.getElementById('pf-image').value = imageUrl;
        const preview = document.getElementById('pf-image-preview');
        document.getElementById('pf-image-img').src = imageUrl;
        preview.style.display = 'block';
    }
    if (quantityInfo) {
        const detected = detectUnitAndQuantity(quantityInfo);
        document.getElementById('pf-unit').value = detected.unit;
        document.getElementById('pf-defqty').value = detected.quantity;
        document.getElementById('pf-defqty').dataset.manuallySet = 'true';
        onPfUnitChange();
    }
    // Trigger auto-detect for remaining empty fields
    if (name && !category) autoDetectCategory();
    closePfAiScanner();
    showToast('✅ Campi compilati dall\'AI', 'success');
}

function _pfAiFillFromAI() {
    const id = window._pfAiIdentified;
    if (!id) return;
    _pfAiFillFields(id.name, id.brand, id.category, '', '', '');
}

async function _pfAiFillFromMatch(idx) {
    const match = window._pfAiMatches[idx];
    if (!match) return;
    closePfAiScanner();
    showLoading(true);
    try {
        const lookupResult = await api('lookup_barcode', { barcode: match.barcode });
        if (lookupResult.found && lookupResult.product) {
            const p = lookupResult.product;
            _pfAiFillFields(p.name || match.name, p.brand || match.brand, p.category || '', match.barcode, p.image_url || match.image_url, p.quantity_info || '');
            showLoading(false);
            return;
        }
    } catch (e) {}
    showLoading(false);
    _pfAiFillFields(match.name, match.brand, match.category, match.barcode, match.image_url, '');
}

// ===== ALL PRODUCTS =====
async function loadAllProducts() {
    try {
        const data = await api('products_list');
        renderProductsList(data.products || []);
    } catch (err) {
        console.error(err);
    }
}

async function searchAllProducts() {
    const q = document.getElementById('products-search').value;
    if (q.length < 2) {
        loadAllProducts();
        return;
    }
    const data = await api('products_search', { q });
    renderProductsList(data.products || []);
}

function renderProductsList(products) {
    const container = document.getElementById('products-list');
    if (products.length === 0) {
        container.innerHTML = `<div class="empty-state"><div class="empty-state-icon">📦</div><p>${t('inventory.empty_db')}</p></div>`;
        return;
    }
    container.innerHTML = products.map(p => {
        const catIcon = CATEGORY_ICONS[mapToLocalCategory(p.category, p.name)] || '📦';
        return `
        <div class="product-item" onclick="selectProductForAction(${p.id})">
            <div class="inv-image">
                ${p.image_url ? `<img src="${escapeHtml(p.image_url)}" alt="" onerror="this.parentElement.innerHTML='${catIcon}'">` : catIcon}
            </div>
            <div class="inv-info">
                <div class="inv-name">${escapeHtml(p.name)}</div>
                ${p.brand ? `<div class="inv-brand">${escapeHtml(p.brand)}</div>` : ''}
                <div class="inv-meta">
                    ${p.barcode ? `<span class="inv-badge" style="background:#f3f4f6;color:#374151">📊 ${p.barcode}</span>` : ''}
                    <span class="inv-badge" style="background:#f3f4f6;color:#374151">${catIcon} ${p.category || 'Non categorizzato'}</span>
                </div>
            </div>
        </div>`;
    }).join('');
}

async function selectProductForAction(productId) {
    showLoading(true);
    try {
        const data = await api('product_get', { id: productId });
        if (data.product) {
            currentProduct = data.product;
            showLoading(false);
            // Clear search inputs after selecting a product
            const psInput = document.getElementById('products-search');
            if (psInput) psInput.value = '';
            const invInput = document.getElementById('inventory-search');
            if (invInput) invInput.value = '';
            showProductAction();
        } else {
            showLoading(false);
            showToast(t('error.not_found'), 'error');
        }
    } catch (err) {
        showLoading(false);
        showToast('Errore', 'error');
    }
}

// ===== SHOPPING LIST (BRING! INTEGRATION) =====
let shoppingListUUID = '';
let shoppingItems = [];
let suggestionItems = [];
let _spesaScanTarget = null; // { name, rawName, idx } when tapping item to scan

// ===== SHOPPING TABS =====
function switchShoppingTab(tab) {
    document.querySelectorAll('.shopping-tab').forEach(b => b.classList.remove('active'));
    document.querySelectorAll('.tab-panel-shopping').forEach(p => p.classList.remove('active'));
    document.getElementById(`tab-${tab}`)?.classList.add('active');
    document.getElementById(`tab-panel-${tab}`)?.classList.add('active');
}

function updateShoppingTabCounts() {
    const acquistoCount = shoppingItems.length;
    const previsioneCount = smartShoppingItems.filter(i => !i.on_bring).length;
    const acqEl = document.getElementById('tab-count-acquisto');
    const prevEl = document.getElementById('tab-count-previsione');
    if (acqEl) acqEl.textContent = acquistoCount;
    if (prevEl) prevEl.textContent = previsioneCount;
    document.getElementById('shopping-tabs')?.style.setProperty('display', 'flex');
}

// ===== LOCAL SHOPPING TAGS =====
function getShoppingTags(itemName) {
    try {
        const tags = JSON.parse(localStorage.getItem('shopping_tags') || '{}');
        return tags[itemName.toLowerCase()] || [];
    } catch { return []; }
}

function toggleShoppingTag(itemIdx, tag) {
    const item = shoppingItems[itemIdx];
    if (!item) return;
    const key = item.name.toLowerCase();
    try {
        const tags = JSON.parse(localStorage.getItem('shopping_tags') || '{}');
        const existing = tags[key] || [];
        const pos = existing.indexOf(tag);
        if (pos >= 0) existing.splice(pos, 1);
        else existing.push(tag);
        if (existing.length) tags[key] = existing;
        else delete tags[key];
        localStorage.setItem('shopping_tags', JSON.stringify(tags));

        // Sync urgente/presto tag to Bring specification so it's visible in the Bring app
        if (tag === 'urgente' && shoppingListUUID) {
            const isNowUrgent = existing.includes('urgente');
            const newSpec = isNowUrgent ? t('shopping.urgency_spec_critical') : '';
            api('bring_add', {}, 'POST', {
                items: [{ name: item.name, specification: newSpec, update_spec: true }],
                listUUID: shoppingListUUID,
            }).catch(() => {});
            // Update local item spec for immediate re-render
            item.specification = newSpec;
        }

        renderShoppingItems();
    } catch (e) { console.error('toggleShoppingTag', e); }
}

// ===== SCAN FROM SHOPPING LIST =====
function openScanForItem(idx) {
    const item = shoppingItems[idx];
    if (!item) return;
    _spesaScanTarget = { name: item.name, rawName: item.rawName || '', idx };
    showPage('scan');
    showToast(t('shopping.scan_toast').replace('{name}', item.name), 'info');
}

async function confirmShoppingItemFound() {
    if (!_spesaScanTarget) return;
    const { name, rawName } = _spesaScanTarget;
    _spesaScanTarget = null;
    document.getElementById('shopping-scan-target-banner').style.display = 'none';
    try {
        const r = await api('bring_remove', {}, 'POST', { name, rawName, listUUID: shoppingListUUID });
        if (r.success) {
            const idx = shoppingItems.findIndex(i => i.name.toLowerCase() === name.toLowerCase());
            if (idx >= 0) shoppingItems.splice(idx, 1);
            showToast(t('shopping.item_removed').replace('{name}', name), 'success');
            logOperation('bring_found', { name });
            loadShoppingCount();
        }
    } catch (e) { console.error('confirmShoppingItemFound', e); }
    showPage('shopping');
}

// ===== AUTO-ADD CRITICAL ITEMS TO BRING! =====

/** Build a Bring specification string that encodes urgency + optional brand. */
function _urgencyToSpec(urgency, brand) {
    const urgencyLabels = { critical: t('shopping.urgency_spec_critical'), high: t('shopping.urgency_spec_high'), medium: '', low: '' };
    const urgLabel = urgencyLabels[urgency] || '';
    if (urgLabel && brand) return `${urgLabel} · ${brand}`;
    if (urgLabel) return urgLabel;
    return brand || '';
}

// ===== BRING! PURCHASED BLOCKLIST =====
// When an item disappears from Bring (user bought it), we block auto-re-add for 4h.
const _BRING_PURCHASED_TTL = 4 * 60 * 60 * 1000; // 4 hours

function _getBringPurchasedBlocklist() {
    try {
        const raw = localStorage.getItem('_bringPurchasedBlocklist');
        const map = raw ? JSON.parse(raw) : {};
        const now = Date.now();
        // Prune expired entries
        let changed = false;
        for (const key of Object.keys(map)) {
            if (now - map[key] > _BRING_PURCHASED_TTL) { delete map[key]; changed = true; }
        }
        if (changed) localStorage.setItem('_bringPurchasedBlocklist', JSON.stringify(map));
        return map;
    } catch(e) { return {}; }
}

function _markBringPurchased(names) {
    const map = _getBringPurchasedBlocklist();
    const now = Date.now();
    for (const n of names) map[n.toLowerCase()] = now;
    localStorage.setItem('_bringPurchasedBlocklist', JSON.stringify(map));
}

function _isBringPurchased(name, urgency) {
    // Critical items: blocked only 30 min (enough to put groceries away).
    // High: 90 min. Others: full 4 h.
    const ttl = urgency === 'critical' ? 30 * 60 * 1000
              : urgency === 'high'     ? 90 * 60 * 1000
              : _BRING_PURCHASED_TTL;
    const map = _getBringPurchasedBlocklist();
    const now = Date.now();
    return Object.keys(map).some(k => {
        const matches = _nameTokens(name)[0] === _nameTokens(k)[0] || k === name.toLowerCase();
        if (!matches) return false;
        return (now - map[k]) < ttl;
    });
}

async function autoAddCriticalItems() {
    // Time-based guard: run at most once every 5 minutes
    const lastRun = parseInt(localStorage.getItem('_autoAddedCriticalTs') || '0');
    if (Date.now() - lastRun < 5 * 60 * 1000) return;
    localStorage.setItem('_autoAddedCriticalTs', String(Date.now()));
    // Auto-add rules:
    // - critical: always
    // - high: always (PHP already applies strict criteria for high urgency)
    // - medium: when running out within 7 days (<1 week) for items used ≥3x/month
    const toAdd = smartShoppingItems.filter(i => {
        const imminentWeek = (i.days_left ?? 999) <= 7 && (i.uses_per_month || 0) >= 3;
        if (i.on_bring) return false;
        // For imminent items, do not honor local "purchased" blocklist too aggressively.
        // If they are predicted to finish within a week, keep Bring aligned automatically.
        // Bypass blocklist for depleted items (current_qty=0) — they ran out and must be re-added
        if (!imminentWeek && (i.current_qty ?? 0) > 0 && _isBringPurchased(i.name, i.urgency)) return false;
        if (i.urgency === 'critical') return true;
        if (i.urgency === 'high') return true;
        if (i.urgency === 'medium' && (i.days_left ?? 999) <= 7 && (i.uses_per_month || 0) >= 3) return true;
        return false;
    });
    if (toAdd.length === 0) return;
    const itemsToAdd = toAdd.map(i => ({ name: i.name, specification: _urgencyToSpec(i.urgency, i.brand) }));
    try {
        const result = await api('bring_add', {}, 'POST', { items: itemsToAdd, listUUID: shoppingListUUID });
        if (result.success && result.added > 0) {
            showToast(t('shopping.add_urgent_toast', { n: result.added }), 'success');
            logOperation('bring_auto_add', { added: itemsToAdd.map(i => i.name) });
            loadShoppingList();
        }
    } catch (e) { /* ignore */ }
}

/**
 * Manually force a full Bring! sync: clears the purchased blocklist and all
 * auto-add/cleanup timers, then re-adds all urgent items from scratch.
 * Triggered by the user pressing "Forza sincronizzazione Bring!".
 */
async function forceSyncBring() {
    const btn = document.getElementById('btn-force-sync');
    if (btn) { btn.disabled = true; btn.textContent = `⏳ ${t('shopping.syncing')}`; }
    // Clear all guards so the next run is unconditional
    localStorage.removeItem('_bringPurchasedBlocklist');
    localStorage.removeItem('_autoAddedCriticalTs');
    localStorage.removeItem('_bringCleanupTs');
    localStorage.removeItem('_userPinnedBring');
    logOperation('force_sync_bring', {});
    // Reload everything from scratch
    await loadShoppingList();
    if (btn) { btn.disabled = false; btn.textContent = `🔄 ${t('shopping.force_sync')}`; }
    showToast(`🔄 ${t('shopping.sync_done')}`, 'success');
}

/**
 * One-time cleanup: remove items from Bring! that were auto-added but the algorithm no
 * longer considers relevant.  CONSERVATIVE: only removes items that match a known product
 * in our inventory with current_qty > 0 AND that no longer appear in smart predictions.
 * Items not matching any DB product are left untouched (likely manually added by user).
 */
async function cleanupObsoleteBringItems() {
    // Run at most once every 30 minutes
    const lastCleanup = parseInt(localStorage.getItem('_bringCleanupTs') || '0');
    if (Date.now() - lastCleanup < 30 * 60 * 1000) return;
    localStorage.setItem('_bringCleanupTs', String(Date.now()));
    if (!shoppingItems.length || !smartShoppingItems.length) return;

    // Load live inventory (has actual quantities unlike products_list)
    let invItems = [];
    try {
        const res = await api('inventory_list');
        invItems = res.inventory || [];
    } catch (e) { return; }

    // Build: every significant token of in-stock products → total qty
    // Any-token matching groups product families:
    // 'Passata di pomodoro' + 'Polpa di pomodoro' share 'pomodoro' → same need
    const stockByAnyToken = new Map();
    for (const inv of invItems) {
        const qty = parseFloat(inv.quantity || 0);
        if (qty <= 0) continue;
        for (const tok of _nameTokens(inv.name || '')) {
            stockByAnyToken.set(tok, (stockByAnyToken.get(tok) || 0) + qty);
        }
    }

    // Build: any matching token → smart item (any urgency — all predictions are protected)
    const smartByToken = new Map();
    for (const si of smartShoppingItems) {
        for (const tok of _nameTokens(si.name)) {
            if (!smartByToken.has(tok)) smartByToken.set(tok, si);
        }
    }

    // User-pinned: items manually added via the suggestions panel — never auto-remove
    let userPinned;
    try {
        const raw = localStorage.getItem('_userPinnedBring');
        const map = raw ? JSON.parse(raw) : {};
        // Prune entries older than 30 days
        const now = Date.now();
        let changed = false;
        for (const k of Object.keys(map)) {
            if (now - map[k] > 30 * 24 * 60 * 60 * 1000) { delete map[k]; changed = true; }
        }
        if (changed) localStorage.setItem('_userPinnedBring', JSON.stringify(map));
        userPinned = map;
    } catch(e) { userPinned = {}; }

    const toRemove = [];
    for (const item of shoppingItems) {
        // Check if any significant token of this Bring item has stock in inventory
        const itemTokens = _nameTokens(item.name);
        const stockQty = itemTokens.reduce((sum, tok) => sum + (stockByAnyToken.get(tok) || 0), 0);

        // No inventory stock for any related product → nothing to remove
        if (stockQty <= 0) continue;

        // Never remove items the user explicitly pinned from suggestions
        if (userPinned[item.name.toLowerCase()]) continue;

        // Check if smart shopping flags something with a matching token as needed (any urgency)
        const smartSi = itemTokens.map(tok => smartByToken.get(tok)).find(Boolean);
        if (smartSi) {
            // Smart still predicts this item will be needed and it has remaining stock → keep it
            if (smartSi.current_qty > 0) continue;
        }

        toRemove.push(item);
    }

    if (toRemove.length === 0) return;

    let removed = 0;
    const removedNames = [];
    for (const item of toRemove) {
        try {
            const r = await api('bring_remove', {}, 'POST', {
                name: item.name,
                rawName: item.rawName || '',
                listUUID: shoppingListUUID
            });
            if (r.success) { removed++; removedNames.push(item.name); }
        } catch (e) { /* ignore individual failures */ }
    }

    if (removed > 0) {
        showToast(t('shopping.removed_sufficient', { removed }), 'info');
        logOperation('bring_cleanup', { removed: removedNames });
        loadShoppingList();
    }
}

/**
 * Log an app operation (not a food transaction) for auditing/debugging.
 * Stored in localStorage under '_opLog', capped at 200 entries.
 */
function logOperation(action, details) {
    try {
        const log = JSON.parse(localStorage.getItem('_opLog') || '[]');
        log.push({ ts: new Date().toISOString(), action, details });
        // Keep last 200 entries
        if (log.length > 200) log.splice(0, log.length - 200);
        localStorage.setItem('_opLog', JSON.stringify(log));
    } catch (e) { /* ignore */ }
}

// Build a better search query from item name + specification
function buildSearchQuery(item) {
    // Only use the item name for search - specification confuses the search engine
    // The AI on the backend will use the specification to pick the right product
    return item.name;
}

// Parse weight/quantity from specification (e.g. "200g" -> 0.2 kg, "500 ml" -> 0.5, "2 pz" -> 2 units)
function parseQtyFromSpec(spec) {
    if (!spec) return null;
    const s = spec.toLowerCase().trim();
    // Match weight/volume: 200g, 0.5kg, 500 g, 1,5 kg, 200 gr
    const m = s.match(/(\d+[.,]?\d*)\s*(g|gr|kg|ml|cl|l|lt)/i);
    if (m) {
        let val = parseFloat(m[1].replace(',', '.'));
        const unit = m[2].toLowerCase();
        if (unit === 'g' || unit === 'gr') return { kg: val / 1000, label: val + 'g', type: 'weight' };
        if (unit === 'kg') return { kg: val, label: (val * 1000) + 'g', type: 'weight' };
        if (unit === 'ml') return { kg: val / 1000, label: val + 'ml', type: 'weight' };
        if (unit === 'cl') return { kg: val / 100, label: val * 10 + 'ml', type: 'weight' };
        if (unit === 'l' || unit === 'lt') return { kg: val, label: (val * 1000) + 'ml', type: 'weight' };
    }
    // Match unit count: 2 pz, 3 pezzi, 5, 2x, ~5 pz
    const pzMatch = s.match(/~?(\d+)\s*(pz|pezzi|x|$)/i);
    if (pzMatch) {
        const count = parseInt(pzMatch[1]);
        if (count > 0 && count <= 50) return { count, label: count + ' pz', type: 'units' };
    }
    return null;
}

// Estimate price when product is sold per-kg/per-L or per-unit and user wants a certain quantity
function estimateItemPrice(product, spec) {
    if (!product.priceUm) return null;
    const umStr = String(product.priceUm);
    const pm = umStr.match(/(\d+[.,]?\d*)/);
    if (!pm) return null;
    const pricePerUnit = parseFloat(pm[1].replace(',', '.'));
    if (!pricePerUnit || pricePerUnit <= 0) return null;
    
    const qty = parseQtyFromSpec(spec);
    if (!qty) return null;
    
    if (qty.type === 'weight') {
        const estimated = pricePerUnit * qty.kg;
        if (estimated <= 0 || estimated > 500) return null;
        return { estimated: Math.round(estimated * 100) / 100, qtyLabel: qty.label };
    } else if (qty.type === 'units') {
        // For unit items: estimate per-item cost from the product price
        // If product is per-kg and we want N pieces, estimate ~200-300g per piece
        const avgWeightPerPiece = 0.25; // ~250g per piece (fruit/veg average)
        const estimated = pricePerUnit * avgWeightPerPiece * qty.count;
        if (estimated <= 0 || estimated > 500) return null;
        return { estimated: Math.round(estimated * 100) / 100, qtyLabel: qty.label };
    }
    return null;
}

// ===== SMART SHOPPING =====
let smartShoppingItems = [];
let smartShoppingFilter = 'all';
let _smartShoppingLastFetch = 0;      // timestamp of last successful fetch
let _bgShoppingInterval = null; // kept for compatibility, cron handles refresh server-side

/** Update dashboard badge from already-cached data */
function _updateSmartUrgencyBadge() {
    const urgentEl = document.getElementById('stat-urgent');
    if (!urgentEl) return;
    const urgent = smartShoppingItems.filter(i => i.urgency === 'critical' || i.urgency === 'high').length;
    if (urgent > 0) {
        urgentEl.textContent = `⚠ ${urgent}`;
        urgentEl.style.display = '';
    } else {
        urgentEl.style.display = 'none';
    }
}

/**
 * Sync the on_bring flag for every smartShoppingItem against the current shoppingItems list.
 * The server cache can be up to 10 min old so on_bring may be stale — this corrects it
 * client-side using strict first-token matching: a Bring item matches a smart item only when
 * the first significant token of the Bring item's name equals the first significant token of
 * the smart item's name (or exact name match). This avoids false positives like
 * "Frutta" (fresh fruit on Bring) matching "Muesli Frutta Secca" (a different product).
 */
function _syncOnBringFlags() {
    for (const si of smartShoppingItems) {
        const siLower = si.name.toLowerCase();
        const siFirst = _nameTokens(si.name)[0];
        const siShoppingLower = (si.shopping_name || '').toLowerCase();
        const siShoppingFirst = si.shopping_name ? _nameTokens(si.shopping_name)[0] : null;
        si.on_bring = !!(
            shoppingItems.find(bi => bi.name.toLowerCase() === siLower) ||
            (siShoppingLower && shoppingItems.find(bi => bi.name.toLowerCase() === siShoppingLower)) ||
            (siFirst && shoppingItems.find(bi => _nameTokens(bi.name)[0] === siFirst)) ||
            (siShoppingFirst && shoppingItems.find(bi => _nameTokens(bi.name)[0] === siShoppingFirst))
        );
    }
}

function _renderSmartLastUpdate() {
    const el = document.getElementById('smart-last-update');
    if (!el || !_smartShoppingLastFetch) return;
    const d = new Date(_smartShoppingLastFetch);
    el.textContent = `Aggiornato ${d.getHours().toString().padStart(2,'0')}:${d.getMinutes().toString().padStart(2,'0')}`;
}

function startBgShoppingRefresh() {
    // No-op: server-side cron handles refresh every 5 minutes.
    // The JS fetches pre-computed cache on demand (instant response).
}

async function loadSmartShopping() {
    try {
        const data = await api('smart_shopping');
        if (data.success && data.items && data.items.length > 0) {
            const prevCriticalNames = new Set(
                smartShoppingItems.filter(i => i.urgency === 'critical').map(i => i.name)
            );
            smartShoppingItems = data.items;
            _smartShoppingLastFetch = Date.now();
            // If the set of critical items changed, reset autoAdd/cleanup timers so
            // they run with fresh data on next shopping page load
            const newCriticalNames = new Set(data.items.filter(i => i.urgency === 'critical').map(i => i.name));
            const criticalChanged = [...prevCriticalNames].some(n => !newCriticalNames.has(n)) ||
                                    [...newCriticalNames].some(n => !prevCriticalNames.has(n));
            if (criticalChanged) {
                localStorage.removeItem('_autoAddedCriticalTs');
                localStorage.removeItem('_bringCleanupTs');
            }
            renderSmartShopping();
            _renderSmartLastUpdate();
            _updateSmartUrgencyBadge();
            document.getElementById('smart-shopping-empty').style.display = 'none';
            document.getElementById('smart-shopping-content').style.display = 'block';
        } else {
            smartShoppingItems = [];
            _smartShoppingLastFetch = Date.now();
            document.getElementById('smart-shopping-empty').style.display = 'block';
            document.getElementById('smart-shopping-content').style.display = 'none';
        }
    } catch (e) {
        console.error('Smart shopping error:', e);
        smartShoppingItems = [];
    }
    updateShoppingTabCounts();
}

function filterSmart(filter) {
    smartShoppingFilter = filter;
    document.querySelectorAll('.smart-filter').forEach(b => b.classList.remove('active'));
    document.querySelector(`.smart-filter[data-filter="${filter}"]`)?.classList.add('active');
    renderSmartShopping();
}

function renderSmartShopping() {
    const container = document.getElementById('smart-items');
    const countEl = document.getElementById('smart-count');
    const actionsEl = document.getElementById('smart-actions');

    let items = smartShoppingItems;
    if (smartShoppingFilter !== 'all') {
        items = items.filter(i => i.urgency === smartShoppingFilter);
    }

    countEl.textContent = items.length;

    if (items.length === 0) {
        container.innerHTML = `<div class="empty-state" style="padding:16px"><p>${t('shopping.empty_category')}</p></div>`;
        actionsEl.style.display = 'none';
        return;
    }

    const urgencyConfig = {
        critical: { color: '#ef4444', bg: 'rgba(239,68,68,0.08)', icon: '🔴', label: t('shopping.urgency_critical') },
        high:     { color: '#f97316', bg: 'rgba(249,115,22,0.08)', icon: '🟠', label: t('shopping.urgency_high') },
        medium:   { color: '#eab308', bg: 'rgba(234,179,8,0.08)',  icon: '🟡', label: t('shopping.urgency_medium') },
        low:      { color: '#22c55e', bg: 'rgba(34,197,94,0.08)',  icon: '🟢', label: t('shopping.urgency_low') },
    };

    // Group by section
    const smartSectionMap = new Map();
    items.forEach(item => {
        const sec = getItemSection(item.name);
        if (!smartSectionMap.has(sec.key)) smartSectionMap.set(sec.key, { sec, items: [] });
        smartSectionMap.get(sec.key).items.push(item);
    });

    let smartHtml = '';
    for (const secDef of SHOPPING_SECTIONS) {
        const group = smartSectionMap.get(secDef.key);
        if (!group) continue;
        smartHtml += `<div class="shopping-section-divider"><span class="sec-icon">${secDef.icon}</span>${secDef.label}</div>`;
        for (const item of group.items) {
            smartHtml += renderSmartItem(item, items);
        }
    }
    container.innerHTML = smartHtml;

    // Show/hide add button based on checkable items
    const hasCheckable = items.some(i => !i.on_bring);
    actionsEl.style.display = hasCheckable ? 'block' : 'none';
}

function renderSmartItem(item) {
    const urgencyConfig = {
        critical: { color: '#ef4444', bg: 'rgba(239,68,68,0.08)', icon: '🔴', label: t('shopping.urgency_critical') },
        high:     { color: '#f97316', bg: 'rgba(249,115,22,0.08)', icon: '🟠', label: t('shopping.urgency_high') },
        medium:   { color: '#eab308', bg: 'rgba(234,179,8,0.08)',  icon: '🟡', label: t('shopping.urgency_medium') },
        low:      { color: '#22c55e', bg: 'rgba(34,197,94,0.08)',  icon: '🟢', label: t('shopping.urgency_low') },
    };
    const u = urgencyConfig[item.urgency] || urgencyConfig.low;
    const catIcon = CATEGORY_ICONS[mapToLocalCategory(item.category, item.name)] || '📦';
    const globalIdx = smartShoppingItems.indexOf(item);

    // Generic vs specific name logic
    const shoppingName = item.shopping_name || item.name;
    const isGeneric = shoppingName !== item.name;
    const variants = item.variants || [];

    // Build title line: generic name (and brand only if not grouped)
    let nameLine = `<div class="smart-item-name">${escapeHtml(shoppingName)}`;
    if (!isGeneric && item.brand) nameLine += ` <small class="smart-brand">${escapeHtml(item.brand)}</small>`;
    nameLine += `</div>`;

    // Build subtitle: specific product + brand when grouped, plus any variants
    let specificLine = '';
    if (isGeneric || variants.length > 0) {
        let specifics = [];
        specifics.push(item.name + (item.brand ? ` (${item.brand})` : ''));
        for (const v of variants) {
            specifics.push(v.name + (v.brand ? ` (${v.brand})` : ''));
        }
        specificLine = `<div class="smart-item-specific">${escapeHtml(specifics.join(' · '))}</div>`;
    }

        // Stock bar
        const pct = Math.min(100, Math.max(0, item.pct_left));
        const barColor = pct <= 15 ? '#ef4444' : pct <= 30 ? '#f97316' : pct <= 50 ? '#eab308' : '#22c55e';

        // Quantity display
        let qtyText = '';
        if (item.current_qty > 0) {
            qtyText = `${item.current_qty} ${item.unit}`;
            if (item.pct_left < 100) qtyText += ` (${pct}%)`;
        } else {
            qtyText = t('shopping.out_of_stock');
        }

        // Usage frequency badge
        let freqBadge = '';
        if (item.use_count >= 8) freqBadge = `<span class="smart-freq-badge freq-high">${t('shopping.freq_high')}</span>`;
        else if (item.use_count >= 4) freqBadge = `<span class="smart-freq-badge freq-med">${t('shopping.freq_regular')}</span>`;
        else if (item.use_count >= 2) freqBadge = `<span class="smart-freq-badge freq-low">${t('shopping.freq_occasional')}</span>`;

        // Days left prediction
        let predBadge = '';
        if (item.days_left <= 3 && item.days_left > 0 && item.current_qty > 0) {
            predBadge = `<span class="smart-pred-badge pred-urgent">${t('expiry.badge_days_left').replace('{n}', item.days_left)}</span>`;
        } else if (item.days_left <= 7 && item.days_left > 0 && item.current_qty > 0) {
            predBadge = `<span class="smart-pred-badge pred-soon">${t('expiry.badge_days_left').replace('{n}', item.days_left)}</span>`;
        }

        // Expiry badge
        let expiryBadge = '';
        if (item.days_to_expiry < 0 && item.current_qty > 0) {
            expiryBadge = `<span class="smart-pred-badge pred-urgent">${t('expiry.badge_expired_bare')}</span>`;
        } else if (item.days_to_expiry <= 3 && item.days_to_expiry >= 0 && item.current_qty > 0) {
            expiryBadge = `<span class="smart-pred-badge pred-urgent">${t('expiry.badge_expires_warn').replace('{n}', item.days_to_expiry)}</span>`;
        }

        return `
        <div class="smart-item" style="border-left: 3px solid ${u.color}; background: ${u.bg}">
            <div class="smart-item-top">
                ${!item.on_bring ? `<input type="checkbox" class="smart-check" data-idx="${globalIdx}">` : ''}
                <span class="smart-item-icon">${catIcon}</span>
                <div class="smart-item-info">
                    ${nameLine}
                    ${specificLine}
                    <div class="smart-item-reasons">${item.reasons.map(r => `<span>${escapeHtml(r)}</span>`).join(' · ')}</div>
                    <div class="smart-item-badges">
                        <span class="smart-urgency-badge" style="color:${u.color}">${u.icon} ${u.label}</span>
                        ${freqBadge}${predBadge}${expiryBadge}
                        ${item.is_opened ? `<span class="smart-freq-badge freq-low">${t('inventory.opened_badge')}</span>` : ''}
                        ${item.on_bring ? `<span class="smart-bring-badge">${t('shopping.bring_badge')}</span>` : ''}
                    </div>
                </div>
                <div class="smart-item-stock">
                    <span class="smart-qty">${qtyText}</span>
                    ${item.current_qty > 0 ? `<div class="smart-stock-bar"><div class="smart-stock-fill" style="width:${pct}%;background:${barColor}"></div></div>` : ''}
                </div>
            </div>
        </div>`;
}

async function migrateBringNames(btn) {
    const statusEl = document.getElementById('bring-migrate-status');
    if (btn) btn.disabled = true;
    if (statusEl) { statusEl.style.display = 'inline'; statusEl.textContent = '⏳ In corso…'; }
    try {
        const data = await api('bring_migrate_names', {}, 'POST', {});
        if (data.success) {
            const msg = t('shopping.migration_done', { migrated: data.migrated, skipped: data.skipped }) + (data.errors ? `, ${data.errors} errori` : '');
            if (statusEl) statusEl.textContent = msg;
            if (data.migrated > 0) {
                showToast(`🔄 ${data.migrated} nomi generalizzati in Bring!`, 'success');
                loadShoppingList(); // refresh the shopping list view
            } else {
                showToast('Tutti i nomi sono già aggiornati', 'info');
            }
        } else {
            if (statusEl) statusEl.textContent = '❌ ' + (data.error || 'Errore');
        }
    } catch(e) {
        if (statusEl) statusEl.textContent = '❌ Errore di connessione';
    }
    if (btn) btn.disabled = false;
}

async function addSmartToBring() {
    const checks = document.querySelectorAll('.smart-check:checked');
    if (checks.length === 0) {
        showToast(t('error.select_items'), 'info');
        return;
    }

    const itemsToAdd = [];
    checks.forEach(cb => {
        const idx = parseInt(cb.dataset.idx);
        const item = smartShoppingItems[idx];
        if (item) {
            const shoppingName = item.shopping_name || item.name;
            const isGeneric = shoppingName !== item.name;
            // When generic, use specific product name + brand as the specification
            const spec = isGeneric
                ? (item.name + (item.brand ? ` · ${item.brand}` : ''))
                : _urgencyToSpec(item.urgency, item.brand);
            itemsToAdd.push({
                name: shoppingName,
                specification: spec,
            });
        }
    });

    showLoading(true);
    try {
        const result = await api('bring_add', {}, 'POST', {
            items: itemsToAdd,
            listUUID: shoppingListUUID,
        });
        showLoading(false);
        if (result.success) {
            const msg = result.added > 0
                ? t('shopping.added_to_bring', { n: result.added }) + (result.skipped > 0 ? ` (${t('shopping.added_to_bring_skip', { n: result.skipped })})` : '')
                : t('shopping.all_on_bring');
            showToast(msg, result.added > 0 ? 'success' : 'info');
            // Mark all manually-added items as user-pinned so cleanupObsoleteBringItems never removes them
            if (result.added > 0) {
                const pinned = JSON.parse(localStorage.getItem('_userPinnedBring') || '{}');
                const now = Date.now();
                for (const it of itemsToAdd) pinned[it.name.toLowerCase()] = now;
                localStorage.setItem('_userPinnedBring', JSON.stringify(pinned));
            }
            // Reload to refresh badges
            loadShoppingList();
        } else {
            showToast(result.error || 'Errore', 'error');
        }
    } catch (e) {
        showLoading(false);
        showToast(t('error.connection'), 'error');
    }
}

// Load just the shopping count for dashboard stat card
async function loadShoppingCount() {
    try {
        const data = await api('bring_list');
        const el = document.getElementById('stat-spesa');
        if (data.success && data.purchase) {
            el.textContent = data.purchase.length;
        } else {
            el.textContent = '-';
        }
    } catch {
        const el = document.getElementById('stat-spesa');
        el.textContent = '-';
    }
    // Smart urgency badge: use cached data if fresh (< 2 min), else fetch
    if (smartShoppingItems.length > 0 && (Date.now() - _smartShoppingLastFetch) < 2 * 60 * 1000) {
        _updateSmartUrgencyBadge();
    } else {
        try {
            const smart = await api('smart_shopping');
            if (smart.success && smart.items) {
                smartShoppingItems = smart.items;
                _smartShoppingLastFetch = Date.now();
                _updateSmartUrgencyBadge();
            }
        } catch { /* ignore */ }
    }
}

/**
 * Sync local 'urgente' tag from Bring specification.
 * If a Bring item's specification contains 'urgente', ensure the local tag is set.
 * If a Bring item's specification is empty/cleared, remove the local urgente tag
 * UNLESS smart shopping considers it critical (to avoid losing urgency on stale specs).
 */
function _syncTagsFromBringSpec() {
    try {
        const tags = JSON.parse(localStorage.getItem('shopping_tags') || '{}');
        let changed = false;
        for (const item of shoppingItems) {
            const key = item.name.toLowerCase();
            const spec = (item.specification || '').toLowerCase();
            const existing = tags[key] || [];
            const hasUrgente = existing.includes('urgente');
            const smartMatch = _matchBringToSmart(item.name, smartShoppingItems);
            const smartIsCritical = smartMatch && (smartMatch.urgency === 'critical' || smartMatch.urgency === 'high');
            if ((spec.includes('urgente') || spec.includes('presto') || smartIsCritical) && !hasUrgente) {
                existing.push('urgente');
                tags[key] = existing;
                changed = true;
            } else if (!spec.includes('urgente') && !spec.includes('presto') && !smartIsCritical && hasUrgente) {
                existing.splice(existing.indexOf('urgente'), 1);
                if (existing.length) tags[key] = existing;
                else delete tags[key];
                changed = true;
            }
        }
        if (changed) localStorage.setItem('shopping_tags', JSON.stringify(tags));
    } catch (e) { /* ignore */ }
}

/**
 * After smart shopping loads, push urgency specifications to Bring for all matched items.
 * This makes urgency visible in the native Bring app via the item specification field.
 * Only updates if the spec has changed (to avoid unnecessary API calls).
 */
async function autoSyncUrgencySpecs() {
    if (!shoppingListUUID || !smartShoppingItems.length) return;
    const toUpdate = [];
    for (const item of shoppingItems) {
        const smartMatch = _matchBringToSmart(item.name, smartShoppingItems);
        if (!smartMatch) continue;
        const expectedSpec = _urgencyToSpec(smartMatch.urgency, '');
        const currentSpec = (item.specification || '').toLowerCase();
        // Only update if urgency marker changed (don't clobber user-set spec info that isn't urgency)
        const currentHasUrgencyMarker = currentSpec.includes('urgente') || currentSpec.includes('presto');
        const needsUpdate = expectedSpec && !currentHasUrgencyMarker;
        const needsClear = !expectedSpec && currentHasUrgencyMarker;
        // Also update if urgency level changed (e.g. medium→high or high→critical)
        const currentIsHigh = currentSpec.includes('urgente');
        const newIsHigh = (expectedSpec || '').toLowerCase().includes('urgente');
        const urgencyEscalated = expectedSpec && currentHasUrgencyMarker && (currentIsHigh !== newIsHigh);
        if (needsUpdate || needsClear || urgencyEscalated) {
            toUpdate.push({ name: item.name, specification: expectedSpec, update_spec: true });
            // Optimistically update local item so re-render is immediate
            item.specification = expectedSpec;
        }
    }
    if (toUpdate.length === 0) return;
    try {
        await api('bring_add', {}, 'POST', { items: toUpdate, listUUID: shoppingListUUID });
    } catch (e) { /* ignore - sync is best-effort */ }
}

async function loadShoppingList() {
    const statusEl = document.getElementById('bring-status');
    const currentEl = document.getElementById('shopping-current');
    const suggestionsEl = document.getElementById('shopping-suggestions');
    
    statusEl.style.display = 'block';
    statusEl.innerHTML = `<div class="bring-loading"><div class="loading-spinner"></div> ${t('shopping.bring_loading')}</div>`;
    currentEl.style.display = 'none';
    suggestionsEl.style.display = 'none';

    // ── Demo mode: show placeholder list, skip all Bring! API calls ──────────
    if (_demoMode) {
        statusEl.style.display = 'none';
        shoppingListUUID = 'demo-list';
        shoppingItems = [
            { name: 'Latte', specification: '🟠 presto · 1L', rawName: 'Latte' },
            { name: 'Pane', specification: '', rawName: 'Pane' },
            { name: 'Uova', specification: '⚡ urgente', rawName: 'Uova' },
            { name: 'Pasta', specification: '500g', rawName: 'Pasta' },
            { name: 'Pomodori', specification: '1kg', rawName: 'Pomodori' },
        ];
        renderShoppingItems();
        currentEl.style.display = 'block';
        loadSmartShopping().then(() => {
            _syncOnBringFlags();
            renderSmartShopping();
            updateShoppingTabCounts();
            renderShoppingItems();
        });
        return;
    }
    
    try {
        const data = await api('bring_list');
        statusEl.style.display = 'none';
        
        if (!data.success) {
            statusEl.style.display = 'block';
            const isMissingCreds = data.error && data.error.toLowerCase().includes('credenziali bring');
            if (isMissingCreds) {
                statusEl.innerHTML = `<div class="bring-error">🔑 ${t('shopping.bring_not_configured') || 'Bring! non è configurato. Aggiungi email e password nelle <a href="#" onclick="showPage(\'settings\');return false;">impostazioni</a>.'}</div>`;
            } else {
                statusEl.innerHTML = `<div class="bring-error">⚠️ ${escapeHtml(data.error || t('error.bring_connection'))}</div>`;
            }
            return;
        }
        
        shoppingListUUID = data.listUUID;
        // Detect items removed from Bring since last load (= just purchased by user)
        const prevNames = new Set((shoppingItems || []).map(i => i.name.toLowerCase()));
        const newItems  = data.purchase || [];
        const newNames  = new Set(newItems.map(i => i.name.toLowerCase()));
        if (prevNames.size > 0) {
            const removedNames = [...prevNames].filter(n => !newNames.has(n));
            if (removedNames.length) _markBringPurchased(removedNames);
        }
        shoppingItems = newItems;
        
        // Sync urgente local tags from Bring specification (items marked urgent by us or manually)
        _syncTagsFromBringSpec();
        renderShoppingItems();
        currentEl.style.display = 'block';
        
        // Load smart shopping predictions, then re-render to show badges + auto-add critical
        loadSmartShopping().then(() => {
            _syncOnBringFlags();          // sync on_bring against current Bring list before any logic reads it
            _syncTagsFromBringSpec();     // re-sync tags now that smart data is available
            autoSyncUrgencySpecs();       // push urgency specs to Bring for matched items
            renderSmartShopping();        // re-render smart tab with corrected on_bring flags
            updateShoppingTabCounts();    // update tab badges with corrected counts
            autoAddCriticalItems();
            cleanupObsoleteBringItems();
            renderShoppingItems();        // re-render shopping tab with urgency badges
        });

    } catch (err) {
        console.error('Bring! error:', err);
        statusEl.style.display = 'block';
        statusEl.innerHTML = `<div class="bring-error">${t('error.bring_connection')}</div>`;
    }
}

/** Return the spec text to show in the UI, stripping urgency markers (those are shown as badges). */
function _specDisplayText(spec) {
    if (!spec) return '';
    // Strip known urgency prefixes set by _urgencyToSpec (case-insensitive, then trim separator)
    const lower = spec.toLowerCase();
    for (const prefix of ['⚡ urgente', '🟠 presto']) {
        if (lower.startsWith(prefix)) {
            return spec.slice(prefix.length).replace(/^\s*[·\-]\s*/, '').trim();
        }
    }
    return spec;
}

/** Return the spec for price search, stripping urgency markers that would confuse the AI. */
function _cleanSpecForSearch(spec) {
    return _specDisplayText(spec);
}

async function renderShoppingItems() {
    const container = document.getElementById('shopping-items');
    const countEl = document.getElementById('shopping-count');

    countEl.textContent = shoppingItems.length;
    // Update tab count too
    const tabCount = document.getElementById('tab-count-acquisto');
    if (tabCount) tabCount.textContent = shoppingItems.length;
    
    if (shoppingItems.length === 0) {
        container.innerHTML = `<div class="empty-state" style="padding:20px"><div class="empty-state-icon">✅</div><p>${t('shopping.empty')}</p></div>`;
        return;
    }
    
    const s = getSettings();

    // Build section groups, sorted by urgency weight within each section
    const TAG_LABELS = { urgente: t('shopping.tag_urgent'), prio: t('shopping.tag_priority'), check: t('shopping.tag_check') };
    const urgencyMap = {
        critical: { icon: '🔴', label: t('shopping.urgency_critical'), cls: 'badge-critical' },
        high:     { icon: '🟠', label: t('shopping.urgency_high'),     cls: 'badge-high' },
        medium:   { icon: '🟡', label: t('shopping.urgency_medium_short'), cls: 'badge-medium' },
        low:      { icon: '🟢', label: t('shopping.urgency_low_short'), cls: 'badge-low' },
    };

    // Map each item to its section + urgency (strict first-token matching to avoid false positives)
    // Also derive urgency from Bring specification if smart matching fails
    const enriched = shoppingItems.map((item, idx) => {
        const smartData = _matchBringToSmart(item.name, smartShoppingItems);
        let urgency = smartData?.urgency || null;
        // Fallback: read urgency from Bring specification (set by our app when adding)
        if (!urgency && item.specification) {
            const spec = item.specification.toLowerCase();
            if (spec.includes('urgente')) urgency = 'critical';
            else if (spec.includes('presto')) urgency = 'high';
        }
        const sec = getItemSection(item.name);
        return { item, idx, smartData, urgency, sec };
    });

    // Group by section key, preserving SHOPPING_SECTIONS order
    const sectionMap = new Map();
    for (const e of enriched) {
        const key = e.sec.key;
        if (!sectionMap.has(key)) sectionMap.set(key, { sec: e.sec, items: [] });
        sectionMap.get(key).items.push(e);
    }

    // Sort items within each section: by urgency weight desc, then by use_count desc
    for (const [, group] of sectionMap) {
        group.items.sort((a, b) => {
            const wa = URGENCY_WEIGHT[a.urgency] || 0;
            const wb = URGENCY_WEIGHT[b.urgency] || 0;
            if (wb !== wa) return wb - wa;
            return (b.smartData?.use_count || 0) - (a.smartData?.use_count || 0);
        });
    }

    // Render sections in canonical order
    let html = '';
    for (const secDef of SHOPPING_SECTIONS) {
        const group = sectionMap.get(secDef.key);
        if (!group) continue;

        html += `<div class="shopping-section-divider"><span class="sec-icon">${secDef.icon}</span>${secDef.label}</div>`;

        for (const { item, idx, smartData, urgency } of group.items) {
            const catIcon = CATEGORY_ICONS[guessCategoryFromName(item.name)] || '🛒';
            const bgStyle = urgency && URGENCY_BG[urgency] ? ` style="background:${URGENCY_BG[urgency]}"` : '';
            const localTags = getShoppingTags(item.name);

            // Urgency badge
            let urgencyBadge = '';
            if (urgency && urgencyMap[urgency]) {
                const u = urgencyMap[urgency];
                urgencyBadge = `<span class="sinv-badge ${u.cls}">${u.icon} ${u.label}</span>`;
            }

            // Frequency badge
            let freqBadge = '';
            if (smartData && smartData.use_count >= 8) freqBadge = `<span class="sinv-badge badge-freq-high">📈 ${smartData.use_count}x</span>`;
            else if (smartData && smartData.use_count >= 4) freqBadge = `<span class="sinv-badge badge-freq-med">📊 ${smartData.use_count}x</span>`;
            else if (smartData && smartData.use_count >= 2) freqBadge = `<span class="sinv-badge badge-freq-low">📉 ${smartData.use_count}x</span>`;

            const localTagHtml = localTags.map(t =>
                `<span class="sinv-badge badge-local-tag" onclick="event.stopPropagation(); toggleShoppingTag(${idx}, '${t}')">${TAG_LABELS[t] || t} ✕</span>`
            ).join('');

            const tagMenu = `<div class="shopping-tag-menu" onclick="event.stopPropagation()">
                ${Object.entries(TAG_LABELS).map(([k, v]) =>
                    `<button class="sinv-badge badge-tag-add ${localTags.includes(k) ? 'active' : ''}" onclick="toggleShoppingTag(${idx}, '${k}')">${v}</button>`
                ).join('')}
            </div>`;

            html += `
            <div class="shopping-item" id="shop-item-${idx}" onclick="openScanForItem(${idx})" title="${t('shopping.tap_to_scan')}"${bgStyle}>
                <span class="shopping-item-icon">${catIcon}</span>
                <div class="shopping-item-body">
                    <div class="shopping-item-top">
                        <div class="shopping-item-info">
                            <div class="shopping-item-name-row">
                                <span class="shopping-item-name">${escapeHtml(item.name)}</span>
                                <span class="shopping-item-scan-hint">📷</span>
                            </div>
                            ${_specDisplayText(item.specification) ? `<div class="shopping-item-spec">${escapeHtml(_specDisplayText(item.specification))}</div>` : ''}
                            ${(urgencyBadge || freqBadge || localTagHtml) ? `<div class="shopping-item-badges">${urgencyBadge}${freqBadge}${localTagHtml}</div>` : ''}
                        </div>
                        <div class="shopping-item-right" onclick="event.stopPropagation()">
                            <button class="shopping-item-tag-btn" onclick="toggleShoppingTagMenu(this)" title="${t('shopping.tag_title')}">🏷️</button>
                            <button class="shopping-item-remove" onclick="removeBringItem(${idx})" title="${t('shopping.remove_title')}">✕</button>
                        </div>
                    </div>
                    <div class="shopping-tag-menu-container" style="display:none">${tagMenu}</div>
                </div>
            </div>`;
        }
    }

    container.innerHTML = html;
}

function toggleShoppingTagMenu(btn) {
    const container = btn.closest('.shopping-item-body').querySelector('.shopping-tag-menu-container');
    if (!container) return;
    const isOpen = container.style.display !== 'none';
    // Close all other menus first
    document.querySelectorAll('.shopping-tag-menu-container').forEach(c => c.style.display = 'none');
    container.style.display = isOpen ? 'none' : 'block';
}

async function removeBringItem(idx) {
    const item = shoppingItems[idx];
    if (!item) return;
    try {
        const data = await api('bring_remove', {}, 'POST', { 
            name: item.name, 
            rawName: item.rawName || '', 
            listUUID: shoppingListUUID 
        });
        if (data.success) {
            shoppingItems.splice(idx, 1);
            renderShoppingItems();
            showToast(t('toast.removed_from_list_short'), 'success');
            logOperation('bring_manual_remove', { name: item.name });
            // Update dashboard shopping count
            loadShoppingCount();
        }
    } catch (err) {
        showToast(t('shopping.remove_error'), 'error');
    }
}

async function generateSuggestions() {
    const btn = document.getElementById('btn-suggest');
    const suggestionsEl = document.getElementById('shopping-suggestions');
    
    btn.disabled = true;
    btn.innerHTML = `<div class="loading-spinner" style="display:inline-block;width:18px;height:18px;margin-right:8px;vertical-align:middle"></div> ${t('shopping.suggest_loading')}`;
    suggestionsEl.style.display = 'none';
    
    try {
        const data = await api('bring_suggest', {}, 'POST', {});
        
        btn.disabled = false;
        btn.innerHTML = `🤖 ${t('shopping.suggest_btn').replace('🤖 ', '')}`;
        
        if (!data.success) {
            showToast(data.error || t('shopping.suggest_error'), 'error');
            return;
        }
        
        suggestionItems = (data.suggestions || []).map(s => ({ ...s, selected: true }));
        
        // Show seasonal tip
        const tipEl = document.getElementById('seasonal-tip');
        if (data.seasonal_tip) {
            tipEl.style.display = 'block';
            tipEl.innerHTML = `🌿 <em>${escapeHtml(data.seasonal_tip)}</em>`;
        } else {
            tipEl.style.display = 'none';
        }
        
        renderSuggestions();
        suggestionsEl.style.display = 'block';
        document.getElementById('suggestion-actions').style.display = 'block';
        
        // Scroll to suggestions
        suggestionsEl.scrollIntoView({ behavior: 'smooth', block: 'start' });

        // AI enrich suggestions in background (best-effort)
        if (_geminiAvailable && suggestionItems.length > 0) {
            _enrichSuggestionsWithAI();
        }
        
    } catch (err) {
        btn.disabled = false;
        btn.innerHTML = `🤖 ${t('shopping.suggest_btn').replace('🤖 ', '')}`;
        console.error('Suggestion error:', err);
        showToast(t('error.connection'), 'error');
    }
}

function renderSuggestions() {
    const container = document.getElementById('suggestion-items');
    
    const priorityOrder = { 'alta': 0, 'media': 1, 'bassa': 2 };
    const sorted = [...suggestionItems].sort((a, b) => (priorityOrder[a.priority] || 2) - (priorityOrder[b.priority] || 2));
    
    container.innerHTML = sorted.map((item, idx) => {
        const catIcon = CATEGORY_ICONS[item.category] || '🛒';
        const priorityBadge = {
            'alta': `<span class="priority-badge priority-high">${t('shopping.priority_high')}</span>`,
            'media': `<span class="priority-badge priority-med">${t('shopping.priority_medium')}</span>`,
            'bassa': `<span class="priority-badge priority-low">${t('shopping.priority_low')}</span>`,
        }[item.priority] || '';
        
        return `
        <div class="suggestion-item ${item.selected ? 'selected' : ''}" onclick="toggleSuggestion(${idx})" data-suggestion-name="${escapeHtml(item.name)}">
            <div class="suggestion-check">${item.selected ? '☑️' : '⬜'}</div>
            <span class="shopping-item-icon">${catIcon}</span>
            <div class="suggestion-info">
                <div class="suggestion-name">${escapeHtml(item.name)}${item.specification ? ` <small>(${escapeHtml(item.specification)})</small>` : ''} ${priorityBadge}</div>
                <div class="suggestion-reason">${escapeHtml(item.reason)}</div>
            </div>
        </div>`;
    }).join('');
    
    updateSuggestionActionBtn();
}

async function _enrichSuggestionsWithAI() {
    try {
        const items = suggestionItems.map(s => ({
            name:     s.name,
            reason:   s.reason   || '',
            category: s.category || '',
            priority: s.priority || 'media',
        }));
        const data = await api('gemini_shopping_enrich', {}, 'POST', { items, lang: _currentLang });
        if (!data.success || !Array.isArray(data.items)) return;

        // For each item that has a tip, find its DOM element and append the tip
        data.items.forEach(enriched => {
            if (!enriched.tip) return;
            const nameAttr = enriched.name.replace(/"/g, '&quot;');
            const el = document.querySelector(`#suggestion-items [data-suggestion-name="${nameAttr}"]`);
            if (!el) return;
            const infoDiv = el.querySelector('.suggestion-info');
            if (!infoDiv) return;
            // Avoid duplicate tips
            if (infoDiv.querySelector('.suggestion-ai-tip')) return;
            const tipEl = document.createElement('div');
            tipEl.className = 'suggestion-ai-tip';
            tipEl.innerHTML = `💡 <em>${escapeHtml(enriched.tip)}</em>`;
            infoDiv.appendChild(tipEl);
        });
    } catch (e) {
        // best-effort — silently ignore
    }
}

function toggleSuggestion(idx) {
    const priorityOrder = { 'alta': 0, 'media': 1, 'bassa': 2 };
    const sorted = [...suggestionItems].sort((a, b) => (priorityOrder[a.priority] || 2) - (priorityOrder[b.priority] || 2));
    const actualItem = sorted[idx];
    // Find in original array
    const origIdx = suggestionItems.indexOf(actualItem);
    if (origIdx >= 0) {
        suggestionItems[origIdx].selected = !suggestionItems[origIdx].selected;
    }
    renderSuggestions();
}

function updateSuggestionActionBtn() {
    const selected = suggestionItems.filter(s => s.selected);
    const btn = document.querySelector('#suggestion-actions .btn-success');
    if (btn) {
        const nItems = selected.length;
        btn.textContent = `✅ ${nItems === 1 ? t('shopping.bring_add_one') : t('shopping.bring_add_many').replace('{n}', nItems)}`;
        btn.disabled = nItems === 0;
    }
}

async function addSelectedSuggestions() {
    const selected = suggestionItems.filter(s => s.selected);
    if (selected.length === 0) {
        showToast(t('error.select_items'), 'error');
        return;
    }
    
    const btn = document.querySelector('#suggestion-actions .btn-success');
    btn.disabled = true;
    btn.innerHTML = `<div class="loading-spinner" style="display:inline-block;width:18px;height:18px;margin-right:8px;vertical-align:middle"></div> ${t('shopping.bring_adding')}`;
    
    try {
        const items = selected.map(s => {
            return { name: s.name };
        });
        
        const data = await api('bring_add', {}, 'POST', { items, listUUID: shoppingListUUID });
        
        if (data.success) {
            let msg = data.added === 1 ? t('shopping.bring_added_one') : t('shopping.bring_added_many').replace('{n}', data.added);
            if (data.skipped > 0) msg += ` ${t('shopping.bring_skipped').replace('{n}', data.skipped)}`;
            showToast(msg, 'success');
            // Refresh list
            await loadShoppingList();
            // Update dashboard shopping count
            loadShoppingCount();
            // Clear suggestions
            document.getElementById('shopping-suggestions').style.display = 'none';
            suggestionItems = [];
        } else {
            showToast(data.error || t('error.generic'), 'error');
        }
    } catch (err) {
        showToast(t('error.connection'), 'error');
    }
    
    btn.disabled = false;
    btn.innerHTML = `✅ ${t('shopping.bring_add_selected')}`;
}

// ===== UTILITY FUNCTIONS =====

// ===== SCAN EXPIRY DATE WITH CAMERA + GEMINI AI =====
let expiryStream = null;

async function scanExpiryWithAI() {
    if (!_requireGemini()) return;
    // Create modal for camera capture
    document.getElementById('modal-content').innerHTML = `
        <div class="modal-header">
            <h3>${t('add.scan_expiry_title')}</h3>
            <button class="modal-close" onclick="closeExpiryScanner()">✕</button>
        </div>
        <div class="expiry-scanner">
            <div id="expiry-cam-container" style="height:180px;overflow:hidden;border-radius:10px;position:relative">
                <video id="expiry-video" autoplay playsinline style="width:100%;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%) scale(2);transform-origin:center center"></video>
                <canvas id="expiry-canvas" style="display:none"></canvas>
                <div style="position:absolute;inset:0;border:2px dashed rgba(255,255,255,0.5);border-radius:10px;pointer-events:none"></div>
            </div>
            <div id="expiry-preview-container" style="display:none;height:180px;overflow:hidden;border-radius:10px">
                <img id="expiry-preview-img" src="" alt="" style="width:100%;height:100%;object-fit:cover;border-radius:10px">
            </div>
            <p class="form-hint" style="text-align:center;margin:6px 0;font-size:0.8rem">${t('scanner.expiry_label_hint')}</p>
            <div id="expiry-scan-status" style="display:none;text-align:center;padding:8px">
                <div class="loading-spinner" style="margin:0 auto 6px"></div>
                <p>${t('scanner.ai_analyzing')}</p>
            </div>
            <div class="expiry-scanner-actions">
                <button class="btn btn-large btn-accent full-width" id="expiry-capture-btn" onclick="captureExpiry()">${t('scanner.capture_photo_btn')}</button>
                <button class="btn btn-large btn-secondary full-width" id="expiry-retake-btn" onclick="retakeExpiry()" style="display:none">${t('scanner.retake_btn')}</button>
            </div>
        </div>
    `;
    document.getElementById('modal-overlay').style.display = 'flex';
    
    // Start camera
    try {
        expiryStream = await navigator.mediaDevices.getUserMedia(getCameraConstraints());
        const video = document.getElementById('expiry-video');
        video.srcObject = expiryStream;
        await video.play();
    } catch (err) {
        console.error('Expiry camera error:', err);
        document.getElementById('expiry-cam-container').innerHTML = `
            <p style="color:var(--danger);text-align:center;padding:20px">⚠️ Impossibile accedere alla fotocamera</p>
        `;
    }
}

function closeExpiryScanner() {
    if (expiryStream) {
        expiryStream.getTracks().forEach(t => t.stop());
        expiryStream = null;
    }
    closeModal();
}

function captureExpiry() {
    const video = document.getElementById('expiry-video');
    const canvas = document.getElementById('expiry-canvas');
    const img = document.getElementById('expiry-preview-img');
    
    // Crop to center 50% (matching the 2x zoom view) for better AI accuracy
    const sw = video.videoWidth / 2;
    const sh = video.videoHeight / 2;
    const sx = (video.videoWidth - sw) / 2;
    const sy = (video.videoHeight - sh) / 2;
    canvas.width = sw;
    canvas.height = sh;
    const ctx = canvas.getContext('2d');
    ctx.drawImage(video, sx, sy, sw, sh, 0, 0, sw, sh);
    
    const dataUrl = canvas.toDataURL('image/jpeg', 0.85);
    img.src = dataUrl;
    
    // Stop camera
    if (expiryStream) {
        expiryStream.getTracks().forEach(t => t.stop());
        expiryStream = null;
    }
    video.srcObject = null;
    
    document.getElementById('expiry-cam-container').style.display = 'none';
    document.getElementById('expiry-preview-container').style.display = 'block';
    document.getElementById('expiry-capture-btn').style.display = 'none';
    document.getElementById('expiry-retake-btn').style.display = 'block';
    
    // Auto-analyze
    analyzeExpiryImage(dataUrl);
}

function retakeExpiry() {
    document.getElementById('expiry-cam-container').style.display = 'block';
    document.getElementById('expiry-preview-container').style.display = 'none';
    document.getElementById('expiry-capture-btn').style.display = 'block';
    document.getElementById('expiry-retake-btn').style.display = 'none';
    document.getElementById('expiry-scan-status').style.display = 'none';
    
    // Restart camera
    navigator.mediaDevices.getUserMedia(getCameraConstraints()).then(stream => {
        expiryStream = stream;
        const video = document.getElementById('expiry-video');
        video.srcObject = stream;
        video.play();
    }).catch(err => console.error(err));
}

async function analyzeExpiryImage(dataUrl) {
    const statusDiv = document.getElementById('expiry-scan-status');
    statusDiv.style.display = 'block';
    statusDiv.innerHTML = `<div class="loading-spinner" style="margin:0 auto 8px"></div><p>${t('scanner.ai_analyzing')}</p>`;
    
    try {
        // Remove data:image/jpeg;base64, prefix
        const base64 = dataUrl.split(',')[1];
        
        const result = await api('gemini_expiry', {}, 'POST', { image: base64 });
        
        if (result.success && result.expiry_date) {
            // Auto-fill the expiry date
            const expiryInput = document.getElementById('add-expiry');
            if (expiryInput) {
                expiryInput.value = result.expiry_date;
            }
            statusDiv.innerHTML = `<p style="color:var(--success);font-weight:600">✅ Data trovata: ${formatDate(result.expiry_date)}</p>`;
            
            // Close modal after delay
            setTimeout(() => closeExpiryScanner(), 1500);
        } else if (result.error === 'no_api_key') {
            statusDiv.innerHTML = `<p style="color:var(--warning)">⚠️ Chiave API Gemini non configurata.<br><small>Aggiungi GEMINI_API_KEY nel file .env sul server.</small></p>`;
        } else {
            statusDiv.innerHTML = `<p style="color:var(--danger)">❌ Non riesco a leggere la data. ${result.raw_text ? '<br><small>Letto: ' + escapeHtml(result.raw_text) + '</small>' : ''}</p>
                <button class="btn btn-secondary" onclick="retakeExpiry()" style="margin-top:8px">${t('btn.retry')}</button>`;
        }
    } catch (err) {
        console.error('Expiry AI error:', err);
        statusDiv.innerHTML = `<p style="color:var(--danger)">❌ ${t('error.network_retry')}</p>
            <button class="btn btn-secondary" onclick="retakeExpiry()" style="margin-top:8px">${t('btn.retry')}</button>`;
    }
}

function escapeHtml(str) {
    if (!str) return '';
    const div = document.createElement('div');
    div.textContent = str;
    return div.innerHTML;
}

function formatDate(dateStr) {
    if (!dateStr) return '';
    const d = new Date(dateStr + 'T00:00:00');
    const _loc1 = _currentLang === 'de' ? 'de-DE' : _currentLang === 'en' ? 'en-GB' : 'it-IT';
    return d.toLocaleDateString(_loc1, { day: '2-digit', month: 'short', year: 'numeric' });
}

function formatDateTime(dtStr) {
    if (!dtStr) return '';
    const d = new Date(dtStr.replace(' ', 'T'));
    const _loc2 = _currentLang === 'de' ? 'de-DE' : _currentLang === 'en' ? 'en-GB' : 'it-IT';
    return d.toLocaleDateString(_loc2, { day: '2-digit', month: 'short' }) + ' ' + 
           d.toLocaleTimeString(_loc2, { hour: '2-digit', minute: '2-digit' });
}

function daysUntilExpiry(dateStr) {
    if (!dateStr) return Infinity;
    const expiry = new Date(dateStr + 'T00:00:00');
    const today = new Date();
    today.setHours(0, 0, 0, 0);
    return Math.round((expiry - today) / 86400000);
}

function adjustQty(inputId, delta) {
    const input = document.getElementById(inputId);
    let val = parseFloat(input.value) || 0;
    val = Math.max(0.1, val + delta);
    input.value = Math.round(val * 10) / 10;
}

function showLoading(show) {
    document.getElementById('loading').style.display = show ? 'flex' : 'none';
}

function showToast(message, type = '') {
    const toast = document.getElementById('toast');
    toast.textContent = message;
    toast.className = 'toast show ' + type;
    setTimeout(() => {
        toast.className = 'toast';
    }, 3000);
}

// ===== LOG =====
let _logOffset = 0;
const LOG_PAGE_SIZE = 50;

async function loadLog(more = false) {
    if (!more) {
        _logOffset = 0;
        document.getElementById('log-list').innerHTML = '<p style="text-align:center;color:var(--text-muted)">Caricamento...</p>';
    }

    try {
        const result = await api(`transactions_list&limit=${LOG_PAGE_SIZE}&offset=${_logOffset}`);
        const txns = result.transactions || [];

        let html = '';
        if (!more && txns.length === 0) {
            html = `<p style="text-align:center;color:var(--text-muted)">${t('log.empty')}</p>`;
        } else {
            let lastDate = more ? '' : null;
            const _logLocale = _currentLang === 'de' ? 'de-DE' : _currentLang === 'en' ? 'en-GB' : 'it-IT';
            txns.forEach(tx => {
                const dt = new Date(tx.created_at + 'Z');
                const dateStr = dt.toLocaleDateString(_logLocale, { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' });
                const timeStr = dt.toLocaleTimeString(_logLocale, { hour: '2-digit', minute: '2-digit' });

                if (dateStr !== lastDate) {
                    html += `<div class="log-date-header">${dateStr}</div>`;
                    lastDate = dateStr;
                }

                let icon, typeLabel, colorClass;
                if (tx.type === 'bring') {
                    icon = '🛒';
                    typeLabel = t('log.type_bring');
                    colorClass = 'log-bring';
                } else if (tx.type === 'in') {
                    icon = '➕';
                    typeLabel = t('log.type_added');
                    colorClass = 'log-in';
                } else {
                    icon = '➖';
                    typeLabel = tx.type === 'waste' ? t('log.type_waste') : t('log.type_used');
                    colorClass = 'log-out';
                }
                const brand = tx.brand ? ` <em>(${tx.brand})</em>` : '';
                const loc = tx.location || '';
                const locLabels = Object.fromEntries(Object.entries(LOCATIONS).map(([k,v]) => [k, `${v.icon} ${v.label}`]));
                const locStr = tx.type === 'bring' ? '' : (locLabels[loc] || ('📍 ' + loc));
                const isAnnotation = (tx.notes || '').includes('[Annullato]');
                const isRecipeNote = !isAnnotation && (tx.notes || '').startsWith('Ricetta:');
                const notes = tx.notes && !isAnnotation && !isRecipeNote ? ` · ${tx.notes}` : '';
                const recipeNote = isRecipeNote ? `<div class="log-recipe-note">🍳 ${escapeHtml(tx.notes)}</div>` : '';
                const undone = tx.undone == 1 || isAnnotation;

                // Can undo if within 24h, not already undone, not a bring entry, not a counter-transaction
                const ageMs = Date.now() - new Date(tx.created_at + 'Z').getTime();
                const canUndo = !undone && tx.type !== 'bring' && ageMs < 86400000;

                html += `<div class="log-entry ${colorClass}${undone ? ' log-undone' : ''}" id="log-entry-${tx.id}">`;
                html += `<span class="log-icon">${icon}</span>`;
                html += `<div class="log-info">`;
                html += `<div class="log-product"><strong>${escapeHtml(tx.name)}</strong>${brand}${undone ? ` <span class="log-undone-badge">${t('log.undone_badge')}</span>` : ''}</div>`;
                html += `<div class="log-detail">${typeLabel} ${tx.type !== 'bring' ? (tx.quantity + ' ' + (tx.unit || '')) + ' · ' : ''}${locStr}${notes} · ${timeStr}</div>`;
                html += recipeNote;
                html += `</div>`;
                if (canUndo) {
                    html += `<button class="btn-log-undo" onclick="undoTransactionEntry(${tx.id}, '${escapeHtml(tx.type)}', '${escapeHtml(tx.name || '')}')" title="${t('log.undo_title')}">↩</button>`;
                }
                html += `</div>`;
            });
        }

        if (more) {
            document.getElementById('log-list').insertAdjacentHTML('beforeend', html);
        } else {
            document.getElementById('log-list').innerHTML = html;
        }

        _logOffset += txns.length;
        document.getElementById('log-load-more').style.display = txns.length >= LOG_PAGE_SIZE ? '' : 'none';

    } catch (err) {
        console.error('Log load error:', err);
        if (!more) document.getElementById('log-list').innerHTML = `<p style="text-align:center;color:var(--danger)">${t('log.load_error')}</p>`;
    }
}

async function undoTransactionEntry(id, type, name) {
    const action = type === 'in' ? t('log.undo_action_remove') : t('log.undo_action_restore');
    const msg = t('log.undo_confirm').replace('{action}', action).replace('{name}', name);
    _showDestructiveConfirm(
        t('log.undo_title') || '↩ Annulla operazione',
        msg,
        () => _doUndoTransaction(id, type, name)
    );
}

async function _doUndoTransaction(id, type, name) {
    try {
        const res = await api('transaction_undo', {}, 'POST', { id });
        if (res.success) {
            showToast(t('log.undo_success').replace('{name}', res.name || name), 'success');
            // Mark the entry visually without reloading all
            const el = document.getElementById(`log-entry-${id}`);
            if (el) {
                el.classList.add('log-undone');
                const undoBtn = el.querySelector('.btn-log-undo');
                if (undoBtn) undoBtn.remove();
                const nameEl = el.querySelector('.log-product strong');
                if (nameEl && !el.querySelector('.log-undone-badge')) {
                    nameEl.insertAdjacentHTML('afterend', ` <span class="log-undone-badge">${t('log.undone_badge')}</span>`);
                }
            }
        } else if (res.already_undone) {
            showToast(t('log.already_undone'), 'info');
        } else if (res.too_old) {
            showToast(t('log.too_old'), 'error');
        } else {
            showToast(res.error || t('log.undo_error'), 'error');
        }
    } catch (e) {
        showToast(t('error.network'), 'error');
    }
}

// ===== WEEKLY MEAL PLAN =====

/**
 * All selectable meal categories per slot.
 * id must be URL-safe; icon + label shown in UI.
 */
const MEAL_PLAN_TYPE_DEFS = [
    { id: 'pasta',      icon: '🍝', i18nKey: 'meal_plan_types.pasta' },
    { id: 'riso',       icon: '🍚', i18nKey: 'meal_plan_types.riso' },
    { id: 'carne',      icon: '🥩', i18nKey: 'meal_plan_types.carne' },
    { id: 'pesce',      icon: '🐟', i18nKey: 'meal_plan_types.pesce' },
    { id: 'legumi',     icon: '🫘', i18nKey: 'meal_plan_types.legumi' },
    { id: 'uova',       icon: '🥚', i18nKey: 'meal_plan_types.uova' },
    { id: 'formaggio',  icon: '🧀', i18nKey: 'meal_plan_types.formaggio' },
    { id: 'pizza',      icon: '🍕', i18nKey: 'meal_plan_types.pizza' },
    { id: 'affettati',  icon: '🥓', i18nKey: 'meal_plan_types.affettati' },
    { id: 'verdure',    icon: '🥦', i18nKey: 'meal_plan_types.verdure' },
    { id: 'zuppa',      icon: '🍲', i18nKey: 'meal_plan_types.zuppa' },
    { id: 'insalata',   icon: '🥗', i18nKey: 'meal_plan_types.insalata' },
    { id: 'pane',       icon: '🥪', i18nKey: 'meal_plan_types.pane' },
    { id: 'dolce',      icon: '🍰', i18nKey: 'meal_plan_types.dolce' },
    { id: 'libero',     icon: '🎲', i18nKey: 'meal_plan_types.libero' },
];

function getMealPlanTypes() {
    return MEAL_PLAN_TYPE_DEFS.map(mpt => ({ ...mpt, label: t(mpt.i18nKey) }));
}

function getMealPlanTypeMap() {
    const map = {};
    getMealPlanTypes().forEach(mpt => { map[mpt.id] = mpt; });
    return map;
}

function getWeekDaysShortLabels() {
    return [
        t('days.mon_short'),
        t('days.tue_short'),
        t('days.wed_short'),
        t('days.thu_short'),
        t('days.fri_short'),
        t('days.sat_short'),
        t('days.sun_short'),
    ];
}

/** Default weekly plan as requested. */
const DEFAULT_MEAL_PLAN = {
    1: { pranzo: 'pasta',   cena: 'pesce' },
    2: { pranzo: 'riso',    cena: 'carne' },
    3: { pranzo: 'legumi',  cena: 'uova' },
    4: { pranzo: 'pasta',   cena: 'pesce' },
    5: { pranzo: 'riso',    cena: 'formaggio' },
    6: { pranzo: 'legumi',  cena: 'pizza' },
    0: { pranzo: 'carne',   cena: 'affettati' },  // 0 = Sunday (getDay())
};

function getMealPlan() {
    const s = getSettings();
    return s.meal_plan || DEFAULT_MEAL_PLAN;
}

/** Return today's planned meal type for a given slot ('pranzo'|'cena'), or null. */
function getTodayMealPlanType(slot) {
    const s = getSettings();
    if (s.meal_plan_enabled === false) return null;
    const dow = new Date().getDay(); // 0=Sun,1=Mon,...,6=Sat
    const plan = getMealPlan();
    return plan[dow]?.[slot] || null;
}

/** Toggle handler for the enable/disable switch in settings. */
function onMealPlanEnabledChange(el) {
    const s = getSettings();
    s.meal_plan_enabled = el.checked;
    saveSettingsToStorage(s);
    const mpConfigSection = document.getElementById('meal-plan-config-section');
    if (mpConfigSection) mpConfigSection.style.display = el.checked ? '' : 'none';
    const mpLegendCard = document.getElementById('meal-plan-legend-card');
    if (mpLegendCard) mpLegendCard.style.display = el.checked ? '' : 'none';
    // Close picker if open
    const picker = document.getElementById('meal-plan-picker');
    if (picker) picker.style.display = 'none';
}

/**
 * Render the weekly meal plan editor into #meal-plan-grid.
 * Each cell shows the current type badge + a picker dropdown.
 */
function renderMealPlanEditor() {
    const container = document.getElementById('meal-plan-grid');
    if (!container) return;
    const plan = getMealPlan();
    // JS getDay: 0=Sun … but we display Mon-Sun (1..6,0)
    const dayOrder = [1,2,3,4,5,6,0];
    const today = new Date().getDay();
    const mealPlanTypeMap = getMealPlanTypeMap();
    const weekDaysShort = getWeekDaysShortLabels();

    const header = `<div class="mplan-header">
        <span class="mplan-col-header">🌤️ ${t('meal_types.pranzo')}</span>
        <span class="mplan-col-header">🌙 ${t('meal_types.cena')}</span>
    </div>`;

    const rows = dayOrder.map((dow, i) => {
        const pranzo = plan[dow]?.pranzo || 'libero';
        const cena   = plan[dow]?.cena   || 'libero';
        const pt = mealPlanTypeMap[pranzo] || mealPlanTypeMap.libero;
        const ct = mealPlanTypeMap[cena]   || mealPlanTypeMap.libero;
        const todayClass = dow === today ? ' mplan-row-today' : '';
        return `<div class="mplan-row${todayClass}">
            <div class="mplan-day-name">${weekDaysShort[i]}</div>
            <span class="mplan-badge mplan-badge-pranzo" onclick="openMealPlanPicker(${dow},'pranzo',this)">${pt.icon} ${pt.label}</span>
            <span class="mplan-badge mplan-badge-cena" onclick="openMealPlanPicker(${dow},'cena',this)">${ct.icon} ${ct.label}</span>
        </div>`;
    }).join('');

    container.innerHTML = header + rows;
}

let _mplanPickerTarget = null; // {dow, slot, badgeEl}
function openMealPlanPicker(dow, slot, badgeEl) {
    // Close any open picker first
    closeMealPlanPicker();
    _mplanPickerTarget = { dow, slot, badgeEl };
    const picker = document.getElementById('meal-plan-picker');
    if (!picker) return;
    const plan = getMealPlan();
    const current = plan[dow]?.[slot] || 'libero';
    picker.innerHTML = getMealPlanTypes().map(mpt =>
        `<button class="mplan-pick-btn${mpt.id === current ? ' active' : ''}" onclick="selectMealPlanType(${dow},'${slot}','${mpt.id}')">${mpt.icon} ${mpt.label}</button>`
    ).join('');
    // Position vertically near the badge, centered horizontally (CSS handles centering)
    const rect = badgeEl.getBoundingClientRect();
    const pickerEl = picker;
    // Show first to measure height
    pickerEl.style.display = 'flex';
    const pickerH = pickerEl.offsetHeight || 160;
    const spaceBelow = window.innerHeight - rect.bottom - 8;
    const top = spaceBelow >= pickerH
        ? rect.bottom + 8
        : Math.max(8, rect.top - pickerH - 8);
    pickerEl.style.top = top + 'px';
    // Close on outside tap
    setTimeout(() => document.addEventListener('click', _mplanPickerOutside, { once: true }), 0);
}
function _mplanPickerOutside(e) {
    const picker = document.getElementById('meal-plan-picker');
    if (picker && !picker.contains(e.target)) closeMealPlanPicker();
}
function closeMealPlanPicker() {
    const picker = document.getElementById('meal-plan-picker');
    if (picker) picker.style.display = 'none';
    _mplanPickerTarget = null;
    document.removeEventListener('click', _mplanPickerOutside);
}
function selectMealPlanType(dow, slot, typeId) {
    const s = getSettings();
    if (!s.meal_plan) s.meal_plan = JSON.parse(JSON.stringify(DEFAULT_MEAL_PLAN));
    if (!s.meal_plan[dow]) s.meal_plan[dow] = {};
    s.meal_plan[dow][slot] = typeId;
    saveSettingsToStorage(s);
    closeMealPlanPicker();
    renderMealPlanEditor();
}
function resetMealPlan() {
    const s = getSettings();
    s.meal_plan = JSON.parse(JSON.stringify(DEFAULT_MEAL_PLAN));
    saveSettingsToStorage(s);
    renderMealPlanEditor();
    showToast(t('meal_plan.reset_success'), 'success');
}

// ===== RECIPE GENERATION =====
const MEAL_TYPE_DEFS = [
    { id: 'colazione',  icon: '☀️', i18nKey: 'meal_types.colazione',  from: 6,  to: 11 },
    { id: 'pranzo',     icon: '🍽️', i18nKey: 'meal_types.pranzo',     from: 11, to: 14 },
    { id: 'merenda',    icon: '🍪', i18nKey: 'meal_types.merenda',    from: 14, to: 17 },
    { id: 'cena',       icon: '🌙', i18nKey: 'meal_types.cena',       from: 17, to: 6  },
    { id: 'dolce',      icon: '🍰', i18nKey: 'meal_types.dolce',      from: -1, to: -1 },
    { id: 'succo',      icon: '🧃', i18nKey: 'meal_types.succo',      from: -1, to: -1 },
];

function getMealTypes() {
    return MEAL_TYPE_DEFS.map(m => ({ ...m, label: t(m.i18nKey) }));
}

function getMealSubTypes() {
    return {
        dolce: [
            { id: 'torta',      icon: '🎂', label: t('meal_sub.dolce_torta') },
            { id: 'crema',      icon: '🍮', label: t('meal_sub.dolce_crema') },
            { id: 'crumble',    icon: '🥧', label: t('meal_sub.dolce_crumble') },
            { id: 'biscotti',   icon: '🍪', label: t('meal_sub.dolce_biscotti') },
            { id: 'frutta',     icon: '🍓', label: t('meal_sub.dolce_frutta') },
        ],
        succo: [
            { id: 'dolce',        icon: '🍑', label: t('meal_sub.succo_dolce') },
            { id: 'energizzante', icon: '⚡', label: t('meal_sub.succo_energizzante') },
            { id: 'detox',        icon: '🥬', label: t('meal_sub.succo_detox') },
            { id: 'rinfrescante', icon: '🧊', label: t('meal_sub.succo_rinfrescante') },
            { id: 'vitaminico',   icon: '🍊', label: t('meal_sub.succo_vitaminico') },
        ]
    };
}

function getMealLabels() {
    const labels = {};
    getMealTypes().forEach(m => { labels[m.id] = `${m.icon} ${m.label}`; });
    return labels;
}

function getMealType() {
    const hour = new Date().getHours();
    for (const m of MEAL_TYPE_DEFS) {
        if (m.from < m.to) { if (hour >= m.from && hour < m.to) return m.id; }
        else { if (hour >= m.from || hour < m.to) return m.id; }
    }
    return 'cena';
}

function _normalizeMealId(rawMeal) {
    if (!rawMeal) return '';
    let meal = String(rawMeal).trim().toLowerCase();
    meal = meal.replace(/^meal_types?\./, '');
    if (meal === 'lunch') return 'pranzo';
    if (meal === 'dinner') return 'cena';
    return meal;
}

function _mealLabel(rawMeal) {
    const mealId = _normalizeMealId(rawMeal);
    const labels = getMealLabels();
    if (labels[mealId]) return labels[mealId];
    const translated = mealId ? t(`meal_types.${mealId}`) : '';
    if (translated && translated !== `meal_types.${mealId}`) return translated;
    return mealId || String(rawMeal || '');
}

function getSelectedMealType() {
    const checked = document.querySelector('input[name="recipe-meal"]:checked');
    return checked ? checked.value : getMealType();
}

// ===== RECIPE ARCHIVE (DB-backed) =====
let _recipeArchiveCache = null;

async function getRecipeArchive() {
    if (_recipeArchiveCache !== null) return _recipeArchiveCache;
    try {
        const res = await api('recipes_list');
        if (res.success) {
            _recipeArchiveCache = res.recipes || [];
            return _recipeArchiveCache;
        }
    } catch(e) { console.warn('Failed to load recipes from DB:', e); }
    return [];
}

async function saveRecipeToArchive(recipe) {
    const today = new Date().toISOString().slice(0, 10);
    try {
        await api('recipes_save', {}, 'POST', { date: today, meal: recipe.meal, recipe });
        // Invalidate cache and refresh the archive list
        _recipeArchiveCache = null;
        loadRecipeArchive();
    } catch(e) { console.error('Failed to save recipe:', e); }
}

async function getTodayRecipeTitles() {
    const archive = await getRecipeArchive();
    const today = new Date().toISOString().slice(0, 10);
    return archive
        .filter(e => e.date === today && e.recipe && e.recipe.title)
        .map(e => e.recipe.title);
}

let _recipeArchiveEntries = [];

async function loadRecipeArchive() {
    const container = document.getElementById('recipe-archive');
    if (!container) return;
    const archive = await getRecipeArchive();
    _recipeArchiveEntries = archive;
    
    if (archive.length === 0) {
        container.innerHTML = `<div class="empty-state" style="padding:20px"><div class="empty-state-icon">🍳</div><p>${t('recipes.archive_empty')}</p></div>`;
        return;
    }
    
    // Group by date
    const byDate = {};
    for (const entry of archive) {
        if (!byDate[entry.date]) byDate[entry.date] = [];
        byDate[entry.date].push(entry);
    }
    
    let html = '';
    let flatIdx = 0;
    const today = new Date().toISOString().slice(0, 10);
    const yesterday = new Date(Date.now() - 86400000).toISOString().slice(0, 10);
    
    for (const [date, entries] of Object.entries(byDate)) {
        const _mealLocale = _currentLang === 'de' ? 'de-DE' : _currentLang === 'en' ? 'en-GB' : 'it-IT';
        let dateLabel = new Date(date + 'T12:00:00').toLocaleDateString(_mealLocale, { weekday: 'long', day: 'numeric', month: 'long' });
        if (date === today) dateLabel = t('date.today');
        else if (date === yesterday) dateLabel = t('date.yesterday');
        
        html += `<div class="recipe-archive-day">`;
        html += `<div class="recipe-archive-date">${escapeHtml(dateLabel)}</div>`;
        
        for (const entry of entries) {
            const r = entry.recipe;
            const mealIcon = _mealLabel(r.meal || entry.meal);
            const tags = (r.tags || []).slice(0, 3).join(', ');
            // Find this entry's index in the flat archive array
            const archiveIdx = archive.indexOf(entry);
            html += `<div class="recipe-archive-card" onclick="viewArchivedRecipe(${archiveIdx})">`;
            html += `<div class="recipe-archive-card-header">`;
            html += `<span class="recipe-archive-meal">${mealIcon}</span>`;
            html += `<span class="recipe-archive-title">${escapeHtml(r.title)}</span>`;
            html += `</div>`;
            html += `<div class="recipe-archive-card-meta">`;
            if (r.prep_time) html += `<span>🔪 ${r.prep_time}</span>`;
            if (r.cook_time) html += `<span>🔥 ${r.cook_time}</span>`;
            html += `<span>👥 ${r.persons}</span>`;
            if (tags) html += `<span>${tags}</span>`;
            html += `</div></div>`;
            flatIdx++;
        }
        html += `</div>`;
    }
    
    container.innerHTML = html;
}

function viewArchivedRecipe(idx) {
    const entry = _recipeArchiveEntries[idx];
    if (!entry) return;
    _cachedRecipe = { meal: _normalizeMealId(entry.meal), recipe: entry.recipe };
    renderRecipe(entry.recipe);
    document.getElementById('recipe-overlay').style.display = 'flex';
    document.getElementById('recipe-ask').style.display = 'none';
    document.getElementById('recipe-loading').style.display = 'none';
    document.getElementById('recipe-result').style.display = '';
}

let _cachedRecipe = null;
let _generatedTodayTitles = []; // client-side list, robust vs race conditions
let _recipeVariationCount = {}; // { 'pranzo': 0, 'cena': 1, ... }
let _rejectedRecipeIngredients = []; // ingredient names from previously rejected recipes

function openRecipeDialog() {
    if (!_requireGemini()) return;
    const meal = getMealType();
    const settings = getSettings();
    document.getElementById('recipe-overlay').style.display = 'flex';

    // Build meal selector radios
    const mealGrid = document.getElementById('recipe-meal-grid');
    if (mealGrid) {
        mealGrid.innerHTML = getMealTypes().map(m => {
            const checked = m.id === meal ? ' checked' : '';
            return `<label class="recipe-meal-chip"><input type="radio" name="recipe-meal" value="${m.id}"${checked}> ${m.icon} ${m.label}</label>`;
        }).join('');
    }
    updateRecipeMealTitle();

    // Show today's meal plan hint
    _renderMealPlanHint(meal);

    // Check for cached recipe matching current meal type
    if (_cachedRecipe && _cachedRecipe.meal === meal && _cachedRecipe.recipe) {
        document.getElementById('recipe-ask').style.display = 'none';
        document.getElementById('recipe-loading').style.display = 'none';
        renderRecipe(_cachedRecipe.recipe);
        document.getElementById('recipe-result').style.display = '';
        return;
    }

    // Pre-fill persons from settings
    document.getElementById('recipe-persons').value = settings.default_persons || 1;
    
    // Pre-select option chips from settings
    const prefMap = {
        'veloce': 'recipe-opt-veloce',
        'pocafame': 'recipe-opt-pocafame', 
        'scadenze': 'recipe-opt-scadenze',
        'salutare': 'recipe-opt-healthy',
        'opened': 'recipe-opt-opened',
        'zerowaste': 'recipe-opt-zerowaste'
    };
    Object.entries(prefMap).forEach(([key, id]) => {
        const cb = document.getElementById(id);
        if (cb) cb.checked = settings.recipe_prefs && settings.recipe_prefs.includes(key);
    });
    
    document.getElementById('recipe-ask').style.display = '';
    document.getElementById('recipe-loading').style.display = 'none';
    document.getElementById('recipe-result').style.display = 'none';
}

// Toggle recipe option chip
function toggleRecipeOption(btn) {
    btn.classList.toggle('active');
}

function closeRecipeDialog() {
    document.getElementById('recipe-overlay').style.display = 'none';
}

function adjustRecipePersons(delta) {
    const input = document.getElementById('recipe-persons');
    let val = parseInt(input.value) || 1;
    val = Math.max(1, Math.min(20, val + delta));
    input.value = val;
}

let _recipeUseContext = null; // { idx, productId, btn, qtyNumber }
let _recipeUseConfMode = null;
let _recipeUseNormalUnit = 'pz';

async function useRecipeIngredient(idx, productId, location, qtyNumber, btn, recipeQty) {
    if (btn.disabled) return;
    if (!qtyNumber || qtyNumber <= 0) qtyNumber = 1;
    
    _recipeUseContext = { idx, productId, btn, qtyNumber, recipeQty };
    _recipeUseConfMode = null;
    
    // Fetch inventory to build the modal
    try {
        const data = await api('inventory_list');
        const items = (data.inventory || []).filter(i => i.product_id == productId);
        
        if (items.length === 0) {
            showToast(t('error.not_in_inventory'), 'error');
            return;
        }
        
        const unit = items[0].unit || 'pz';
        const pkgSize = parseFloat(items[0].default_quantity) || 0;
        const pkgUnit = items[0].package_unit || '';
        const isConf = unit === 'conf' && pkgSize > 0 && pkgUnit;
        
        // Find opened package location
        const openedItem = items.find(_isOpenedInventoryItem);
        const defaultLoc = openedItem ? openedItem.location : (items.find(i => i.location === location) ? location : items[0].location);
        
        // Build location buttons
        const productLocations = [...new Set(items.map(i => i.location))];
        const locButtons = productLocations.map(loc => {
            const locInfo = LOCATIONS[loc] || { icon: '📦', label: loc };
            const locItems = items.filter(i => i.location === loc);
            const locQty = locItems.reduce((s, i) => s + parseFloat(i.quantity), 0);
            const qtyLabel = formatQuantity(locQty, unit, pkgSize, pkgUnit);
            const openedBadge = _locationHasOpenedPackage(items, loc)
                ? ` <span class="loc-opened-badge">🔓 ${t('use.opened_badge')}</span>`
                : '';
            return `<button type="button" class="loc-btn ${loc === defaultLoc ? 'active' : ''}${openedBadge ? ' loc-btn-opened' : ''}" onclick="selectRecipeUseLoc(this, '${loc}')">${locInfo.icon} ${locInfo.label}${openedBadge}<br><small>${qtyLabel}</small></button>`;
        }).join('');
        
        // Build quantity controls
        let qtySection = '';
        let defaultQtyValue = qtyNumber;
        
        if (isConf) {
            const totalConf = items.reduce((s, i) => s + parseFloat(i.quantity), 0);
            const totalSub = totalConf * pkgSize;
            const unitLabels = { 'ml': 'ml', 'g': 'g', 'pz': 'pz' };
            const subLabel = unitLabels[pkgUnit] || pkgUnit;
            _recipeUseConfMode = { packageSize: pkgSize, packageUnit: pkgUnit, totalSub, totalConf, subLabel, _activeUnit: 'sub' };
            
            // qtyNumber from recipe is in sub-units (g, ml)
            const step = getSubUnitStep(pkgUnit);
            defaultQtyValue = qtyNumber;
            
            qtySection = `
                <div class="use-unit-switch" style="display:flex;margin-bottom:8px">
                    <button type="button" class="use-unit-btn active" id="ruse-unit-sub" onclick="switchRecipeUseUnit('sub')">${subLabel}</button>
                    <button type="button" class="use-unit-btn" id="ruse-unit-conf" onclick="switchRecipeUseUnit('conf')">${t('recipes.packs_label')}</button>
                </div>
                <p id="ruse-hint" style="font-size:0.85rem;color:var(--text-muted);margin-bottom:8px">${t('recipes.quantity_in_total').replace('{unit}', subLabel).replace('{total}', Math.round(totalSub) + subLabel)}</p>
                <div class="qty-control">
                    <button type="button" class="qty-btn" onclick="adjustRecipeUseQty(-1)">−</button>
                    <input type="number" id="ruse-quantity" value="${defaultQtyValue}" min="${step}" step="${step}" class="qty-input"
                           oninput="_scaleRecipeAutoFillPaused=true; _cancelScaleAutoConfirm(false); var h=document.getElementById('ruse-scale-hint'); if(h) h.style.display='none';">
                    <button type="button" class="qty-btn" onclick="adjustRecipeUseQty(1)">+</button>
                </div>`;
        } else {
            _recipeUseNormalUnit = unit;
            const unitLabels = { 'pz': 'pz', 'g': 'g', 'ml': 'ml' };
            const unitLabel = unitLabels[unit] || unit;
            const inputMin = '0.1';
            qtySection = `
                <p style="font-size:0.85rem;color:var(--text-muted);margin-bottom:8px">${t('recipes.amount_label')} (${unitLabel}):</p>
                <div class="qty-control">
                    <button type="button" class="qty-btn" onclick="adjustRecipeUseQty(-1)">−</button>
                    <input type="number" id="ruse-quantity" value="${defaultQtyValue}" min="${inputMin}" step="any" class="qty-input"
                           oninput="_scaleRecipeAutoFillPaused=true; _cancelScaleAutoConfirm(false); var h=document.getElementById('ruse-scale-hint'); if(h) h.style.display='none';">
                    <button type="button" class="qty-btn" onclick="adjustRecipeUseQty(1)">+</button>
                </div>`;
        }
        
        // Scale live UI: show only when scale is connected and unit is g or ml
        const availInfo = items.map(i => {
            const loc = LOCATIONS[i.location] || { icon: '📦', label: i.location };
            return `${loc.icon} ${formatQuantity(i.quantity, i.unit, i.default_quantity, i.package_unit)}`;
        }).join(' · ');

        const showScaleLive = _scaleConnected && (unit === 'g' || unit === 'ml' ||
            (_recipeUseConfMode && ((_recipeUseConfMode.packageUnit || '').toLowerCase() === 'g' || (_recipeUseConfMode.packageUnit || '').toLowerCase() === 'ml')));
        const scaleLiveSection = showScaleLive ? `
            <div id="ruse-scale-live-box" class="scale-live-box" style="flex-direction:column;align-items:stretch;border-color:var(--color-accent,#7c3aed)">
                <div style="display:flex;align-items:center;gap:8px;margin-bottom:4px">
                    <span class="scale-live-icon">⚖️</span>
                    <span id="ruse-scale-live-val" class="scale-live-val" style="color:var(--color-accent,#7c3aed)">— —</span>
                    <span id="ruse-scale-live-status" style="font-size:0.75rem;color:var(--text-muted);margin-left:auto"></span>
                </div>
                <div style="height:4px;background:var(--border);border-radius:2px;overflow:hidden;margin-bottom:4px">
                    <div id="ruse-scale-progress-bar" style="height:100%;width:0%;background:var(--color-accent,#7c3aed);transition:none;border-radius:2px"></div>
                </div>
                <div style="height:4px;background:var(--border);border-radius:2px;overflow:hidden;display:none" id="ruse-scale-confirm-wrap">
                    <div id="ruse-scale-confirm-bar" style="height:100%;width:100%;background:#22c55e;transition:none;border-radius:2px"></div>
                </div>
                <div id="ruse-scale-live-label" class="scale-live-label" style="margin-top:3px">${t('recipes.scale_wait_stable')}</div>
            </div>` : '';

        document.getElementById('modal-content').innerHTML = `
            <div class="modal-header">
                <h3>📤 ${t('recipes.use_ingredient_title')}</h3>
                <button class="modal-close" onclick="closeModal()">✕</button>
            </div>
            <div style="padding:0 16px 16px">
                <p style="margin-bottom:4px;font-weight:600">${escapeHtml(items[0].name)}</p>
                ${recipeQty ? `<p style="margin-bottom:8px;background:var(--bg-elevated,rgba(124,58,237,0.12));border-left:3px solid var(--color-accent,#7c3aed);border-radius:6px;padding:6px 10px;font-size:0.9rem">📋 ${t('recipes.recipe_qty_label')}: <strong>${escapeHtml(recipeQty)}</strong></p>` : ''}
                <p style="font-size:0.82rem;color:var(--text-muted);margin-bottom:12px">📦 ${availInfo}</p>
                ${scaleLiveSection}
                <div class="form-group">
                    <label>📍 ${t('recipes.from_where_label')}</label>
                    <div class="location-selector">${locButtons}</div>
                    <input type="hidden" id="ruse-location" value="${defaultLoc}">
                </div>
                <div class="form-group">
                    <label>${t('recipes.amount_label')}?</label>
                    ${qtySection}
                    <small id="ruse-scale-hint" style="display:none; color: var(--color-accent, #7c3aed); margin-top:4px"></small>
                </div>
                <button type="button" id="btn-ruse-submit" class="btn btn-large btn-danger full-width move-countdown-btn" onclick="submitRecipeUse(false)" style="margin-top:8px">
                    📤 ${t('recipes.use_amount_btn')}
                </button>
                <button type="button" class="btn btn-large btn-secondary full-width" style="margin-top:8px" onclick="submitRecipeUse(true)">
                    🗑️ ${t('recipes.use_all_btn')}
                </button>
            </div>
        `;
        document.getElementById('modal-overlay').style.display = 'flex';
        
    } catch (err) {
        console.error('useRecipeIngredient error:', err);
        showToast(t('recipes.load_error'), 'error');
    }
}

function selectRecipeUseLoc(btn, loc) {
    btn.parentElement.querySelectorAll('.loc-btn').forEach(b => b.classList.remove('active'));
    btn.classList.add('active');
    document.getElementById('ruse-location').value = loc;
}

function switchRecipeUseUnit(mode) {
    if (!_recipeUseConfMode) return;
    const subBtn = document.getElementById('ruse-unit-sub');
    const confBtn = document.getElementById('ruse-unit-conf');
    const qtyInput = document.getElementById('ruse-quantity');
    const hint = document.getElementById('ruse-hint');
    
    if (mode === 'sub') {
        subBtn.classList.add('active');
        confBtn.classList.remove('active');
        _recipeUseConfMode._activeUnit = 'sub';
        const step = getSubUnitStep(_recipeUseConfMode.packageUnit);
        qtyInput.value = _recipeUseContext.qtyNumber || step;
        qtyInput.step = step;
        qtyInput.min = step;
        hint.textContent = t('recipes.quantity_in_total').replace('{unit}', _recipeUseConfMode.subLabel).replace('{total}', Math.round(_recipeUseConfMode.totalSub) + _recipeUseConfMode.subLabel);
    } else {
        confBtn.classList.add('active');
        subBtn.classList.remove('active');
        _recipeUseConfMode._activeUnit = 'conf';
        qtyInput.value = 1;
        qtyInput.step = 0.5;
        qtyInput.min = 0.5;
        hint.textContent = t('recipes.packs_of_have').replace('{size}', `${_recipeUseConfMode.packageSize}${_recipeUseConfMode.subLabel}`).replace('{count}', _recipeUseConfMode.totalConf.toFixed(1));
    }
}

function adjustRecipeUseQty(direction) {
    const input = document.getElementById('ruse-quantity');
    let val = parseFloat(input.value) || 0;
    let step;
    if (_recipeUseConfMode && _recipeUseConfMode._activeUnit === 'sub') {
        step = getSubUnitStep(_recipeUseConfMode.packageUnit);
    } else if (_recipeUseConfMode && _recipeUseConfMode._activeUnit === 'conf') {
        step = 0.5;
    } else {
        const u = _recipeUseNormalUnit || 'pz';
        if (u === 'g' || u === 'ml') {
            step = val < 50 ? 1 : (val < 500 ? 10 : 50);
        } else {
            step = 1;
        }
    }
    val = Math.max(step, val + direction * step);
    input.value = Math.round(val * 1000) / 1000;
}

async function submitRecipeUse(useAll) {
    if (!_recipeUseContext) return;
    const { idx, productId, btn } = _recipeUseContext;
    const location = document.getElementById('ruse-location').value;
    
    let qty;
    if (useAll) {
        qty = 0; // API handles use_all
    } else {
        qty = parseFloat(document.getElementById('ruse-quantity').value) || 1;
        if (_recipeUseConfMode && _recipeUseConfMode._activeUnit === 'sub') {
            qty = qty / _recipeUseConfMode.packageSize;
        }
    }
    
    closeModal();
    btn.disabled = true;
    btn.textContent = '⏳...';
    
    try {
        const recipeTitle = _cachedRecipe?.recipe?.title || '';
        const result = await api('inventory_use', {}, 'POST', {
            product_id: productId,
            quantity: qty,
            use_all: useAll,
            location: location,
            notes: recipeTitle ? `Ricetta: ${recipeTitle}` : '',
        });
        
        if (result.success) {
            const li = document.getElementById(`recipe-ing-${idx}`);
            if (li) li.classList.add('recipe-ing-used');
            btn.textContent = t('cooking.ingredient_used');
            btn.classList.add('btn-used');
            
            if (_cachedRecipe && _cachedRecipe.recipe && _cachedRecipe.recipe.ingredients && _cachedRecipe.recipe.ingredients[idx]) {
                _cachedRecipe.recipe.ingredients[idx].used = true;
                // Persist used state to DB
                saveRecipeToArchive(_cachedRecipe.recipe);
            }
            
            showToast(t('recipes.ingredient_scaled_toast'), 'success');
            if (result.added_to_bring) {
                setTimeout(() => showToast(t('recipes.finished_added_bring_toast'), 'info'), 1500);
            }
            
            // Check low stock → Bring! prompt, then offer move
            const moveCallback = result.remaining > 0
                ? () => setTimeout(() => {
                    const ingData = _cachedRecipe?.recipe?.ingredients?.[_recipeUseContext?.idx];
                    const wasVacuum = !!(ingData?.vacuum_sealed);
                    showRecipeMoveModal(productId, location, result.remaining, result.opened_id, wasVacuum);
                  }, 300)
                : null;
            setTimeout(() => showLowStockBringPrompt(result, moveCallback), 300);
        } else {
            btn.disabled = false;
            btn.textContent = t('cooking.ingredient_use_btn');
            showToast(result.error || t('error.generic'), 'error');
        }
    } catch (err) {
        console.error('Recipe use error:', err);
        btn.disabled = false;
        btn.textContent = t('cooking.ingredient_use_btn');
        showToast(t('error.connection'), 'error');
    }
    _recipeUseContext = null;
}

function showRecipeMoveModal(productId, fromLoc, remaining, openedId, wasVacuum) {
    const otherLocs = Object.entries(LOCATIONS).filter(([k]) => k !== fromLoc);
    const locButtons = otherLocs.map(([k, v]) =>
        `<button type="button" class="loc-btn" onclick="clearMoveModalTimer();confirmRecipeMove(${productId}, '${fromLoc}', '${k}', ${openedId || 0})">${v.icon} ${v.label}</button>`
    ).join('');
    const vacuumRow = wasVacuum ? `
        <label style="display:flex;align-items:center;gap:8px;margin-top:12px;cursor:pointer">
            <input type="checkbox" id="move-vacuum-check" checked>
            <span>${t('move.vacuum_restore')}</span>
        </label>` : '';
    document.getElementById('modal-content').innerHTML = `
        <div class="modal-header">
            <h3>${t('move.title')}</h3>
            <button class="modal-close" onclick="clearMoveModalTimer();closeModal()">✕</button>
        </div>
        <div style="padding:0 16px 16px">
            <p style="margin-bottom:12px">${t('move.question_short').replace('{thing}', openedId ? t('move.thing_opened') : t('move.thing_rest'))}</p>
            <div class="location-selector">${locButtons}</div>
            ${vacuumRow}
            <button type="button" id="btn-move-stay" class="btn btn-secondary full-width move-countdown-btn" style="margin-top:12px" onclick="clearMoveModalTimer();closeModal()">No, resta in ${LOCATIONS[fromLoc]?.label || fromLoc}</button>
        </div>
    `;
    document.getElementById('modal-overlay').style.display = 'flex';
    startMoveModalCountdown('btn-move-stay', () => { closeModal(); });
}

async function confirmRecipeMove(productId, fromLoc, toLoc, openedId) {
    clearMoveModalTimer();
    const newVacuum = document.getElementById('move-vacuum-check')?.checked ? 1 : 0;
    closeModal();
    try {
        if (openedId) {
            let days = estimateExpiryDays({ name: '', category: '' }, toLoc);
            if (newVacuum) days = getVacuumExpiryDays(days);
            await api('inventory_update', {}, 'POST', {
                id: openedId,
                location: toLoc,
                expiry_date: addDays(days),
                product_id: productId,
                vacuum_sealed: newVacuum,
            });
        } else {
            const data = await api('inventory_list');
            const item = (data.inventory || []).find(i => i.product_id == productId && i.location === fromLoc && parseFloat(i.quantity) > 0);
            if (item) {
                let days = estimateExpiryDays({ name: item.name || '', category: item.category || '' }, toLoc);
                if (newVacuum) days = getVacuumExpiryDays(days);
                await api('inventory_update', {}, 'POST', {
                    id: item.id,
                    location: toLoc,
                    expiry_date: addDays(days),
                    product_id: productId,
                    vacuum_sealed: newVacuum,
                });
            }
        }
        showToast(`📦 Spostato in ${LOCATIONS[toLoc]?.label || toLoc}`, 'success');
    } catch (e) {
        console.error('Recipe move error:', e);
    }
}

function renderRecipe(r) {
    let html = `<h2>${r.title}</h2>`;

    // Meta tags
    html += '<div class="recipe-meta">';
    html += `<span class="recipe-tag">${_mealLabel(r.meal)}</span>`;
    html += `<span class="recipe-tag">👥 ${r.persons} ${t('recipes.persons_short')}</span>`;
    if (r.prep_time) html += `<span class="recipe-tag">🔪 ${r.prep_time}</span>`;
    if (r.cook_time) html += `<span class="recipe-tag">🔥 ${r.cook_time}</span>`;
    if (r.tags) r.tags.forEach(t => { html += `<span class="recipe-tag">${t}</span>`; });
    html += '</div>';

    // Expiry note
    if (r.expiry_note) {
        html += `<div class="recipe-expiry-note">⚠️ ${r.expiry_note}</div>`;
    }

    // Ingredients
    html += `<h3>${t('recipes.ingredients_title')}</h3><ul class="recipe-ingredients">`;
    (r.ingredients || []).forEach((ing, idx) => {
        if (ing.from_pantry && ing.product_id) {
            const qtyNum = ing.qty_number || 0;
            const loc = (ing.location || 'dispensa').replace(/'/g, "\\'");
            const alreadyUsed = ing.used === true;
            html += `<li class="recipe-ingredient${alreadyUsed ? ' recipe-ing-used' : ''}" id="recipe-ing-${idx}">`;
            html += `<span class="recipe-ing-text"><strong>${ing.name}</strong>${ing.brand ? ' <em>(' + ing.brand + ')</em>' : ''}: ${ing.qty} ✅`;
            // Detail line: location + expiry
            let details = [];
            const ingredientLocLabels = Object.fromEntries(Object.entries(LOCATIONS).map(([k,v]) => [k, `${v.icon} ${v.label}`]));
            details.push(ingredientLocLabels[ing.location] || ('📍 ' + ing.location));
            if (ing.expiry_date) {
                const exp = new Date(ing.expiry_date);
                const now = new Date(); now.setHours(0,0,0,0);
                const diffDays = Math.round((exp - now) / 86400000);
                if (diffDays < 0) details.push(t('expiry.badge_expired_ago').replace('{n}', Math.abs(diffDays)));
                else if (diffDays <= 3) details.push(t('expiry.badge_expires_red').replace('{n}', diffDays));
                else if (diffDays <= 7) details.push(t('expiry.badge_expires_yellow').replace('{n}', diffDays));
                else details.push('📅 ' + exp.toLocaleDateString(_currentLang === 'de' ? 'de-DE' : _currentLang === 'en' ? 'en-GB' : 'it-IT'));
            }
            if (details.length) html += `<br><small class="recipe-ing-detail">${details.join(' · ')}</small>`;
            html += `</span>`;
            if (alreadyUsed) {
                html += `<button class="btn-use-ingredient btn-used" disabled>${t('cooking.ingredient_used')}</button>`;
            } else {
                html += `<button class="btn-use-ingredient" onclick="useRecipeIngredient(${idx}, ${ing.product_id}, '${loc}', ${qtyNum}, this, '${(ing.qty || '').replace(/'/g, "&apos;")}')" title="${t('cooking.ingredient_deduct_title')}">${t('cooking.ingredient_use_btn')}</button>`;
            }
            html += `</li>`;
        } else {
            const pantryIcon = ing.from_pantry ? ' ✅' : ' 🛒';
            html += `<li class="recipe-ingredient"><span class="recipe-ing-text"><strong>${ing.name}</strong>: ${ing.qty}${pantryIcon}</span></li>`;
        }
    });
    html += '</ul>';

    // Steps
    html += `<h3>${t('recipes.steps_title')}</h3><ol>`;
    (r.steps || []).forEach(step => {
        const cleanStep = step.replace(/^Passo\s*\d+\s*:\s*/i, '');
        html += `<li>${cleanStep}</li>`;
    });
    html += '</ol>';

    // Nutrition note
    if (r.nutrition_note) {
        html += `<p style="color:var(--text-muted);font-size:0.85rem;margin-top:12px">💡 ${r.nutrition_note}</p>`;
    }

    document.getElementById('recipe-content').innerHTML = html;
}

// ===== COOKING MODE =====
let _cookingRecipe = null;
let _cookingStep = 0;
let _cookingTTS = true;
let _cookingVisited = new Set(); // indices of steps already seen

function startCookingMode() {
    const recipe = _cachedRecipe && _cachedRecipe.recipe ? _cachedRecipe.recipe : null;
    if (!recipe || !(recipe.steps || []).length) {
        showToast(t('recipes.no_steps'), 'info');
        return;
    }
    // Resume if same recipe; otherwise start fresh
    const isSame = _cookingRecipe && _cookingRecipe.title === recipe.title;
    if (!isSame) {
        _cookingRecipe = JSON.parse(JSON.stringify(recipe));
        _cookingStep = 0;
        _cookingVisited = new Set();
        clearAllCookingTimers();
    }
    _cookingTTS = true;
    document.getElementById('cooking-title').textContent = _cookingRecipe.title || '';
    document.getElementById('cooking-tts-btn').textContent = '🔊';
    document.getElementById('cooking-overlay').style.display = 'flex';
    document.body.classList.add('cooking-mode-active');
    try { screen.orientation?.lock('portrait').catch(() => {}); } catch (_) { /* ignore */ }
    renderCookingStep();
    if (_cookingTTS) {
        const text = ((_cookingRecipe.steps || [])[_cookingStep] || '').replace(/^Passo\s*\d+\s*[:.]\s*/i, '');
        speakCookingStep(text);
    }
}
function closeCookingMode() {
    document.getElementById('cooking-overlay').style.display = 'none';
    document.body.classList.remove('cooking-mode-active');
    // NOTE: intentionally keep _cookingRecipe, _cookingStep, _cookingVisited
    // so the user can resume from the same step when they reopen
    try { screen.orientation?.unlock().catch(() => {}); } catch (_) { /* ignore */ }
}

function restartCookingMode() {
    _cookingStep = 0;
    _cookingVisited = new Set();
    clearAllCookingTimers();
    renderCookingStep();
}

function renderCookingStep() {
    if (!_cookingRecipe) return;
    const steps = _cookingRecipe.steps || [];
    const step = steps[_cookingStep] || '';
    const cleanStep = step.replace(/^Passo\s*\d+\s*[:.]\s*/i, '');
    const total = steps.length;

    // Mark current step as visited
    _cookingVisited.add(_cookingStep);

    document.getElementById('cooking-step-num').textContent = `${_cookingStep + 1} / ${total}`;
    document.getElementById('cooking-step-text').textContent = cleanStep;

    // Progress dots
    const dotsEl = document.getElementById('cooking-progress-dots');
    if (dotsEl) {
        dotsEl.innerHTML = Array.from({ length: total }, (_, i) => {
            let cls = 'cprog-dot';
            if (i === _cookingStep) cls += ' current';
            else if (_cookingVisited.has(i)) cls += ' visited';
            return `<span class="${cls}"></span>`;
        }).join('');
    }

    // Show ALL unused from_pantry ingredients (not filtered by step text).
    // The AI often uses pronouns ("tagliarla", "aggiungile") instead of the ingredient
    // name, so text-matching would miss them. Better to always show what's available.
    const ings = (_cookingRecipe.ingredients || [])
        .map((ing, idx) => ({ ...ing, _idx: idx }))
        .filter(ing => ing.from_pantry && ing.product_id && ing.used !== true);

    const ingsEl = document.getElementById('cooking-step-ings');
    if (ings.length > 0) {
        const cookingLocLabels = Object.fromEntries(Object.entries(LOCATIONS).map(([k,v]) => [k, `${v.icon} ${v.label}`]));
        ingsEl.innerHTML = ings.map(ing => {
            const loc = (ing.location || 'dispensa').replace(/'/g, "\\'");
            const qtyNum = ing.qty_number || 0;
            // Build info chips: brand, location, expiry
            const chips = [];
            if (ing.brand) chips.push(`<span class="cooking-ing-chip">${escapeHtml(ing.brand)}</span>`);
            const locLabel = cookingLocLabels[ing.location] || (ing.location ? `📍 ${ing.location}` : `${LOCATIONS.dispensa.icon} ${LOCATIONS.dispensa.label}`);
            chips.push(`<span class="cooking-ing-chip">${locLabel}</span>`);
            if (ing.expiry_date) {
                const daysLeft = Math.round((new Date(ing.expiry_date) - new Date()) / 86400000);
                const expClass = daysLeft <= 3 ? 'exp-soon' : daysLeft <= 7 ? 'exp-close' : '';
                chips.push(`<span class="cooking-ing-chip ${expClass}">📅 ${t('cooking.expires_chip').replace('{date}', formatDate(ing.expiry_date))}</span>`);
            }
            return `<div class="cooking-ing-row">
                <div style="flex:1;min-width:0">
                    <span class="cooking-ing-name">📦 <strong>${escapeHtml(ing.name)}</strong>: ${escapeHtml(ing.qty)}</span>
                    <div class="cooking-ing-meta">${chips.join('')}</div>
                </div>
                <button class="cooking-use-btn" onclick="cookingUseIngredient(${ing._idx}, ${ing.product_id}, '${loc}', ${qtyNum}, this)">${t('cooking.ingredient_use_btn')}</button>
            </div>`;
        }).join('');
        ingsEl.style.display = 'flex';
    } else {
        ingsEl.innerHTML = '';
        ingsEl.style.display = 'none';
    }

    // Navigation button states
    const prevBtn = document.getElementById('cooking-prev');
    const nextBtn = document.getElementById('cooking-next');
    prevBtn.disabled = _cookingStep === 0;
    nextBtn.textContent = _cookingStep === total - 1 ? t('cooking.finish') : t('cooking.next');

    // Timer: detect duration in step text and show suggestion
    setupCookingTimerSuggestion(cleanStep);

    // TTS: auto-speak is handled by navigateCookingStep() and startCookingMode() callers.
    // Use replayCookingTTS() to re-read the current step manually ("Rileggi" button).
}

function _buildTtsRequest(text, s) {
    const url = s.tts_url || '';
    const method = s.tts_method || 'POST';
    const authType = s.tts_auth_type || 'bearer';
    const token = s.tts_token || '';
    const payloadKey = s.tts_payload_key || 'message';
    const contentType = s.tts_content_type || 'application/json';
    let extraFields = {};
    try { extraFields = JSON.parse(s.tts_extra_fields || '{}'); } catch(e) { /* invalid JSON, ignore */ }
    const headers = { 'Content-Type': contentType };
    if (authType === 'bearer' && token) {
        headers['Authorization'] = `Bearer ${token}`;
    } else if (authType === 'header' && s.tts_auth_header_name) {
        headers[s.tts_auth_header_name] = s.tts_auth_header_value || '';
    }
    const payload = { [payloadKey]: text, ...extraFields };
    let body;
    if (contentType === 'application/json') {
        body = JSON.stringify(payload);
    } else if (contentType === 'application/x-www-form-urlencoded') {
        body = new URLSearchParams(Object.entries(payload).map(([k, v]) => [k, String(v)])).toString();
    } else {
        body = text;
    }
    return { url, method, headers, body };
}

async function _ttsViaProxy(req) {
    // Route through server-side proxy to avoid mixed-content / CORS issues
    return fetch('api/index.php?action=tts_proxy', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
            url: req.url,
            method: req.method,
            headers: req.headers,
            payload: req.body
        })
    });
}

async function speakCookingStep(text) {
    if (!text) return;
    const s = getSettings();
    // Use custom TTS endpoint only when explicitly configured; otherwise always use browser TTS.
    // Do NOT gate on s.tts_enabled — the _cookingTTS toggle in cooking mode is the only gate.
    try {
        if (s.tts_engine === 'custom' && s.tts_url) {
            const req = _buildTtsRequest(text, s);
            await _ttsViaProxy(req);
        } else {
            _speakBrowser(text);
        }
    } catch(e) { /* silent — TTS is non-critical */ }
}

function replayCookingTTS() {
    if (!_cookingRecipe) return;
    const steps = _cookingRecipe.steps || [];
    const text = (steps[_cookingStep] || '').replace(/^Passo\s*\d+\s*[:.]\s*/i, '');
    if (text) speakCookingStep(text);
}

function onTtsAuthTypeChange(type) {
    const tokenGroup = document.getElementById('tts-token-group');
    const headerGroup = document.getElementById('tts-custom-header-group');
    if (tokenGroup) tokenGroup.style.display = type === 'bearer' ? '' : 'none';
    if (headerGroup) headerGroup.style.display = type === 'header' ? '' : 'none';
}

function onTtsEngineChange(engine) {
    const browserSect = document.getElementById('tts-browser-section');
    const serverSect = document.getElementById('tts-server-section');
    if (browserSect) browserSect.style.display = engine === 'browser' ? '' : 'none';
    if (serverSect) serverSect.style.display = engine === 'server' ? '' : 'none';
}

/** Populate voice selector from Web Speech API. Called on settings load and on voiceschanged. */
function _initBrowserTtsVoices(selectedVoice) {
    const sel = document.getElementById('setting-tts-voice');
    if (!sel) return;

    // Inside the EverShelf Kiosk Android app the native TTS bridge handles
    // speech — no Web Speech API voice list needed.
    if (typeof _kioskBridge !== 'undefined' && typeof _kioskBridge.speak === 'function') {
        sel.innerHTML = '<option value="">— Voce nativa Android (kiosk) —</option>';
        return;
    }

    if (!window.speechSynthesis) {
        sel.innerHTML = '<option value="">— Voce non supportata dal browser —</option>';
        return;
    }

    // Reset to loading state each time (settings page may be re-opened)
    sel.innerHTML = '<option value="">— Caricamento voci… —</option>';

    const populate = () => {
        const voices = window.speechSynthesis.getVoices();
        if (!voices.length) return false;
        // Italian voices first, then others
        const it = voices.filter(v => v.lang.startsWith('it'));
        const others = voices.filter(v => !v.lang.startsWith('it'));
        const sorted = [...it, ...others];
        sel.innerHTML = sorted.map(v =>
            `<option value="${v.name}" ${v.name === selectedVoice ? 'selected' : ''}>${v.name} (${v.lang})${v.localService ? '' : ' ☁️'}</option>`
        ).join('');
        // Auto-select first Italian voice if no preference set
        if (!selectedVoice) {
            const paola = sorted.find(v => v.name === 'Paola');
            const firstIt = sorted.find(v => v.lang.startsWith('it'));
            if (paola) sel.value = paola.name;
            else if (firstIt) sel.value = firstIt.name;
        }
        return true;
    };

    // Try immediately (voices already cached from previous call)
    if (populate()) return;

    // onvoiceschanged fires in Firefox / some Chrome versions
    window.speechSynthesis.onvoiceschanged = () => { populate(); };

    // Polling fallback: Chrome/WebView loads voices async (up to ~3s on desktop, longer on Android)
    let tries = 0;
    const interval = setInterval(() => {
        tries++;
        if (populate()) {
            clearInterval(interval);
        } else if (tries >= 50) { // 50 × 200ms = 10s
            clearInterval(interval);
            if (!window.speechSynthesis.getVoices().length) {
                sel.innerHTML = '<option value="">— Nessuna voce disponibile su questo dispositivo —</option>';
            }
        }
    }, 200);
}

/** Speak text using the browser Web Speech API (offline).
 *  When running inside the EverShelf Kiosk Android app the native TTS bridge
 *  is preferred — it bypasses Web Speech API voice limitations on Android. */
function _speakBrowser(text) {
    const s = getSettings();
    const rate  = parseFloat(s.tts_rate)  || 1;
    const pitch = parseFloat(s.tts_pitch) || 1;

    // ── Native Android TTS bridge (kiosk WebView) ──────────────────────
    if (typeof _kioskBridge !== 'undefined' && typeof _kioskBridge.speak === 'function') {
        try { _kioskBridge.speak(text, rate, pitch); } catch(_e) { /* silent */ }
        return;
    }

    // ── Web Speech API (desktop / mobile browser) ──────────────────────
    if (!window.speechSynthesis) return;
    window.speechSynthesis.cancel();
    const utt = new SpeechSynthesisUtterance(text);
    utt.rate  = rate;
    utt.pitch = pitch;
    const voices = window.speechSynthesis.getVoices();
    const preferred = voices.find(v => v.name === s.tts_voice);
    if (preferred) {
        utt.voice = preferred;
        utt.lang  = preferred.lang;
    } else {
        utt.lang = _currentLang === 'de' ? 'de-DE' : _currentLang === 'en' ? 'en-US' : 'it-IT';
    }
    window.speechSynthesis.speak(utt);
}

async function testTTS() {
    const statusEl = document.getElementById('tts-test-status');
    const enabled = document.getElementById('setting-tts-enabled')?.checked;
    if (!enabled) {
        if (statusEl) { statusEl.style.display = 'block'; statusEl.className = 'settings-status error'; statusEl.textContent = '⚠️ TTS non attivo — attiva il toggle prima di testare.'; }
        return;
    }
    const engine = document.getElementById('setting-tts-engine')?.value || 'browser';
    if (engine === 'browser') {
        // Kiosk native TTS bridge takes priority over Web Speech API
        if (typeof _kioskBridge !== 'undefined' && typeof _kioskBridge.speak === 'function') {
            const s = getSettings();
            s.tts_rate  = parseFloat(document.getElementById('setting-tts-rate')?.value)  || 1;
            s.tts_pitch = parseFloat(document.getElementById('setting-tts-pitch')?.value) || 1;
            saveSettingsToStorage(s);
            _speakBrowser('Test vocale EverShelf. La sintesi vocale funziona correttamente.');
            if (statusEl) { statusEl.style.display = 'block'; statusEl.className = 'settings-status success'; statusEl.textContent = '✅ Riproduzione in corso — controlla l\'audio del dispositivo.'; }
            return;
        }
        if (!window.speechSynthesis) {
            if (statusEl) { statusEl.style.display = 'block'; statusEl.className = 'settings-status error'; statusEl.textContent = '❌ Web Speech API non supportata da questo browser.'; }
            return;
        }
        // Temporarily apply form values for the test
        const s = getSettings();
        const voiceName = document.getElementById('setting-tts-voice')?.value;
        s.tts_voice = voiceName || s.tts_voice;
        s.tts_rate = parseFloat(document.getElementById('setting-tts-rate')?.value) || 1;
        s.tts_pitch = parseFloat(document.getElementById('setting-tts-pitch')?.value) || 1;
        saveSettingsToStorage(s);
        _speakBrowser('Test vocale EverShelf. La sintesi vocale funziona correttamente.');
        if (statusEl) { statusEl.style.display = 'block'; statusEl.className = 'settings-status success'; statusEl.textContent = '✅ Riproduzione in corso — controlla l\'audio del dispositivo.'; }
        return;
    }
    // Server engine
    let extraFields = {};
    try { extraFields = JSON.parse((document.getElementById('setting-tts-extra-fields')?.value || '{}').trim() || '{}'); } catch(e) { /* ignore */ }
    const formSettings = {
        tts_url: (document.getElementById('setting-tts-url')?.value || '').trim(),
        tts_method: document.getElementById('setting-tts-method')?.value || 'POST',
        tts_auth_type: document.getElementById('setting-tts-auth-type')?.value || 'bearer',
        tts_token: (document.getElementById('setting-tts-token')?.value || '').trim(),
        tts_auth_header_name: (document.getElementById('setting-tts-auth-header-name')?.value || '').trim(),
        tts_auth_header_value: (document.getElementById('setting-tts-auth-header-value')?.value || '').trim(),
        tts_content_type: document.getElementById('setting-tts-content-type')?.value || 'application/json',
        tts_payload_key: (document.getElementById('setting-tts-payload-key')?.value || '').trim() || 'message',
        tts_extra_fields: document.getElementById('setting-tts-extra-fields')?.value || ''
    };
    if (!formSettings.tts_url) {
        if (statusEl) { statusEl.style.display = 'block'; statusEl.className = 'settings-status error'; statusEl.textContent = '⚠️ URL endpoint mancante.'; }
        return;
    }
    if (statusEl) { statusEl.style.display = 'block'; statusEl.className = 'settings-status'; statusEl.textContent = '⏳ Invio in corso…'; }
    try {
        const req = _buildTtsRequest('Test vocale EverShelf', formSettings);
        const res = await _ttsViaProxy(req);
        const data = await res.json().catch(() => ({}));
        const httpCode = data.status || res.status;
        if (res.ok && httpCode >= 200 && httpCode < 300) {
            if (statusEl) { statusEl.className = 'settings-status success'; statusEl.textContent = `✅ Risposta ${httpCode} — controlla che l'altoparlante abbia parlato.`; }
        } else {
            const errDetail = data.error || data.body || res.statusText;
            if (statusEl) { statusEl.className = 'settings-status error'; statusEl.textContent = `⚠️ HTTP ${httpCode}: ${errDetail}`; }
        }
    } catch(e) {
        if (statusEl) { statusEl.className = 'settings-status error'; statusEl.textContent = `❌ Errore: ${e.message}`; }
    }
}

// ===== COOKING TIMER SYSTEM =====
let _cookingTimers = [];          // { id, label, total, seconds, running, interval }
let _cookingTimerIdCounter = 0;
let _cookingSuggestedSeconds = 0;
let _cookingSuggestedLabel = '';

/**
 * Parse time durations from step text.
 * Returns total seconds or 0 if no time found.
 */
function _parseStepTimer(text) {
    const t = text.toLowerCase();
    let totalSec = 0;
    if (/mezz['']?\s*ora/i.test(t)) totalSec += 30 * 60;
    if (/un\s+quarto\s+d['']?\s*ora/i.test(t)) totalSec += 15 * 60;
    if (/un['']?\s*ora(?!\s*e)/i.test(t) && !/\d\s*or[ae]/i.test(t)) totalSec += 60 * 60;
    if (totalSec > 0) return totalSec;
    const reOre = /(\d+(?:[.,]\d+)?)\s*or[ae]/gi;
    const reMin = /(\d+(?:[.,]\d+)?)\s*min(?:ut[oi])?/gi;
    const reSec = /(\d+(?:[.,]\d+)?)\s*second[oi]/gi;
    let m;
    while ((m = reOre.exec(t)) !== null) totalSec += parseFloat(m[1].replace(',', '.')) * 3600;
    while ((m = reMin.exec(t)) !== null) totalSec += parseFloat(m[1].replace(',', '.')) * 60;
    while ((m = reSec.exec(t)) !== null) totalSec += parseFloat(m[1].replace(',', '.'));
    if (totalSec === 0 && /(?:un\s+paio\s+di|qualche|pochi)\s+minut/i.test(t)) totalSec = 2 * 60;
    if (totalSec === 0 && /qualche\s+second/i.test(t)) totalSec = 15;
    return Math.round(totalSec);
}

function _formatTimerDisplay(sec) {
    const abs = Math.abs(sec);
    const m = Math.floor(abs / 60);
    const s = abs % 60;
    const sign = sec < 0 ? '+' : '';
    return `${sign}${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
}

/** Extract a short 2-3 word label from the step text for the timer. */
function _extractTimerLabel(text, stepNum) {
    const fillers = new Set(['il','la','lo','le','gli','i','dell','della','dello','delle','degli','dei',
        'un','una','uno','del','al','alla','allo','alle','agli','ai','nel','nella','nello','nelle',
        'negli','nei','per','con','che','poi','e','o','non','se','in','di','a','da','fino','mentre',
        'quando','dopo','prima','circa','bene','ancora','subito','su','ad','ed','più','meno','tutto','tutta']);
    const timePatterns = [/mezz['']?\s*ora/i, /\bor[ae]\b/i, /\bmin(?:ut[oi])?\b/i, /\bsecond[oi]\b/i, /\bquarto\s+d['']?\s*ora/i];
    let timeIdx = text.length;
    for (const p of timePatterns) { const r = p.exec(text); if (r && r.index < timeIdx) timeIdx = r.index; }
    const beforeTime = (text.slice(0, timeIdx).trim() || text);
    const words = beforeTime.replace(/[.,!?;:'"()\[\]]/g, '').split(/\s+/).filter(w => w.length > 2 && !/^\d+$/.test(w));
    const meaningful = words.filter(w => !fillers.has(w.toLowerCase()));
    if (meaningful.length >= 1) return meaningful.slice(0, 3).join(' ');
    return `Passo ${stepNum + 1}`;
}

function setupCookingTimerSuggestion(stepText) {
    const seconds = _parseStepTimer(stepText);
    const suggestEl = document.getElementById('cooking-timer-suggest');
    if (seconds <= 0) {
        suggestEl.style.display = 'none';
        _cookingSuggestedSeconds = 0;
        _cookingSuggestedLabel = '';
        return;
    }
    _cookingSuggestedSeconds = seconds;
    _cookingSuggestedLabel = _extractTimerLabel(stepText, _cookingStep);
    document.getElementById('cooking-timer-suggest-text').textContent =
        `⏱️ ${_formatTimerDisplay(seconds)}  ·  ${_cookingSuggestedLabel}`;
    suggestEl.style.display = 'flex';
}

function addSuggestedCookingTimer() {
    if (_cookingSuggestedSeconds <= 0) return;
    addCookingTimer(_cookingSuggestedSeconds, _cookingSuggestedLabel);
    document.getElementById('cooking-timer-suggest').style.display = 'none';
    _cookingSuggestedSeconds = 0;
}

function addCookingTimer(seconds, label) {
    const id = ++_cookingTimerIdCounter;
    _cookingTimers.push({ id, label, total: seconds, seconds, running: false, interval: null });
    renderTimersBar();
    toggleCookingTimerById(id); // auto-start
}

function removeCookingTimer(id) {
    const t = _cookingTimers.find(t => t.id === id);
    if (t && t.interval) clearInterval(t.interval);
    _cookingTimers = _cookingTimers.filter(t => t.id !== id);
    renderTimersBar();
    _updateScreenFlash();
}

function toggleCookingTimerById(id) {
    const t = _cookingTimers.find(t => t.id === id);
    if (!t) return;
    if (t.running) {
        clearInterval(t.interval);
        t.interval = null;
        t.running = false;
    } else {
        t.running = true;
        t.interval = setInterval(() => {
            t.seconds--;
            if (t.seconds === 10 && _cookingTTS) {
                speakCookingStep(t('cooking.timer_warning_tts').replace('{label}', t.label));
            }
            if (t.seconds === 0) _cookingTimerDoneById(id);
            _updateTimerCard(id);
        }, 1000);
    }
    _updateTimerCard(id);
}

function resetCookingTimerById(id) {
    const t = _cookingTimers.find(t => t.id === id);
    if (!t) return;
    clearInterval(t.interval);
    t.interval = null;
    t.running = false;
    t.seconds = t.total;
    _updateTimerCard(id);
}

function _cookingTimerDoneById(id) {
    if (navigator.vibrate) navigator.vibrate([300, 100, 300, 100, 300]);
    const timer = _cookingTimers.find(ti => ti.id === id);
    if (_cookingTTS && timer) speakCookingStep(t('cooking.timer_expired_tts').replace('{label}', timer.label));
}

function _updateTimerCard(id) {
    const t = _cookingTimers.find(t => t.id === id);
    if (!t) return;
    const card = document.getElementById(`ctimer-${id}`);
    if (!card) { renderTimersBar(); return; }
    const sec = t.seconds;
    const dispEl = card.querySelector('.ctimer-display');
    const toggleBtn = card.querySelector('.ctimer-toggle');
    dispEl.textContent = _formatTimerDisplay(sec);
    if (sec <= 0) {
        dispEl.className = 'ctimer-display ctimer-done';
    } else if (sec <= 30) {
        dispEl.className = 'ctimer-display ctimer-warning';
    } else {
        dispEl.className = 'ctimer-display';
    }
    toggleBtn.textContent = t.running ? '⏸' : '▶';
    toggleBtn.classList.toggle('running', t.running);
    _updateScreenFlash();
}

/** Update the full-screen colour flash based on the worst active timer state. */
function _updateScreenFlash() {
    const flashEl = document.getElementById('cooking-flash-overlay');
    if (!flashEl) return;
    let hasDone = false, hasWarning = false;
    for (const t of _cookingTimers) {
        if (t.seconds <= 0) { hasDone = true; break; }
        if (t.seconds <= 30 && t.running) hasWarning = true;
    }
    if (hasDone) {
        flashEl.className = 'cooking-flash-overlay flash-done';
    } else if (hasWarning) {
        flashEl.className = 'cooking-flash-overlay flash-warning';
    } else {
        flashEl.className = 'cooking-flash-overlay';
    }
}

function renderTimersBar() {
    const bar = document.getElementById('cooking-timers-bar');
    if (!bar) return;
    if (_cookingTimers.length === 0) {
        bar.style.display = 'none';
        bar.innerHTML = '';
        return;
    }
    bar.style.display = 'flex';
    bar.innerHTML = _cookingTimers.map(t => {
        const sec = t.seconds;
        const doneClass = sec <= 0 ? ' ctimer-done' : sec <= 30 ? ' ctimer-warning' : '';
        const runClass = t.running ? ' running' : '';
        return `<div class="cooking-timer-card" id="ctimer-${t.id}">
            <span class="ctimer-label">${escapeHtml(t.label)}</span>
            <span class="ctimer-display${doneClass}">${_formatTimerDisplay(sec)}</span>
            <div class="ctimer-btns">
                <button class="ctimer-btn ctimer-toggle${runClass}" onclick="toggleCookingTimerById(${t.id})">${t.running ? '⏸' : '▶'}</button>
                <button class="ctimer-btn ctimer-reset" onclick="resetCookingTimerById(${t.id})">↩</button>
                <button class="ctimer-btn ctimer-remove" onclick="removeCookingTimer(${t.id})">✕</button>
            </div>
        </div>`;
    }).join('');
}

function clearAllCookingTimers() {
    _cookingTimers.forEach(t => { if (t.interval) clearInterval(t.interval); });
    _cookingTimers = [];
    _cookingTimerIdCounter = 0;
    _cookingSuggestedSeconds = 0;
    _cookingSuggestedLabel = '';
    const bar = document.getElementById('cooking-timers-bar');
    if (bar) { bar.style.display = 'none'; bar.innerHTML = ''; }
    _updateScreenFlash();
}
// ===== END COOKING TIMER SYSTEM =====

function toggleCookingTTS() {
    _cookingTTS = !_cookingTTS;
    const btn = document.getElementById('cooking-tts-btn');
    btn.textContent = _cookingTTS ? '🔊' : '🔇';
    if (_cookingTTS) {
        const steps = _cookingRecipe?.steps || [];
        const text = (steps[_cookingStep] || '').replace(/^Passo\s*\d+\s*[:.]\s*/i, '');
        speakCookingStep(text);
    }
}

function navigateCookingStep(delta) {
    if (!_cookingRecipe) return;
    const total = (_cookingRecipe.steps || []).length;
    const next = _cookingStep + delta;
    if (next < 0) return;
    if (next >= total) {
        // All steps done: mark all visited, announce completion, then close overlay
        for (let i = 0; i < total; i++) _cookingVisited.add(i);
        if (_cookingTTS) {
            const doneText = t('cooking.recipe_done_tts').replace('{title}', _cookingRecipe.title || '');
            speakCookingStep(doneText);
        }
        closeCookingMode();
        return;
    }
    _cookingStep = next;
    renderCookingStep();
    if (_cookingTTS) {
        const text = ((_cookingRecipe.steps || [])[_cookingStep] || '').replace(/^Passo\s*\d+\s*[:.]\s*/i, '');
        speakCookingStep(text);
    }
}

function cookingUseIngredient(idx, productId, location, qtyNumber, btn) {
    // Reuse the same modal used in the recipe dialog
    useRecipeIngredient(idx, productId, location, qtyNumber, btn);
    // Mark ingredient as used so it's hidden from further steps
    if (_cookingRecipe && _cookingRecipe.ingredients && _cookingRecipe.ingredients[idx]) {
        _cookingRecipe.ingredients[idx].used = true;
    }
    setTimeout(() => renderCookingStep(), 400);
}
// ===== END COOKING MODE =====

function updateRecipeMealTitle() {
    const meal = getSelectedMealType();
    const mealLabels = getMealLabels();
    document.getElementById('recipe-meal-title').textContent = mealLabels[meal] || t('recipes.dialog_title');
    _renderMealPlanHint(meal);
    _renderMealSubTypes(meal);
}

function _renderMealSubTypes(mealId) {
    const container = document.getElementById('recipe-subtype-group');
    if (!container) return;
    const subs = getMealSubTypes()[mealId];
    if (!subs) {
        container.style.display = 'none';
        container.innerHTML = '';
        return;
    }
    container.style.display = '';
    container.innerHTML = subs.map((s, i) =>
        `<label class="recipe-meal-chip recipe-subtype-chip"><input type="radio" name="recipe-subtype" value="${s.id}"${i === 0 ? ' checked' : ''}> ${s.icon} ${s.label}</label>`
    ).join('');
}

function getSelectedSubType() {
    const checked = document.querySelector('input[name="recipe-subtype"]:checked');
    return checked ? checked.value : '';
}

/** Show/hide the meal-plan badge hint + top banner in the recipe dialog. */
function onMealPlanChipChange(cb) {
    const show = cb.checked;
    const banner = document.getElementById('recipe-mealplan-banner');
    const hint   = document.getElementById('recipe-mealplan-hint');
    if (banner) banner.style.display = show ? 'flex' : 'none';
    if (hint)   hint.style.display   = show ? 'flex' : 'none';
}

function _renderMealPlanHint(mealSlot) {
    const el = document.getElementById('recipe-mealplan-hint');
    const banner = document.getElementById('recipe-mealplan-banner');
    const chipWrap = document.getElementById('recipe-opt-mealplan-wrap');
    const chipLabel = document.getElementById('recipe-opt-mealplan-label');
    const chipCb = document.getElementById('recipe-opt-mealplan');
    // mealSlot = 'pranzo' or 'cena' (from getMealType/getSelectedMealType)
    const typeId = (mealSlot === 'pranzo' || mealSlot === 'cena')
        ? getTodayMealPlanType(mealSlot)
        : null;
    if (!typeId || typeId === 'libero') {
        if (el) el.style.display = 'none';
        if (banner) banner.style.display = 'none';
        if (chipWrap) chipWrap.style.display = 'none';
        return;
    }
    const mpt = getMealPlanTypeMap()[typeId];
    if (!mpt) {
        if (el) el.style.display = 'none';
        if (banner) banner.style.display = 'none';
        if (chipWrap) chipWrap.style.display = 'none';
        return;
    }
    if (el) {
        el.innerHTML = `<span class="mplan-hint-badge">${mpt.icon} ${mpt.label}</span> <span class="mplan-hint-label">${t('meal_plan.suggested_by')}</span>`;
        el.style.display = 'flex';
    }
    if (banner) {
        const slotLabel = mealSlot === 'pranzo' ? '🌤️ ' + t('meal_types.pranzo') : '🌙 ' + t('meal_types.cena');
        banner.innerHTML = `<span style="opacity:0.75;font-weight:500">${slotLabel}</span><span style="opacity:0.45">·</span><span>${mpt.icon} ${mpt.label}</span>`;
        banner.style.display = 'flex';
    }
    // Show the meal-plan chip (active by default, user can uncheck to ignore the plan)
    if (chipWrap) {
        chipWrap.style.display = '';
        if (chipLabel) chipLabel.textContent = `${mpt.icon} ${mpt.label}`;
        if (chipCb) chipCb.checked = true;
    }
}

function regenerateRecipe() {
    // Collect main ingredients from the rejected recipe to exclude them
    if (_cachedRecipe && _cachedRecipe.recipe && _cachedRecipe.recipe.ingredients) {
        const mainIngs = _cachedRecipe.recipe.ingredients
            .filter(i => i.from_pantry)
            .map(i => i.name);
        _rejectedRecipeIngredients = [...new Set([..._rejectedRecipeIngredients, ...mainIngs])];
    }
    _cachedRecipe = null;
    const meal = getSelectedMealType();
    _recipeVariationCount[meal] = (_recipeVariationCount[meal] || 0) + 1;
    document.getElementById('recipe-result').style.display = 'none';
    document.getElementById('recipe-loading').style.display = 'none';
    document.getElementById('recipe-ask').style.display = '';
}

async function generateRecipe() {
    if (!_requireGemini()) return;
    const meal = getSelectedMealType();
    const persons = parseInt(document.getElementById('recipe-persons').value) || 1;
    const settings = getSettings();

    // Reset rejected ingredients on first generation (not regeneration)
    if ((_recipeVariationCount[meal] || 0) === 0) {
        _rejectedRecipeIngredients = [];
    }

    // Determine meal plan type for today's selected slot,
    // but only if the user has NOT unchecked the meal-plan chip
    const mealPlanChipWrap = document.getElementById('recipe-opt-mealplan-wrap');
    const mealPlanCb = document.getElementById('recipe-opt-mealplan');
    const mealPlanChipActive = !mealPlanChipWrap || mealPlanChipWrap.style.display === 'none' || (mealPlanCb && mealPlanCb.checked);
    const mealPlanType = mealPlanChipActive && (meal === 'pranzo' || meal === 'cena')
        ? (getTodayMealPlanType(meal) || null)
        : null;

    // Gather active options from checkboxes
    const options = [];
    const optMap = {
        'recipe-opt-veloce': 'veloce',
        'recipe-opt-pocafame': 'pocafame',
        'recipe-opt-scadenze': 'scadenze',
        'recipe-opt-healthy': 'salutare',
        'recipe-opt-opened': 'opened',
        'recipe-opt-zerowaste': 'zerowaste'
    };
    Object.entries(optMap).forEach(([id, key]) => {
        const cb = document.getElementById(id);
        if (cb && cb.checked) options.push(key);
    });

    document.getElementById('recipe-ask').style.display = 'none';
    document.getElementById('recipe-loading').style.display = '';
    document.getElementById('recipe-result').style.display = 'none';
    const loadingMsg = document.getElementById('recipe-loading-msg');

    try {
        const payload = {
            meal,
            persons,
            lang: _currentLang,
            sub_type: getMealSubTypes()[meal] ? getSelectedSubType() : '',
            options,
            appliances: settings.appliances || [],
            dietary_restrictions: settings.dietary_restrictions || '',
            today_recipes: [...new Set([...await getTodayRecipeTitles(), ..._generatedTodayTitles])],
            meal_plan_type: mealPlanType,
            variation: _recipeVariationCount[meal] || 0,
            rejected_ingredients: _rejectedRecipeIngredients,
        };

        const response = await fetch('api/index.php?action=generate_recipe_stream', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(payload)
        });

        if (!response.ok) {
            const data = await response.json().catch(() => ({}));
            document.getElementById('recipe-loading').style.display = 'none';
            document.getElementById('recipe-ask').style.display = '';
            if (data.error === 'no_api_key') {
                showToast(t('error.no_api_key'), 'warning');
            } else {
                showToast(data.error || t('recipes.generate_error'), 'error');
            }
            return;
        }

        const reader = response.body.getReader();
        const decoder = new TextDecoder();
        let buffer = '';
        let recipe = null;
        let errorEvent = null;

        while (true) {
            const { done, value } = await reader.read();
            if (done) break;
            buffer += decoder.decode(value, { stream: true });
            const lines = buffer.split('\n');
            buffer = lines.pop();
            for (const line of lines) {
                if (!line.startsWith('data: ')) continue;
                try {
                    const event = JSON.parse(line.slice(6));
                    if (event.type === 'status' && loadingMsg) {
                        loadingMsg.textContent = event.message;
                    } else if (event.type === 'recipe') {
                        recipe = event.recipe;
                    } else if (event.type === 'error') {
                        errorEvent = event;
                    }
                } catch (_) { /* ignore malformed SSE lines */ }
            }
        }

        if (recipe) {
            renderRecipe(recipe);
            if (recipe.title) _generatedTodayTitles.push(recipe.title);
            await saveRecipeToArchive(recipe);
            _cachedRecipe = { meal, recipe };
            document.getElementById('recipe-loading').style.display = 'none';
            document.getElementById('recipe-result').style.display = '';
        } else {
            document.getElementById('recipe-loading').style.display = 'none';
            document.getElementById('recipe-ask').style.display = '';
            if (errorEvent) {
                if (errorEvent.error === 'no_api_key') {
                    showToast(t('error.no_api_key'), 'warning');
                } else {
                    const detail = errorEvent.detail ? ` (${errorEvent.detail})` : '';
                    showToast((errorEvent.error || t('recipes.generate_error')) + detail, 'error');
                }
            } else {
                showToast(t('error.connection'), 'error');
            }
        }

    } catch (err) {
        console.error('Recipe error:', err);
        document.getElementById('recipe-loading').style.display = 'none';
        document.getElementById('recipe-ask').style.display = '';
        showToast(t('error.connection'), 'error');
    }
}

// ===== GEMINI CHAT =====
let chatHistory = [];
let chatInventoryContext = null;
let _chatSavedCount = 0; // track how many messages already saved to DB

function initChat() {
    // Load chat history from DB
    api('chat_list').then(res => {
        if (res.success && res.messages && res.messages.length > 0) {
            chatHistory = res.messages.map(m => ({ role: m.role, text: m.text }));
            _chatSavedCount = chatHistory.length;
            renderChatHistory();
        } else {
            _chatSavedCount = 0;
        }
    }).catch(() => { _chatSavedCount = 0; });
    // Always reload fresh inventory context
    loadChatContext();
    // Focus input
    setTimeout(() => {
        const input = document.getElementById('chat-input');
        if (input) input.focus();
    }, 300);
}

async function loadChatContext() {
    try {
        const data = await api('inventory_list');
        chatInventoryContext = data.inventory || [];
    } catch(e) { chatInventoryContext = []; }
}

function sendChatSuggestion(text) {
    document.getElementById('chat-input').value = text;
    sendChatMessage();
}

async function sendChatMessage() {
    const input = document.getElementById('chat-input');
    const text = input.value.trim();
    if (!text) return;
    
    input.value = '';
    
    // Hide welcome if first message
    const welcome = document.querySelector('.chat-welcome');
    if (welcome) welcome.style.display = 'none';
    
    // Add user message
    chatHistory.push({ role: 'user', text });
    appendChatBubble('user', text);
    saveChatHistory();
    
    // Show typing indicator
    const typingEl = appendChatBubble('gemini', '<div class="chat-typing"><span></span><span></span><span></span></div>', true);
    scrollChatBottom();
    
    // Disable send
    const btn = document.getElementById('btn-chat-send');
    btn.disabled = true;
    
    try {
        const settings = getSettings();
        const result = await api('gemini_chat', {}, 'POST', {
            message: text,
            history: chatHistory.slice(0, -1).slice(-20), // last 20 messages for context
            appliances: settings.appliances || [],
            dietary_restrictions: settings.dietary_restrictions || ''
        });
        
        // Remove typing indicator
        typingEl.remove();
        
        if (result.success) {
            chatHistory.push({ role: 'gemini', text: result.reply });
            appendChatBubble('gemini', formatChatReply(result.reply));
        } else {
            const errMsg = result.error === 'no_api_key' ? 'Configura la chiave API Gemini nelle impostazioni.' : (result.error || 'Errore nella risposta');
            appendChatBubble('gemini', `⚠️ ${escapeHtml(errMsg)}`);
        }
    } catch(err) {
        typingEl.remove();
        appendChatBubble('gemini', '⚠️ ' + t('error.connection'));
    }
    
    btn.disabled = false;
    saveChatHistory();
    scrollChatBottom();
}

function appendChatBubble(role, html, isRaw = false) {
    const container = document.getElementById('chat-messages');
    const bubble = document.createElement('div');
    bubble.className = `chat-bubble chat-${role}`;
    if (isRaw) {
        bubble.innerHTML = html;
    } else if (role === 'user') {
        bubble.textContent = html;
    } else {
        bubble.innerHTML = html;
    }
    container.appendChild(bubble);
    scrollChatBottom();
    return bubble;
}

function formatChatReply(text) {
    // Convert markdown-like formatting
    let html = escapeHtml(text);
    // Bold **text**
    html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
    // Italic *text*
    html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
    // Lists
    html = html.replace(/^- (.+)$/gm, '<li>$1</li>');
    html = html.replace(/(<li>.*<\/li>)/s, '<ul>$1</ul>');
    // Numbered lists  
    html = html.replace(/^(\d+)\. (.+)$/gm, '<li>$2</li>');
    // Line breaks
    html = html.replace(/\n/g, '<br>');
    // Clean up consecutive ul tags
    html = html.replace(/<\/ul>\s*<br>\s*<ul>/g, '');
    return html;
}

function renderChatHistory() {
    const container = document.getElementById('chat-messages');
    if (chatHistory.length === 0) return;
    
    // Hide welcome
    const welcome = container.querySelector('.chat-welcome');
    if (welcome) welcome.style.display = 'none';
    
    chatHistory.forEach(msg => {
        if (msg.role === 'user') {
            appendChatBubble('user', msg.text);
        } else {
            appendChatBubble('gemini', formatChatReply(msg.text));
        }
    });
    scrollChatBottom();
}

function scrollChatBottom() {
    const container = document.getElementById('chat-messages');
    setTimeout(() => container.scrollTop = container.scrollHeight, 50);
}

function clearChat() {
    chatHistory = [];
    api('chat_clear', {}, 'POST').catch(() => {});
    const container = document.getElementById('chat-messages');
    container.innerHTML = `
        <div class="chat-welcome">
            <svg class="gemini-icon-lg" viewBox="0 0 24 24" width="48" height="48" fill="#6366f1"><path d="M12 0C12 6.627 6.627 12 0 12c6.627 0 12 5.373 12 12 0-6.627 5.373-12 12-12-6.627 0-12-5.373-12-12z"/></svg>
            <h3>${t('chat.welcome')}</h3>
            <p>${t('chat.welcome_desc')}</p>
            <div class="chat-suggestions">
                <button class="chat-suggestion" onclick="sendChatSuggestion(t('chat.suggestion_snack_text'))">${t('chat.suggestion_snack')}</button>
                <button class="chat-suggestion" onclick="sendChatSuggestion(t('chat.suggestion_juice_text'))">${t('chat.suggestion_juice')}</button>
                <button class="chat-suggestion" onclick="sendChatSuggestion(t('chat.suggestion_light_text'))">${t('chat.suggestion_light')}</button>
                <button class="chat-suggestion" onclick="sendChatSuggestion(t('chat.suggestion_expiry_text'))">${t('chat.suggestion_expiry')}</button>
            </div>
        </div>
    `;
    showToast(t('chat.cleared'), 'success');
}

function saveChatHistory() {
    // Keep last 50 messages max
    if (chatHistory.length > 50) {
        const trimmed = chatHistory.length - 50;
        chatHistory = chatHistory.slice(-50);
        _chatSavedCount = Math.max(0, _chatSavedCount - trimmed);
    }
    // Only save messages that haven't been saved yet (prevent duplicates)
    const unsaved = chatHistory.slice(_chatSavedCount);
    if (unsaved.length === 0) return;
    api('chat_save', {}, 'POST', { messages: unsaved }).then(() => {
        _chatSavedCount = chatHistory.length;
    }).catch(() => {});
}

// ===== SCREENSAVER & INACTIVITY AUTO-REFRESH =====
let _inactivityTimer = null;
let _screensaverActive = false;
let _screensaverClockInterval = null;
let _screensaverFactInterval = null;
let _screensaverData = null; // cached data for fact generation
const SCREENSAVER_FACT_DURATION = 5 * 60 * 1000; // 5 minutes per fact
const INACTIVITY_TIMEOUT = 5 * 60 * 1000; // 5 minutes

function resetInactivityTimer() {
    if (_screensaverActive) return; // don't reset while screensaver is showing
    clearTimeout(_inactivityTimer);
    _inactivityTimer = setTimeout(activateScreensaver, INACTIVITY_TIMEOUT);
}

function activateScreensaver() {
    if (_screensaverActive) return;
    if (document.body.classList.contains('cooking-mode-active')) return;
    _screensaverActive = true;
    const overlay = document.getElementById('screensaver');
    overlay.style.display = 'flex';
    // Fade in
    requestAnimationFrame(() => overlay.classList.add('visible'));
    updateScreensaverClock();
    _screensaverClockInterval = setInterval(updateScreensaverClock, 1000);
    // Load data and start facts
    loadScreensaverData().then(() => {
        showNextScreensaverFact();
        _screensaverFactInterval = setInterval(showNextScreensaverFact, SCREENSAVER_FACT_DURATION);
    });
}

function updateScreensaverClock() {
    const now = new Date();
    const _ssLocale = _currentLang === 'de' ? 'de-DE' : _currentLang === 'en' ? 'en-GB' : 'it-IT';
    const time = now.toLocaleTimeString(_ssLocale, { hour: '2-digit', minute: '2-digit' });
    const date = now.toLocaleDateString(_ssLocale, { weekday: 'long', day: 'numeric', month: 'long' });
    const el = document.getElementById('screensaver-clock');
    if (el) el.innerHTML = `${time}<div class="screensaver-date">${date}</div>`;
    updateScreensaverMealPlan();
}

/** Show/hide the planned meal type badge on the screensaver based on current time slot. */
function updateScreensaverMealPlan() {
    const el = document.getElementById('screensaver-mealplan');
    if (!el) return;
    const s = getSettings();
    if (s.meal_plan_enabled === false) { el.style.display = 'none'; return; }
    const hour = new Date().getHours();
    // Before 15:00 show pranzo, from 15:00 onwards show cena
    const slot = hour < 15 ? 'pranzo' : 'cena';
    const typeId = getTodayMealPlanType(slot);
    if (!typeId || typeId === 'libero') { el.style.display = 'none'; return; }
    const mpt = getMealPlanTypeMap()[typeId];
    if (!mpt) { el.style.display = 'none'; return; }
    const slotLabel = slot === 'pranzo' ? '🌤️ ' + t('meal_types.pranzo') : '🌙 ' + t('meal_types.cena');
    el.innerHTML = `<span class="screensaver-mealplan-badge">${slotLabel} · ${mpt.icon} ${mpt.label}</span>`;
    el.style.display = 'block';
}

function dismissScreensaver(targetPage) {
    if (!_screensaverActive) return;
    clearInterval(_screensaverClockInterval);
    clearInterval(_screensaverFactInterval);
    const overlay = document.getElementById('screensaver');
    overlay.classList.remove('visible');
    setTimeout(() => {
        overlay.style.display = 'none';
        _screensaverActive = false;
        _screensaverData = null;
        if (targetPage) {
            showPage(targetPage);
        } else {
            refreshCurrentPage();
        }
        resetInactivityTimer();
    }, 400);
}

// Load all data needed for screensaver facts
async function loadScreensaverData() {
    try {
        const [statsRes, invRes, bringRes] = await Promise.all([
            api('stats'),
            api('inventory_list'),
            api('bring_list').catch(() => null)
        ]);
        _screensaverData = {
            stats: statsRes,
            inventory: invRes.inventory || [],
            shopping: bringRes && bringRes.success ? (bringRes.purchase || []) : []
        };
    } catch (e) {
        _screensaverData = { stats: {}, inventory: [], shopping: [] };
    }
}

// Show next random fact with fade in/out
function showNextScreensaverFact() {
    const el = document.getElementById('screensaver-fact');
    if (!el) return;
    el.classList.remove('visible');
    setTimeout(() => {
        el.textContent = generateScreensaverFact();
        el.classList.add('visible');
    }, 1600);
}

// Generate a dynamic fact from available data
function generateScreensaverFact() {
    const d = _screensaverData || { stats: {}, inventory: [], shopping: [] };
    const inv = d.inventory;
    const stats = d.stats;
    const shop = d.shopping;
    const now = new Date();
    const hour = now.getHours();

    // Pre-compute useful data
    const expired = stats.expired || [];
    const expiringSoon = stats.expiring_soon || [];
    const totalProducts = stats.total_products || inv.length;
    const totalItems = stats.total_items || 0;

    const byLocation = {};
    const byCategory = {};
    const withExpiry = [];
    const noExpiry = [];
    const expiringThisWeek = [];
    const expiringThisMonth = [];
    const inFreezer = [];
    const inFrigo = [];
    const inDispensa = [];

    for (const item of inv) {
        // by location
        const loc = item.location || 'altro';
        if (!byLocation[loc]) byLocation[loc] = [];
        byLocation[loc].push(item);
        if (loc === 'freezer') inFreezer.push(item);
        else if (loc === 'frigo') inFrigo.push(item);
        else if (loc === 'dispensa') inDispensa.push(item);

        // by category
        const cat = mapToLocalCategory(item.category, item.name);
        if (!byCategory[cat]) byCategory[cat] = [];
        byCategory[cat].push(item);

        // expiry
        if (item.expiry_date) {
            withExpiry.push(item);
            const days = daysUntilExpiry(item.expiry_date);
            if (days >= 0 && days <= 7) expiringThisWeek.push(item);
            if (days >= 0 && days <= 30) expiringThisMonth.push(item);
        } else {
            noExpiry.push(item);
        }
    }

    // Greeting based on time
    const greeting = hour < 12 ? 'Buongiorno' : hour < 18 ? 'Buon pomeriggio' : 'Buonasera';

    // Random item picker
    const rItem = (arr) => arr.length ? arr[Math.floor(Math.random() * arr.length)] : null;

    // All fact generators
    const facts = [];

    // --- Expired items facts ---
    if (expired.length > 0) {
        facts.push(() => `Hai ${expired.length} ${expired.length === 1 ? 'prodotto scaduto' : 'prodotti scaduti'} in dispensa. Controlla!`);
        facts.push(() => {
            const names = expired.slice(0, 3).map(i => i.name);
            return `Prodotti scaduti: ${names.join(', ')}${expired.length > 3 ? ` e altri ${expired.length - 3}` : ''}`;
        });
        const freezerExpired = expired.filter(i => i.location === 'freezer');
        if (freezerExpired.length > 0) {
            facts.push(() => {
                const item = rItem(freezerExpired);
                const safety = getExpiredSafety(item, Math.abs(daysUntilExpiry(item.expiry_date)));
                if (safety.level === 'ok' || safety.level === 'warning') {
                    return `${item.name} è scaduto, ma essendo in freezer potrebbe essere ancora buono! Controlla.`;
                }
                return `${item.name} in freezer è scaduto da troppo tempo. Meglio buttarlo.`;
            });
        }
        const frigoExpired = expired.filter(i => i.location === 'frigo');
        if (frigoExpired.length > 0) {
            facts.push(() => `Hai ${frigoExpired.length} ${frigoExpired.length === 1 ? 'prodotto scaduto' : 'prodotti scaduti'} in frigo!`);
        }
    }

    // --- Expiring soon facts ---
    if (expiringSoon.length > 0) {
        facts.push(() => {
            const item = expiringSoon[0];
            const days = daysUntilExpiry(item.expiry_date);
            if (days === 0) return `${item.name} scade oggi! Usalo subito.`;
            if (days === 1) return `${item.name} scade domani. Pensaci!`;
            return `${item.name} scade tra ${days} giorni.`;
        });
        if (expiringSoon.length > 1) {
            facts.push(() => `Hai ${expiringSoon.length} prodotti in scadenza ravvicinata.`);
        }
    }
    if (expiringThisWeek.length > 0) {
        facts.push(() => `Questa settimana scadono ${expiringThisWeek.length} prodotti. Pianifica i pasti di conseguenza!`);
        facts.push(() => {
            const item = rItem(expiringThisWeek);
            const days = daysUntilExpiry(item.expiry_date);
            const locLabel = LOCATIONS[item.location]?.label || item.location;
            return `${item.name} (${locLabel}) scade tra ${days} ${days === 1 ? 'giorno' : 'giorni'}.`;
        });
    }
    if (expiringThisMonth.length > 0) {
        facts.push(() => `In questo mese scadranno ${expiringThisMonth.length} prodotti.`);
    }

    // --- Shopping list facts ---
    if (shop.length > 0) {
        facts.push(() => `Hai ${shop.length} ${shop.length === 1 ? 'prodotto' : 'prodotti'} nella lista della spesa.`);
        facts.push(() => {
            const names = shop.slice(0, 4).map(i => i.name);
            return `Nella spesa: ${names.join(', ')}${shop.length > 4 ? '...' : ''}`;
        });
    }
    if (shop.length === 0) {
        facts.push(() => `La lista della spesa è vuota. Tutto a posto!`);
    }

    // --- Location-based facts ---
    if (inFrigo.length > 0) {
        facts.push(() => `Hai ${inFrigo.length} prodotti in frigo.`);
        facts.push(() => {
            const item = rItem(inFrigo);
            return `In frigo c'è: ${item.name}${item.brand ? ' (' + item.brand + ')' : ''}.`;
        });
    }
    if (inFreezer.length > 0) {
        facts.push(() => `Hai ${inFreezer.length} prodotti nel freezer.`);
        facts.push(() => {
            const item = rItem(inFreezer);
            return `Nel freezer c'è: ${item.name}. Non dimenticartelo!`;
        });
    }
    if (inDispensa.length > 0) {
        facts.push(() => `In dispensa ci sono ${inDispensa.length} prodotti.`);
    }

    // --- Category-based facts ---
    const catEntries = Object.entries(byCategory);
    if (catEntries.length > 0) {
        facts.push(() => {
            const sorted = catEntries.sort((a, b) => b[1].length - a[1].length);
            const top = sorted[0];
            const catLabel = top[0];
            const icon = CATEGORY_ICONS[catLabel] || '📦';
            return `La categoria più presente è ${icon} ${catLabel} con ${top[1].length} prodotti.`;
        });
        if (byCategory['carne'] && byCategory['carne'].length > 0) {
            facts.push(() => `Hai ${byCategory['carne'].length} prodotti di carne. 🥩`);
        }
        if (byCategory['latticini'] && byCategory['latticini'].length > 0) {
            facts.push(() => `Hai ${byCategory['latticini'].length} latticini in casa. 🥛`);
        }
        if (byCategory['verdura'] && byCategory['verdura'].length > 0) {
            facts.push(() => `Hai ${byCategory['verdura'].length} tipi di verdura. Ottimo per la salute! 🥬`);
        }
        if (byCategory['frutta'] && byCategory['frutta'].length > 0) {
            facts.push(() => `Hai ${byCategory['frutta'].length} tipi di frutta. 🍎`);
        }
        if (byCategory['bevande'] && byCategory['bevande'].length > 0) {
            facts.push(() => `Hai ${byCategory['bevande'].length} bevande disponibili. 🥤`);
        }
        if (byCategory['surgelati'] && byCategory['surgelati'].length > 0) {
            facts.push(() => `Hai ${byCategory['surgelati'].length} surgelati nel freezer. ❄️`);
        }
        if (byCategory['pasta'] && byCategory['pasta'].length > 0) {
            facts.push(() => `Hai ${byCategory['pasta'].length} tipi di pasta. 🍝 Che ne dici di una carbonara?`);
        }
        if (byCategory['conserve'] && byCategory['conserve'].length > 0) {
            facts.push(() => `Hai ${byCategory['conserve'].length} conserve in dispensa. 🥫`);
        }
        if (byCategory['snack'] && byCategory['snack'].length > 0) {
            facts.push(() => `Hai ${byCategory['snack'].length} snack. Resisti alla tentazione! 🍪`);
        }
        if (byCategory['condimenti'] && byCategory['condimenti'].length > 0) {
            facts.push(() => `Hai ${byCategory['condimenti'].length} condimenti a disposizione. 🧂`);
        }
    }

    // --- General inventory facts ---
    if (inv.length > 0) {
        facts.push(() => `Hai ${totalProducts} prodotti diversi in casa per un totale di ${Math.round(totalItems)} pezzi.`);
        facts.push(() => {
            const item = rItem(inv);
            return `Lo sapevi? Hai ${item.name} in ${LOCATIONS[item.location]?.label || item.location}.`;
        });
        facts.push(() => {
            const item = rItem(inv);
            const qty = formatQuantity(item.quantity, item.unit, item.default_quantity, item.package_unit);
            return `${item.name}: ne hai ${qty}.`;
        });
    }
    if (noExpiry.length > 0) {
        facts.push(() => `${noExpiry.length} prodotti non hanno una data di scadenza impostata.`);
    }
    if (withExpiry.length > 0) {
        // Find the one expiring furthest away
        const furthest = withExpiry.reduce((best, item) => {
            const d = daysUntilExpiry(item.expiry_date);
            return d > (best.d || 0) ? { item, d } : best;
        }, { d: 0 });
        if (furthest.item && furthest.d > 30) {
            facts.push(() => `Il prodotto con scadenza più lontana è ${furthest.item.name}: ${Math.round(furthest.d / 30)} mesi.`);
        }
    }

    // --- Quantity-based facts ---
    const highQtyItems = inv.filter(i => parseFloat(i.quantity) >= 5);
    if (highQtyItems.length > 0) {
        facts.push(() => {
            const item = rItem(highQtyItems);
            const qty = formatQuantity(item.quantity, item.unit, item.default_quantity, item.package_unit);
            return `Hai una bella scorta di ${item.name}: ${qty}!`;
        });
    }
    const lowQtyItems = inv.filter(i => parseFloat(i.quantity) <= 1 && parseFloat(i.quantity) > 0);
    if (lowQtyItems.length > 0) {
        facts.push(() => {
            const item = rItem(lowQtyItems);
            return `${item.name} sta per finire. Aggiungilo alla spesa?`;
        });
        facts.push(() => `Ci sono ${lowQtyItems.length} prodotti quasi finiti.`);
    }

    // --- Time-of-day greetings & suggestions ---
    facts.push(() => `${greeting}! Se vuoi che ti preparo una ricetta, tocca qui.`);
    facts.push(() => `${greeting}! La tua dispensa è sotto controllo. 😊`);
    if (hour >= 6 && hour < 10) {
        facts.push(() => `Buongiorno! Pronto per la colazione? ☕`);
        if (byCategory['pane']) facts.push(() => `Buongiorno! Hai del pane per la colazione. 🍞`);
        if (byCategory['latticini']) facts.push(() => `C'è del latte in frigo per il cappuccino? ☕🥛`);
    }
    if (hour >= 11 && hour < 14) {
        facts.push(() => `È quasi ora di pranzo! Cosa cuciniamo? 🍽️`);
        if (byCategory['pasta']) facts.push(() => `Ora di pranzo… Un bel piatto di pasta? 🍝`);
    }
    if (hour >= 17 && hour < 21) {
        facts.push(() => `Buona sera! Hai pensato alla cena? 🍽️`);
        if (byCategory['carne']) facts.push(() => `Per cena potresti usare la carne che hai. 🥩`);
        if (byCategory['pesce']) facts.push(() => `Che ne dici di pesce per cena? 🐟`);
    }
    if (hour >= 21 || hour < 6) {
        facts.push(() => `Buonanotte! Domani controlla le scadenze. 🌙`);
    }

    // --- Weekly stats ---
    const recentIn = stats.recent_in || 0;
    const recentOut = stats.recent_out || 0;
    if (recentIn > 0) {
        facts.push(() => `Questa settimana hai aggiunto ${recentIn} prodotti.`);
    }
    if (recentOut > 0) {
        facts.push(() => `Questa settimana hai consumato ${recentOut} prodotti.`);
    }
    if (recentIn > 0 && recentOut > 0) {
        facts.push(() => `Bilancio settimanale: +${recentIn} entrati, -${recentOut} usciti.`);
    }

    // --- Tips & curiosità (statici ma ruotano) ---
    facts.push(() => `💡 Lo sapevi? I prodotti in freezer durano molto più a lungo della data di scadenza.`);
    facts.push(() => `💡 Il pane congelato mantiene la fragranza per settimane.`);
    facts.push(() => `💡 Le uova si conservano fino a 3-4 settimane dopo la data preferita.`);
    facts.push(() => `💡 Lo yogurt chiuso in frigo dura spesso 1-2 settimane oltre la scadenza.`);
    facts.push(() => `💡 Per evitare sprechi, usa prima i prodotti con scadenza più vicina.`);
    facts.push(() => `💡 La carne in freezer può durare fino a 6 mesi senza problemi.`);
    facts.push(() => `💡 Le verdure fresche durano di più se conservate nel cassetto del frigo.`);
    facts.push(() => `💡 Controlla regolarmente la dispensa per evitare doppioni nella spesa.`);
    facts.push(() => `💡 I latticini vanno conservati nella parte più fredda del frigo.`);
    facts.push(() => `💡 Non ricongelare mai un alimento già scongelato. Cucinalo subito!`);
    facts.push(() => `💡 Un frigo ordinato ti fa risparmiare tempo e denaro.`);
    facts.push(() => `💡 Le conserve aperte vanno in frigo e consumate in pochi giorni.`);

    // --- Brand-based facts ---
    const brands = inv.filter(i => i.brand).map(i => i.brand);
    if (brands.length > 0) {
        const brandCount = {};
        brands.forEach(b => { brandCount[b] = (brandCount[b] || 0) + 1; });
        const topBrand = Object.entries(brandCount).sort((a, b) => b[1] - a[1])[0];
        facts.push(() => `Il marca più presente nella tua dispensa è ${topBrand[0]} con ${topBrand[1]} prodotti.`);
    }

    // --- Specific food combo facts ---
    if (byCategory['pasta'] && byCategory['condimenti']) {
        facts.push(() => `Hai pasta e condimenti: sei pronto per un primo piatto! 🍝`);
    }
    if (byCategory['pane'] && byCategory['carne']) {
        facts.push(() => `Pane e carne: un panino veloce è sempre una buona idea! 🥪`);
    }
    if (byCategory['verdura'] && byCategory['carne']) {
        facts.push(() => `Verdura e carne: hai tutto per un piatto equilibrato! 🥗🥩`);
    }

    // --- Empty states ---
    if (inv.length === 0) {
        facts.push(() => `La dispensa è vuota! Fai una bella spesa. 🛒`);
        facts.push(() => `Nessun prodotto registrato. Scansiona qualcosa per iniziare!`);
    }

    // --- Location distribution ---
    const locCount = Object.keys(byLocation).length;
    if (locCount > 1) {
        facts.push(() => {
            const parts = Object.entries(byLocation).map(([loc, items]) => 
                `${LOCATIONS[loc]?.icon || '📦'} ${items.length}`
            );
            return `Distribuzione: ${parts.join('  ·  ')}`;
        });
    }

    // --- Anti-waste knowledge facts ---
    const awFacts = _awGetFacts();
    for (const f of awFacts) { facts.push(() => f); }

    // Pick a random fact
    if (facts.length === 0) {
        return `${greeting}! La tua Dispensa ti aspetta.`;
    }
    return facts[Math.floor(Math.random() * facts.length)]();
}

// ===== SPESA MODE (long-press camera for continuous scanning) =====
let _spesaMode = false;
let _longPressTimer = null;
let _spesaSession = []; // { name, qty, unit } per ogni prodotto aggiunto

function initSpesaMode() {
    const btn = document.getElementById('btn-header-scan');
    if (!btn) return;

    btn.addEventListener('pointerdown', (e) => {
        _longPressTimer = setTimeout(() => {
            _longPressTimer = null;
            startSpesaMode();
        }, 600);
    });
    btn.addEventListener('pointerup', () => {
        if (_longPressTimer) {
            clearTimeout(_longPressTimer);
            _longPressTimer = null;
            // Short press — normal scan
            showPage('scan');
        }
    });
    btn.addEventListener('pointerleave', () => {
        if (_longPressTimer) {
            clearTimeout(_longPressTimer);
            _longPressTimer = null;
        }
    });
}

function startSpesaMode() {
    _spesaMode = true;
    _spesaSession = [];
    showToast('🛒 Modalità Spesa attivata!', 'success');
    showPage('scan');
    updateSpesaBanner();
}

function endSpesaMode() {
    _spesaMode = false;
    updateSpesaBanner();
    stopScanner();
    showPage('dashboard');
}

function updateSpesaBanner() {
    const banner = document.getElementById('spesa-mode-banner');
    if (!banner) return;
    banner.style.display = _spesaMode ? 'flex' : 'none';
    const statEl = banner.querySelector('.spesa-stat');
    if (statEl) statEl.textContent = _spesaBannerStat();
}

// Called after successful add — returns true if spesa mode handled navigation
function spesaModeAfterAdd() {
    if (!_spesaMode) return false;
    // Track this product in the session
    if (currentProduct) {
        _spesaSession.push({ name: currentProduct.name, category: currentProduct.category || '' });
        updateSpesaBanner();
    }
    showPage('scan');
    return true;
}

function _spesaBannerStat() {
    const n = _spesaSession.length;
    if (n === 0) return t('shopping.session_empty');
    const cats = {};
    _spesaSession.forEach(p => { const c = p.category || 'altro'; cats[c] = (cats[c]||0)+1; });
    const topCat = Object.entries(cats).sort((a,b)=>b[1]-a[1])[0];
    const names = _spesaSession.map(p => p.name);
    const unique = [...new Set(names)];
    const dupes = names.length - unique.length;
    const phrases = [
        n === 1 ? t('kiosk_session.first_item').replace('{name}', _spesaSession[0].name) : null,
        n >= 2 && n < 5 ? t('kiosk_session.items_two_four').replace('{n}', n) : null,
        n >= 5 && n < 10 ? t('kiosk_session.items_five_nine').replace('{n}', n) : null,
        n >= 10 && n < 20 ? t('kiosk_session.items_ten_twenty').replace('{n}', n) : null,
        n >= 20 ? t('kiosk_session.items_twenty_plus').replace('{n}', n) : null,
        dupes > 0 ? (dupes === 1 ? t('kiosk_session.duplicates_one') : t('kiosk_session.duplicates_many').replace('{n}', dupes)) : null,
        topCat && topCat[1] > 1 ? t('kiosk_session.top_category').replace('{cat}', topCat[0]).replace('{count}', topCat[1]) : null,
    ].filter(Boolean);
    return phrases[n % phrases.length] || t('kiosk_session.items_fallback').replace('{n}', n).replace('{plural}', n===1?'o':'i');
}

function _initScreensaverShortcutBtn(btnId, targetPage, longPressFn) {
    const btn = document.getElementById(btnId);
    if (!btn) return;
    let ssLongPress = null;
    btn.addEventListener('pointerdown', (e) => {
        e.stopPropagation();
        if (longPressFn) {
            ssLongPress = setTimeout(() => {
                ssLongPress = null;
                dismissScreensaver(targetPage);
                setTimeout(longPressFn, 500);
            }, 600);
        }
    });
    btn.addEventListener('pointerup', (e) => {
        e.stopPropagation();
        if (longPressFn && ssLongPress) {
            clearTimeout(ssLongPress);
            ssLongPress = null;
        }
        dismissScreensaver(targetPage);
    });
    btn.addEventListener('pointerleave', (e) => {
        e.stopPropagation();
        if (ssLongPress) {
            clearTimeout(ssLongPress);
            ssLongPress = null;
        }
    });
    ['click', 'touchstart', 'touchend'].forEach(evt => {
        btn.addEventListener(evt, (e) => e.stopPropagation(), { passive: false });
    });
}

function initScreensaverShortcuts() {
    _initScreensaverShortcutBtn('screensaver-scan-btn', 'scan', () => startSpesaMode());
    _initScreensaverShortcutBtn('screensaver-recipe-btn', 'recipe', null);
}

function initInactivityWatcher() {
    const s = getSettings();
    if (!s.screensaver_enabled) return; // disabled by default
    const events = ['pointerdown', 'pointermove', 'keydown', 'scroll', 'touchstart'];
    events.forEach(evt => {
        document.addEventListener(evt, () => {
            if (_screensaverActive) {
                dismissScreensaver();
            } else {
                resetInactivityTimer();
            }
        }, { passive: true });
    });
    resetInactivityTimer();
}

// ===== INITIALIZATION =====
document.addEventListener('DOMContentLoaded', () => {
    // Load translations first, then initialize the app
    loadTranslations(_currentLang).then(() => {
        _initApp();
    }).catch(() => {
        _initApp(); // fallback: initialize even if translations fail
    });
});

// ===== SETUP WIZARD =====
let _setupStep = 0;
let _setupPendingSteps = [];
const _setupData = { lang: _currentLang, gemini_key: '', bring_email: '', bring_password: '' };

/**
 * Returns indices of setup steps that still need configuration.
 * Accepts optional serverSettings fetched from the API so server-side
 * credentials (stored in .env) are also considered.
 */
function _getMissingSetupSteps(serverSettings) {
    const missing = [];
    const s = getSettings();
    const srv = serverSettings || {};
    const setupDone = localStorage.getItem('evershelf_setup_done');

    // Step 0 — language: missing only if never set at all (fresh install)
    if (!localStorage.getItem('evershelf_lang') && !setupDone) {
        missing.push(0);
    }
    // Steps 1 & 2 only show on first run (before setup is completed/skipped)
    if (!setupDone) {
        // Step 1 — Gemini API key (check both localStorage and server .env)
        if (!s.gemini_key && !srv.gemini_key_set) missing.push(1);
        // Step 2 — Bring! credentials (check both localStorage and server .env)
        if ((!s.bring_email && !srv.bring_email) || (!s.bring_password && !srv.bring_password_set)) missing.push(2);
    }
    // Note: step 3 (done screen) gets appended automatically when there are missing steps

    return missing;
}

function _setupSteps() {
    return [
        {
            title: '🌐 ' + t('settings.language.label'),
            desc: t('settings.language.hint'),
            render: () => {
                let html = '<div class="setup-lang-grid">';
                for (const [code, name] of Object.entries(_SUPPORTED_LANGS)) {
                    const sel = code === _setupData.lang ? ' selected' : '';
                    html += `<button class="setup-lang-btn${sel}" onclick="_setupSelectLang('${code}')">${name}</button>`;
                }
                html += '</div>';
                return html;
            }
        },
        {
            title: '🤖 Google Gemini AI',
            desc: t('settings.gemini.hint'),
            render: () => `
                <div class="form-group">
                    <label>${t('settings.gemini.key_label')}</label>
                    <input type="text" id="setup-gemini-key" class="form-input" placeholder="AIza..." value="${_setupData.gemini_key}">
                    <p style="color:#999;font-size:0.8rem;margin-top:8px">
                        <a href="https://aistudio.google.com/apikey" target="_blank" rel="noopener">→ Get a free API key from Google AI Studio</a>
                    </p>
                </div>
                <span class="setup-skip-link" onclick="_setupSkipStep()">${t('btn.cancel')} — ${_currentLang === 'it' ? 'configura dopo' : 'configure later'}</span>
            `
        },
        {
            title: '🛒 Bring! Shopping List',
            desc: t('settings.bring.hint'),
            render: () => `
                <div class="form-group">
                    <label>${t('settings.bring.email_label')}</label>
                    <input type="email" id="setup-bring-email" class="form-input" placeholder="email@example.com" value="${_setupData.bring_email}">
                </div>
                <div class="form-group">
                    <label>${t('settings.bring.password_label')}</label>
                    <input type="password" id="setup-bring-password" class="form-input" placeholder="Password" value="${_setupData.bring_password}">
                </div>
                <span class="setup-skip-link" onclick="_setupSkipStep()">${t('btn.cancel')} — ${_currentLang === 'it' ? 'configura dopo' : 'configure later'}</span>
            `
        },
        {
            title: '✅ ' + (_currentLang === 'it' ? 'Tutto pronto!' : _currentLang === 'de' ? 'Alles bereit!' : 'All set!'),
            desc: _currentLang === 'it' ? 'La configurazione è completata. Puoi sempre modificare queste impostazioni dalla pagina Configurazione.'
                 : _currentLang === 'de' ? 'Die Konfiguration ist abgeschlossen. Du kannst diese Einstellungen jederzeit ändern.'
                 : 'Setup is complete. You can always change these settings from the Settings page.',
            render: () => {
                let summary = '<div style="text-align:center;font-size:2.5rem;margin:12px 0">🎉</div>';
                return summary;
            }
        }
    ];
}

function showSetupWizard(pendingSteps) {
    _setupPendingSteps = pendingSteps || _getMissingSetupSteps();
    if (_setupPendingSteps.length === 0) return;
    // Append the "done" step (3) at the end
    _setupPendingSteps.push(3);
    _setupStep = 0;
    // Pre-fill _setupData from existing settings so we don't lose them
    const s = getSettings();
    if (s.gemini_key) _setupData.gemini_key = s.gemini_key;
    if (s.bring_email) _setupData.bring_email = s.bring_email;
    if (s.bring_password) _setupData.bring_password = s.bring_password;
    document.getElementById('setup-wizard').style.display = '';
    _renderSetupStep();
}

function _renderSetupStep() {
    const allSteps = _setupSteps();
    const totalPending = _setupPendingSteps.length;
    const realIndex = _setupPendingSteps[_setupStep];
    const step = allSteps[realIndex];

    // Progress dots (based on pending steps only)
    const dotsHtml = _setupPendingSteps.map((_, i) => {
        let cls = 'setup-dot';
        if (i < _setupStep) cls += ' done';
        if (i === _setupStep) cls += ' active';
        return `<div class="${cls}"></div>`;
    }).join('');
    document.getElementById('setup-progress').innerHTML = dotsHtml;

    // Body
    document.getElementById('setup-body').innerHTML = `<h3>${step.title}</h3><p>${step.desc}</p>${step.render()}`;

    // Buttons
    const prevBtn = document.getElementById('setup-prev');
    const nextBtn = document.getElementById('setup-next');
    prevBtn.style.display = _setupStep > 0 ? '' : 'none';
    prevBtn.textContent = t('btn.back');

    if (_setupStep === totalPending - 1) {
        nextBtn.textContent = _currentLang === 'it' ? '🚀 Inizia!' : _currentLang === 'de' ? '🚀 Los geht\'s!' : '🚀 Start!';
    } else {
        nextBtn.textContent = _currentLang === 'it' ? 'Avanti →' : _currentLang === 'de' ? 'Weiter →' : 'Next →';
    }
}

function _setupSelectLang(lang) {
    _setupData.lang = lang;
    document.querySelectorAll('.setup-lang-btn').forEach(b => b.classList.remove('selected'));
    event.target.classList.add('selected');
}

function _setupSkipStep() {
    _setupStep++;
    _renderSetupStep();
}

function _setupCollectCurrent() {
    const realIndex = _setupPendingSteps[_setupStep];
    if (realIndex === 1) {
        const el = document.getElementById('setup-gemini-key');
        if (el) _setupData.gemini_key = el.value.trim();
    } else if (realIndex === 2) {
        const email = document.getElementById('setup-bring-email');
        const pass = document.getElementById('setup-bring-password');
        if (email) _setupData.bring_email = email.value.trim();
        if (pass) _setupData.bring_password = pass.value.trim();
    }
}

function setupWizardNav(dir) {
    _setupCollectCurrent();
    const totalPending = _setupPendingSteps.length;
    const realIndex = _setupPendingSteps[_setupStep];

    if (dir === 1 && _setupStep === totalPending - 1) {
        _finishSetup();
        return;
    }

    // If language changed, apply it
    if (realIndex === 0 && dir === 1 && _setupData.lang !== _currentLang) {
        localStorage.setItem('evershelf_lang', _setupData.lang);
        localStorage.setItem('evershelf_setup_step', String(_setupStep + 1));
        localStorage.setItem('evershelf_setup_pending', JSON.stringify(_setupPendingSteps));
        localStorage.setItem('evershelf_setup_data', JSON.stringify(_setupData));
        location.reload();
        return;
    }

    _setupStep = Math.max(0, Math.min(totalPending - 1, _setupStep + dir));
    _renderSetupStep();
}

async function _finishSetup() {
    // Save settings
    const s = getSettings();
    if (_setupData.gemini_key) s.gemini_key = _setupData.gemini_key;
    if (_setupData.bring_email) s.bring_email = _setupData.bring_email;
    if (_setupData.bring_password) s.bring_password = _setupData.bring_password;
    saveSettingsToStorage(s);

    // Save server-side settings (.env) — only send non-empty values to avoid overwriting existing config
    const envPayload = {};
    if (_setupData.gemini_key) envPayload.gemini_key = _setupData.gemini_key;
    if (_setupData.bring_email) envPayload.bring_email = _setupData.bring_email;
    if (_setupData.bring_password) envPayload.bring_password = _setupData.bring_password;
    try {
        if (Object.keys(envPayload).length > 0) {
            await api('save_settings', {}, 'POST', envPayload);
        }
    } catch(e) { /* will work locally */ }

    localStorage.setItem('evershelf_setup_done', '1');
    localStorage.removeItem('evershelf_setup_step');
    localStorage.removeItem('evershelf_setup_data');
    document.getElementById('setup-wizard').style.display = 'none';
}

async function _initApp() {
    // Check for setup wizard resume (after language change)
    const resumeStep = localStorage.getItem('evershelf_setup_step');
    const resumeData = localStorage.getItem('evershelf_setup_data');
    const resumePending = localStorage.getItem('evershelf_setup_pending');
    if (resumeStep && resumePending) {
        try { Object.assign(_setupData, JSON.parse(resumeData)); } catch(e) {}
        try { _setupPendingSteps = JSON.parse(resumePending); } catch(e) {}
        _setupStep = parseInt(resumeStep) || 0;
        localStorage.removeItem('evershelf_setup_step');
        localStorage.removeItem('evershelf_setup_data');
        localStorage.removeItem('evershelf_setup_pending');
        document.getElementById('setup-wizard').style.display = '';
        _renderSetupStep();
    } else {
        // Fetch server settings first so .env credentials (Bring!, Gemini)
        // are taken into account before deciding which wizard steps to show.
        let serverSettings = {};
        try { serverSettings = await api('get_settings'); } catch(e) {}
        _geminiAvailable = !!(serverSettings.gemini_key_set);
        _demoMode = !!serverSettings.demo_mode;
        _updateGeminiButtonState();
        _applyDemoModeUI();
        const missing = _getMissingSetupSteps(serverSettings);
        if (missing.length > 0 && !_demoMode) {
            showSetupWizard(missing);
        }
    }

    // Migrate old session-based flags to time-based
    if (sessionStorage.getItem('_autoAddedCritical')) {
        sessionStorage.removeItem('_autoAddedCritical');
    }
    // One-time reset of bg sync timestamp so first load always triggers a sync
    if (!localStorage.getItem('_bgBringSyncReset_v1')) {
        localStorage.removeItem('_bgBringSyncTs');
        localStorage.setItem('_bgBringSyncReset_v1', '1');
    }
    syncSettingsFromDB().then(() => {
        scaleInit(); // connect to smart scale gateway if configured (needs settings)
        initInactivityWatcher();
    });
    showPage('dashboard');
    initSpesaMode();
    initScreensaverShortcuts();
    startBgShoppingRefresh();
    _injectKioskOverlay(); // kiosk X / refresh buttons (only when running inside Android WebView)

    // Hide preloader once the dashboard is rendered
    const preloader = document.getElementById('app-preloader');
    if (preloader) {
        preloader.classList.add('fade-out');
        setTimeout(() => preloader.remove(), 380);
    }

    // Defer update check: fire 6 s after app is ready so it doesn't compete
    // with initial API calls and the PHP worker isn't blocked during startup.
    setTimeout(_checkWebappUpdate, 6000);

    // ── Background intervals ───────────────────────────────────────────────
    // 1) Ogni 5 min: ricarica la pagina corrente (scadenze, inventario, ecc.)
    setInterval(() => {
        if (!_screensaverActive) refreshCurrentPage();
    }, 5 * 60 * 1000);

    // 2) Ogni 2 min: aggiorna contatore lista spesa nel badge dashboard
    setInterval(() => {
        if (_screensaverActive) return;
        if (_currentPageId === 'shopping') {
            loadShoppingList();
        } else {
            loadShoppingCount();
        }
    }, 2 * 60 * 1000);

    // 3) Aggiorna immediatamente quando la tab torna visibile (es. torni da Bring! app)
    document.addEventListener('visibilitychange', () => {
        if (!document.hidden) {
            refreshCurrentPage();
            _checkWebappUpdate(); // also check for app updates when user returns to tab
        }
    });

    // 4) Background Bring sync ogni 5 min — completamente autonomo, non dipende
    //    dalla pagina corrente. Aggiunge urgenti, aggiorna spec, rimuove risolti.
    _backgroundBringSync();
    setInterval(() => { if (!_screensaverActive) _backgroundBringSync(); }, 5 * 60 * 1000);

    // 5) Anti-waste live refresh — starts/stops based on connectivity.
    window.addEventListener('online',  () => { _updateAwLiveDot(true);  _startAntiWasteAutoRefresh(); });
    window.addEventListener('offline', () => { _updateAwLiveDot(false); clearInterval(_awRefreshTimer); });
    // ─────────────────────────────────────────────────────────────────────
}

/**
 * Background sync — runs every 5 min regardless of current page.
 * Fully autonomous: fetches fresh data, syncs Bring urgency specs,
 * adds missing urgent items, removes obsolete auto-added items.
 * Never depends on page navigation or user interaction.
 */
async function _backgroundBringSync() {
    const lastRun = parseInt(localStorage.getItem('_bgBringSyncTs') || '0');
    if (Date.now() - lastRun < 5 * 60 * 1000) return;
    localStorage.setItem('_bgBringSyncTs', String(Date.now()));

    try {
        const [bringData, smartData] = await Promise.all([
            api('bring_list').catch(() => null),
            api('smart_shopping').catch(() => null),
        ]);

        if (!bringData?.success || !smartData?.success) return;

        const listUUID = bringData.listUUID;
        const bringItems = bringData.purchase || [];
        const smartItems = smartData.items || [];

        if (!listUUID || !smartItems.length) return;

        // Always update local caches with fresh data
        smartShoppingItems = smartItems;
        _smartShoppingLastFetch = Date.now();
        shoppingListUUID = listUUID;
        shoppingItems = bringItems;

        const toAdd    = []; // new items not yet on Bring
        const toUpdate = []; // items on Bring that need spec updated
        const toRemove = []; // items on Bring that are no longer urgent (auto-added, now resolved)

        // Build set of auto-added item names so we can safely remove them if resolved
        const autoAdded = new Set(JSON.parse(localStorage.getItem('_bgAutoAdded') || '[]'));

        for (const si of smartItems) {
            const expectedSpec = _urgencyToSpec(si.urgency, '');
            const bringMatch = bringItems.find(bi => {
                if (bi.name.toLowerCase() === si.name.toLowerCase()) return true;
                const biFirst = _nameTokens(bi.name)[0];
                const siFirst = _nameTokens(si.name)[0];
                return biFirst && siFirst && biFirst === siFirst;
            });

            if (!bringMatch) {
                // Not on Bring — add if high/critical and not blocklisted
                if ((si.urgency === 'critical' || si.urgency === 'high') && !_isBringPurchased(si.name, si.urgency)) {
                    toAdd.push({ name: si.name, specification: expectedSpec });
                    autoAdded.add(si.name.toLowerCase());
                }
            } else {
                // Already on Bring — sync urgency spec unconditionally
                const currentSpec = (bringMatch.specification || '').toLowerCase();
                const hasUrgencyMarker = currentSpec.includes('urgente') || currentSpec.includes('presto');
                const expectedLower = (expectedSpec || '').toLowerCase();
                const specChanged = expectedSpec
                    ? !currentSpec.includes(expectedLower.split(' ')[1] || expectedLower) // marker changed
                    : hasUrgencyMarker; // need to clear

                if (specChanged) {
                    toUpdate.push({ name: bringMatch.name, specification: expectedSpec, update_spec: true });
                    bringMatch.specification = expectedSpec;
                }
            }
        }

        // Remove items auto-added by us that are no longer urgent (resolved)
        for (const bi of bringItems) {
            const nameLower = bi.name.toLowerCase();
            if (!autoAdded.has(nameLower)) continue; // not auto-added by us, skip
            const stillUrgent = smartItems.some(si => {
                if (si.name.toLowerCase() === nameLower) return si.urgency === 'high' || si.urgency === 'critical';
                const siFirst = _nameTokens(si.name)[0];
                const biFirst = _nameTokens(bi.name)[0];
                return siFirst && biFirst && siFirst === biFirst && (si.urgency === 'high' || si.urgency === 'critical');
            });
            if (!stillUrgent) {
                toRemove.push(bi.name);
                autoAdded.delete(nameLower);
            }
        }

        // Persist updated auto-added set
        localStorage.setItem('_bgAutoAdded', JSON.stringify([...autoAdded]));

        const allChanges = [...toAdd, ...toUpdate];
        if (allChanges.length > 0) {
            await api('bring_add', {}, 'POST', { items: allChanges, listUUID });
            logOperation('bg_bring_sync', { added: toAdd.map(i=>i.name), updated: toUpdate.map(i=>i.name) });
        }

        if (toRemove.length > 0) {
            await api('bring_remove', {}, 'POST', { items: toRemove.map(n => ({ name: n })), listUUID });
            logOperation('bg_bring_remove', { removed: toRemove });
        }

        // Update urgency badge on dashboard without re-rendering anything visible
        _updateSmartUrgencyBadge();

        // If shopping page is open, re-render it with fresh data
        if (_currentPageId === 'shopping') {
            _syncOnBringFlags();
            _syncTagsFromBringSpec();
            renderSmartShopping();
            renderShoppingItems();
            updateShoppingTabCounts();
        }

    } catch (e) { /* silent — best effort */ }
}

