(function () {
    function debounce(fn, delay) {
        let timer = null;
        return function (...args) {
            window.clearTimeout(timer);
            timer = window.setTimeout(() => fn.apply(this, args), delay);
        };
    }

    function escapeHtml(value) {
        return String(value ?? '')
            .replace(/&/g, '&amp;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;')
            .replace(/"/g, '&quot;')
            .replace(/'/g, '&#039;');
    }

    function formatDueDate(unixValue) {
        const value = Number(unixValue || 0);
        if (!value) return '';
        const date = new Date(value * 1000);
        const pad = (n) => String(n).padStart(2, '0');
        return `${pad(date.getDate())}-${pad(date.getMonth() + 1)}-${date.getFullYear()}`;
    }

    function formatManufactureDate(value) {
        const clean = String(value || '').trim();
        if (!clean) return '';
        if (/^\d{10}$/.test(clean)) {
            return formatDueDate(Number(clean));
        }
        return clean;
    }

    function getStatusFromDue(unixValue) {
        const now = Math.floor(Date.now() / 1000);
        const value = Number(unixValue || 0);
        if (!value) return 'unknown';
        if (value < now) return 'overdue';
        if (value < now + (30 * 24 * 3600)) return 'pending';
        return 'not_due';
    }

    function formatAssetCondition(value) {
        const clean = String(value || '').trim().toUpperCase();
        const labels = {
            A: 'As New',
            B: 'In use. Used',
            C: 'In use. Tired',
            D: 'In storage. Ready to use',
            E: 'In storage. Attention Needed',
            F: 'Disregarded',
            G: 'Decommissioned'
        };

        if (!clean || clean === '0') return '-';
        return labels[clean] || clean;
    }

    const DESKTOP_COLUMN_ORDER = [
        'itemnumber',
        'description',
        'manufacturer',
        'make_model',
        'condition',
        'plantnumber',
        'serialnumber',
        'locname',
        'subloc',
        'postcode',
        'lastReport',
        'duedate'
    ];

    const STATUS_FILTER_ORDER = ['overdue', 'pending', 'not_due'];
    const STATUS_FILTER_META = {
        overdue: { label: 'Overdue', className: 'danger-color-dark text-light' },
        pending: { label: 'Pending', className: 'warning-color-dark text-light' },
        not_due: { label: 'Not Due', className: 'success-color-dark text-light' }
    };

    const PLANT_ROW_COLOURS = {
        rose: { bg: '#fce4ec', text: '#6a1b3a' },
        red: { bg: '#fdecea', text: '#7f1d1d' },
        orange: { bg: '#fff3e0', text: '#8a3b12' },
        amber: { bg: '#fff8e1', text: '#7a5600' },
        yellow: { bg: '#fffde7', text: '#6b5b00' },
        lime: { bg: '#f1f8e9', text: '#466a12' },
        green: { bg: '#e8f5e9', text: '#1b5e20' },
        mint: { bg: '#e6f7f1', text: '#0f5c4d' },
        cyan: { bg: '#e0f7fa', text: '#155e75' },
        blue: { bg: '#e3f2fd', text: '#0d47a1' },
        indigo: { bg: '#e8eaf6', text: '#283593' },
        purple: { bg: '#f3e5f5', text: '#6a1b9a' }
    };

    function getPlantRowStyle(rowColour) {
        const key = String(rowColour || '').trim().toLowerCase();
        const cfg = PLANT_ROW_COLOURS[key] || null;
        if (!cfg) return { className: '', style: '' };
        return {
            className: ' has-plant-row-colour',
            style: ` style="--plant-row-bg:${cfg.bg};--plant-row-text:${cfg.text};"`
        };
    }

    function getWsBadgeClass(renewal) {
        const value = Number(renewal || 0);
        const now = Math.floor(Date.now() / 1000);

        if (!value) return 'bg-dark';
        if (value < now) return 'danger-color-dark';
        if (value < now + (30 * 24 * 3600)) return 'warning-color-dark';
        return 'success-color-dark';
    }

    function renderSchemeBadges(row, config) {
        const badges = [];

        if (Number(row.ws_id || 0) > 0 && String(row.ws_no || '').trim() !== '') {
            const wsNo = escapeHtml(row.ws_no);
            const wsClass = getWsBadgeClass(row.ws_renewal);
            badges.push(`
<a href="javascript:void(0)"
   onclick="editWSC('${escapeHtml(row.ws_id)}')"
   class="badge rounded-pill text-light ${wsClass} asset-scheme-badge"
   data-mdb-tooltip-init data-mdb-placement="right" data-mdb-trigger="hover"
   title="This item is part of ${wsNo}">
   <small><i class="fad fa-cog"></i> ${wsNo}</small>
</a>`);
        }

        if (Number(row.lws_id || 0) > 0 && String(row.lws_no || '').trim() !== '') {
            const lwsNo = escapeHtml(row.lws_no);
            badges.push(`
<a href="javascript:void(0)"
   onclick="editLWSC('${escapeHtml(row.lws_id)}')"
   class="badge rounded-pill text-dark bg-warning asset-scheme-badge"
   data-mdb-tooltip-init data-mdb-placement="right" data-mdb-trigger="hover"
   title="This item is part of ${lwsNo}">
   <small><i class="fad fa-cog"></i> ${lwsNo}</small>
</a>`);
        }

        if (Number(row.puwer_set_id || 0) > 0) {
            const title = escapeHtml(row.puwer_set_title || 'PUWER');
            const puwerUrl =
                `${config.rootUrl}/includes/puwer/puwer_set_manage.php` +
                `?clientnumber=${encodeURIComponent(row.clientnumber)}` +
                `&set_id=${encodeURIComponent(row.puwer_set_id)}` +
                `&tenant=${encodeURIComponent(config.tenant)}`;

            badges.push(`
<a href="javascript:void(0)"
   onclick="loadModalContent('${puwerUrl}', 'popupBox', 'modal-xl', 1060, 'true', 'Manage PUWER Set')"
   class="badge rounded-pill text-light asset-scheme-badge asset-puwer-badge"
   data-mdb-tooltip-init data-mdb-placement="right" data-mdb-trigger="hover"
   title="This item is part of PUWER set ${title}">
   <small>PUWER</small>
</a>`);
        }

        return badges.join('');
    }

    function renderItemNumberHtml(row, config) {
        const itemnumber = escapeHtml(row.itemnumber);
        const badges = renderSchemeBadges(row, config);

        return `
<div class="asset-itemnumber-wrap">
    <span class="asset-itemnumber-text">${itemnumber}</span>
    ${badges ? `<span class="asset-itemnumber-badges">${badges}</span>` : ''}
</div>`;
    }

    function renderDescriptionHtml(row) {
        const rawDescription = String(row.description || '').trim();
        const description = escapeHtml(rawDescription);
        const popoverTitle = `${rawDescription || 'Asset'} Information`;
        const detailRows = [
            ['Model', row.model || ''],
            ['Manufacturer', row.manf || ''],
            ['Date of Manufacture', formatManufactureDate(row.dateofmanf)],
            ['SWL', row.swl || '']
        ].filter((entry) => String(entry[1] || '').trim() !== '');

        const popoverContent = detailRows.length
            ? detailRows.map(([label, value]) => `<div><strong>${escapeHtml(label)}:</strong> ${escapeHtml(value)}</div>`).join('')
            : '<div>No additional details recorded.</div>';
        const frequency = String(row.frequency || '').trim();

        return `
<div class="asset-description-wrap">
    <span class="asset-description-text"
          tabindex="0"
          data-asset-inline-filter="${description}"
          data-mdb-popover-init
          data-mdb-trigger="hover focus"
          data-mdb-placement="right"
          data-mdb-html="true"
          title="${escapeHtml(popoverTitle)}"
          data-mdb-content="${escapeHtml(popoverContent)}">${description || '<span class="text-muted">No description</span>'}</span>
    ${frequency ? `<span class="badge unique-color-dark text-light asset-period-badge">${escapeHtml(frequency)}</span>` : ''}
</div>`;
    }

    function renderLocationPopoverHtml(row, value, titlePrefix) {
        const cleanValue = String(value || '').trim();
        const safeValue = escapeHtml(cleanValue);
        const addressParts = [
            row.locname || '',
            row.locaddress1 || '',
            row.locaddress2 || '',
            row.locaddress3 || '',
            row.locaddress4 || '',
            row.postcode || ''
        ].map((part) => String(part || '').trim()).filter(Boolean);

        const content = addressParts.length
            ? addressParts.map((part) => `<div>${escapeHtml(part)}</div>`).join('')
            : '<div>No address recorded.</div>';
        const title = `${titlePrefix || 'Site'} Address`;

        return `
<span class="asset-location-popover"
      tabindex="0"
      data-asset-inline-filter="${safeValue}"
      data-mdb-popover-init
      data-mdb-trigger="hover focus"
      data-mdb-placement="right"
      data-mdb-html="true"
      title="${escapeHtml(title)}"
      data-mdb-content="${escapeHtml(content)}">${cleanValue ? safeValue : '<span class="text-muted">Not set</span>'}</span>`;
    }

    function renderChainButtonHtml(row, config, extraClass) {
        const linkedStr = String(row.linked || '');
        if (!linkedStr) return '';

        const parts = linkedStr.split(':');
        const lcount = parseInt(parts[0], 10) || 0;
        if (lcount < 1) return '';

        const client = row.clientnumber;
        const item = row.itemnumber;
        const ldesc = escapeHtml(parts[2] || `Chain C${lcount}`);
        const classes = `btn btn-secondary btn-sm noprint d-inline-flex align-items-center gap-1 ${extraClass || ''}`.trim();

        return `
<a class="${classes}"
   href="index.php?t=includes/master&p=linked.php&clientnumber=${client}&itemnumber=${item}"
   data-mdb-tooltip-init data-mdb-trigger="hover" data-mdb-placement="top"
   title="${ldesc}" aria-label="Open chain C${lcount}">
  <i class="fad fa-link"></i><span>C${lcount}</span>
</a>`;
    }

    function createActionHtml(row, config) {
        const client = row.clientnumber;
        const item = row.itemnumber;
        const loc = row.locnumber;
        const description = escapeHtml(row.description || '');
        const archiveMode = config.filter === 'archive' ? 'activate' : 'archive';
        const archiveIcon = config.filter === 'archive' ? 'fad fa-upload' : 'fad fa-archive';
        const archiveTitle = config.filter === 'archive' ? 'Reinstate' : 'Archive';
        const tooltipAttrs = 'data-mdb-tooltip-init data-mdb-trigger="hover" data-mdb-placement="top"';

        const chainBtn = renderChainButtonHtml(row, config);

        const locNoteBtn = Number(row.has_loc_notes || 0) === 1 ? `
<a href="javascript:void(0)" class="btn btn-secondary btn-sm noprint"
   ${tooltipAttrs} title="Location notes detected"
   onclick="loadModalContent('${config.rootUrl}/includes/master/notes.php?type=locations&locnumber=${loc}&tenant=${config.tenant}','locationBox','modal-md',1060,'true','Location Notes','close')">
   <i class="fad fa-location-question"></i>
</a>` : '';

        const itemNoteBtn = Number(row.has_item_notes || 0) === 1 ? `
<a href="javascript:void(0)" class="btn unique-color-dark btn-sm noprint"
   ${tooltipAttrs} title="Item notes detected"
   onclick="loadModalContent('${config.rootUrl}/includes/master/notes.php?type=itemdetails&itemnumber=${item}&tenant=${config.tenant}','notesBox','modal-md',1060,'true','Item Notes','close')">
   <i class="fad fa-file-exclamation"></i>
</a>` : '';

        return `
<div class="asset-action-wrap" style="white-space:nowrap;">
  ${locNoteBtn}
  ${chainBtn}
  <a href="javascript:void(0)" class="btn btn-info btn-sm formButton"
     ${tooltipAttrs} title="New Report" data-values="${client}|${loc}|${item}" data-href="${config.rootUrl}/includes/master/form.php">
     <i class="fad fa-file-alt"></i>
  </a>
  <a href="index.php?t=includes/master&p=items.php&data=${client}|${loc}|${item}&view=list"
     class="btn btn-secondary btn-sm" ${tooltipAttrs} title="Completed Reports">
     <i class="fad fa-file-contract"></i>
  </a>
  <a href="javascript:void(0)" class="btn btn-secondary btn-sm transferButton"
     ${tooltipAttrs} title="Move" data-values="${client}|${loc}|${item}|${description}" data-href="${config.rootUrl}/includes/master/transfer.php">
     <i class="fad fa-exchange-alt"></i>
  </a>
  <a href="javascript:void(0)" class="btn btn-secondary btn-sm cloneButton"
     ${tooltipAttrs} title="Clone Item" data-values="${client}|${loc}|${item}|${description}" data-href="${config.rootUrl}/includes/master/clone.php">
     <i class="fad fa-clone"></i>
  </a>
  <a href="javascript:void(0)" class="btn btn-secondary btn-sm archiveButton"
     ${tooltipAttrs} title="${archiveTitle}" data-values="${client}|${loc}|${item}|${archiveMode}" data-href="${config.rootUrl}/includes/master/archive.php">
     <i class="${archiveIcon}"></i>
  </a>
  <a href="index.php?t=includes/master&p=asset.php&view=edit_attached&data=${client}|${loc}|${item}"
     class="btn btn-secondary btn-sm" ${tooltipAttrs} title="Edit">
     <i class="fad fa-info-circle"></i>
  </a>
  ${itemNoteBtn}
</div>`;
    }

    class AssetGrid {
        constructor(config) {
            this.config = Object.assign({
                endpoint: '',
                rootUrl: '',
                tenant: '',
                clientNumber: 0,
                status: 1,
                filter: 'live'
            }, config || {});

            this.defaultState = {
                page: 1,
                perPage: 25,
                search: '',
                sortField: 'duedate',
                sortDir: 'asc',
                statusFilter: ''
            };
            this.state = Object.assign({}, this.defaultState);

            this.rows = [];
            this.total = 0;
            this.filtered = 0;
            this.selectedRows = new Set();
            this.isLoading = false;
            this.loadingMode = '';
            this.queuedLoadAll = false;
            this.queuedPageLoad = false;
            this.reloadAfterPageClamp = false;
            window.getSelectedAssetItems = () => this.getSelectedItemArray().join(',');

            this.cacheElements();
            this.restoreState();
            this.applyStateToControls();
            this.bindEvents();
            this.renderSortIndicators();
            this.bindLazyUiHelpers();
            this.bindViewportRender();
            if (this.currentPageSizeValue() === '-1') {
                this.loadAllRows();
            } else {
                this.load();
            }
        }

        cacheElements() {
            this.desktopBody = document.getElementById('assetGridBody');
            this.mobileBody = document.getElementById('assetMobileBody');
            this.desktopSearch = document.getElementById('assetDesktopSearch');
            this.mobileSearch = document.getElementById('assetQuickSearch');
            this.pageSize = document.getElementById('assetGridPageSize');
            this.mobilePageSize = document.getElementById('assetGridPageSizeMobile');
            this.prevBtn = document.getElementById('assetGridPrev');
            this.nextBtn = document.getElementById('assetGridNext');
            this.pageInfo = document.getElementById('assetGridPageInfo');
            this.summary = document.getElementById('assetGridSummary');
            this.mobileSummary = document.getElementById('assetGridSummaryMobile');
            this.processing = document.getElementById('assetGridProcessing');
            this.allProgress = document.getElementById('assetAllProgress');
            this.allProgressText = document.getElementById('assetAllProgressText');
            this.allProgressBar = document.getElementById('assetAllProgressBar');
            this.selectedItems = document.getElementById('selectedItems');
            this.mainToolbar = document.getElementById('mainToolbar');
            this.massToolbar = document.getElementById('massToolbar');
            this.mobileActionBody = document.getElementById('assetActionsCanvasBody');
            this.activeFilterHosts = Array.from(document.querySelectorAll('[data-asset-active-filters]'));
            this.overlay = document.getElementById('loadingOverlay');
            this.container = document.getElementById('assetTableContainer');
            this.isDesktopLayout = this.isDesktopViewport();
        }

        bindEvents() {
            const search = debounce((value) => {
                this.state.search = value;
                this.state.page = 1;
                this.syncSearchControls(value);
                this.saveState();
                this.requestLoad();
            }, 300);

            if (this.desktopSearch) this.desktopSearch.addEventListener('input', (e) => search(e.target.value));
            if (this.mobileSearch) this.mobileSearch.addEventListener('input', (e) => search(e.target.value));

            document.querySelectorAll('[data-asset-clear-search]').forEach((btn) => {
                btn.addEventListener('click', () => {
                    if (this.isInteractionLocked()) return;
                    this.resetFilters();
                });
            });

            document.addEventListener('click', (event) => {
                const clearBtn = event.target.closest('[data-asset-clear-filter]');
                if (!clearBtn) return;
                event.preventDefault();
                event.stopPropagation();
                if (this.isInteractionLocked()) return;
                this.clearFilter(clearBtn.getAttribute('data-asset-clear-filter') || '');
            }, true);

            document.querySelectorAll('[data-asset-status-filter]').forEach((btn) => {
                btn.addEventListener('click', () => {
                    if (this.isInteractionLocked()) return;
                    this.setStatusFilter(btn.getAttribute('data-asset-status-filter') || '');
                    this.state.page = 1;
                    this.saveState();
                    this.load();
                });
            });

            document.querySelectorAll('[data-sort-field]').forEach((th) => {
                th.addEventListener('click', () => {
                    this.applySortField(th.getAttribute('data-sort-field') || '');
                });
            });

            if (this.pageSize) {
                if (!this.previousPageSize) {
                    this.previousPageSize = String(this.pageSize.value || this.defaultState.perPage);
                }
                this.pageSize.addEventListener('change', () => {
                    this.handlePageSizeChange(String(this.pageSize.value || this.defaultState.perPage), this.pageSize);
                });
            }
            if (this.mobilePageSize) {
                this.mobilePageSize.addEventListener('change', () => {
                    this.handlePageSizeChange(String(this.mobilePageSize.value || this.defaultState.perPage), this.mobilePageSize);
                });
            }

            if (this.prevBtn) this.prevBtn.addEventListener('click', () => this.changePage(-1));
            if (this.nextBtn) this.nextBtn.addEventListener('click', () => this.changePage(1));

            const selectVisible = document.getElementById('assetGridSelectVisible');
            const clearVisible = document.getElementById('assetGridClearVisible');
            const selectVisibleMobile = document.getElementById('assetGridSelectVisibleMobile');
            const clearVisibleMobile = document.getElementById('assetGridClearVisibleMobile');
            if (selectVisible) selectVisible.addEventListener('click', () => this.setCurrentRowsSelected(true));
            if (clearVisible) clearVisible.addEventListener('click', () => this.setCurrentRowsSelected(false));
            if (selectVisibleMobile) selectVisibleMobile.addEventListener('click', () => this.setCurrentRowsSelected(true));
            if (clearVisibleMobile) clearVisibleMobile.addEventListener('click', () => this.setCurrentRowsSelected(false));

            document.addEventListener('change', (event) => {
                const checkbox = event.target.closest('.asset-row-select');
                if (!checkbox) return;
                const itemnumber = String(checkbox.getAttribute('data-itemnumber') || '');
                if (!itemnumber) return;
                if (checkbox.checked) {
                    this.selectedRows.add(itemnumber);
                } else {
                    this.selectedRows.delete(itemnumber);
                }
                this.syncSelectionUi();
                this.renderSelectionState();
                this.syncMobileActionPanel();
            });

            document.addEventListener('click', (event) => {
                const mobileSortBtn = event.target.closest('[data-asset-mobile-sort]');
                if (mobileSortBtn) {
                    event.preventDefault();
                    this.applySortField(mobileSortBtn.getAttribute('data-asset-mobile-sort') || '');
                    return;
                }

                const filterLink = event.target.closest('[data-asset-inline-filter]');
                if (filterLink) {
                    event.preventDefault();
                    if (this.isInteractionLocked()) return;
                    this.applyInlineSearch(filterLink.getAttribute('data-asset-inline-filter') || '');
                    return;
                }

                const modalBtn = event.target.closest('[data-asset-mobile-modal]');
                if (!modalBtn) return;
                event.preventDefault();
                this.openMobileModal(
                    modalBtn.getAttribute('data-asset-mobile-modal'),
                    modalBtn.getAttribute('data-itemnumber')
                );
            });

            document.addEventListener('keydown', (event) => {
                if (!['Enter', ' '].includes(event.key)) return;
                const filterLink = event.target.closest('[data-asset-inline-filter]');
                if (!filterLink) return;
                event.preventDefault();
                if (this.isInteractionLocked()) return;
                this.applyInlineSearch(filterLink.getAttribute('data-asset-inline-filter') || '');
            });

            const dismissFloatingHelpers = () => this.dismissFloatingHelpers();
            window.addEventListener('scroll', dismissFloatingHelpers, { passive: true, capture: true });
            window.addEventListener('wheel', dismissFloatingHelpers, { passive: true });
            window.addEventListener('touchmove', dismissFloatingHelpers, { passive: true });
        }

        isDesktopViewport() {
            return !window.matchMedia || window.matchMedia('(min-width: 1200px)').matches;
        }

        bindViewportRender() {
            if (!window.matchMedia) return;
            const media = window.matchMedia('(min-width: 1200px)');
            const handleChange = () => {
                const isDesktop = this.isDesktopViewport();
                if (this.isDesktopLayout === isDesktop) return;
                this.isDesktopLayout = isDesktop;
                this.renderRows();
            };

            if (typeof media.addEventListener === 'function') {
                media.addEventListener('change', handleChange);
            } else if (typeof media.addListener === 'function') {
                media.addListener(handleChange);
            }
        }

        bindLazyUiHelpers() {
            const ensureTooltip = (el) => {
                if (!el || !window.mdb || !mdb.Tooltip || mdb.Tooltip.getInstance(el)) return;
                new mdb.Tooltip(el);
            };
            const ensurePopover = (el) => {
                if (!el || !window.mdb || !mdb.Popover || mdb.Popover.getInstance(el)) return;
                new mdb.Popover(el);
            };

            document.addEventListener('mouseover', (event) => {
                ensureTooltip(event.target.closest('[data-mdb-tooltip-init]'));
                ensurePopover(event.target.closest('[data-mdb-popover-init]'));
            });

            document.addEventListener('focusin', (event) => {
                ensureTooltip(event.target.closest('[data-mdb-tooltip-init]'));
                ensurePopover(event.target.closest('[data-mdb-popover-init]'));
            });
        }

        dismissFloatingHelpers() {
            if (window.mdb && mdb.Popover) {
                document.querySelectorAll('[data-mdb-popover-init]').forEach((el) => {
                    const instance = mdb.Popover.getInstance(el);
                    if (instance && typeof instance.hide === 'function') {
                        instance.hide();
                    }
                });
            }

            if (window.mdb && mdb.Tooltip) {
                document.querySelectorAll('[data-mdb-tooltip-init]').forEach((el) => {
                    const instance = mdb.Tooltip.getInstance(el);
                    if (instance && typeof instance.hide === 'function') {
                        instance.hide();
                    }
                });
            }

            document.querySelectorAll('.popover.show, .tooltip.show').forEach((el) => el.remove());
        }

        storageKey() {
            return String(this.config.stateKey || '');
        }

        stateVersion() {
            return Number(this.config.stateVersion || 1);
        }

        normaliseSavedState(saved) {
            const source = saved && typeof saved === 'object' ? saved : {};
            const rawState = source.state && typeof source.state === 'object' ? source.state : source;
            const state = Object.assign({}, this.defaultState);
            const rowOptions = ['10', '25', '50', '100', '250'];

            state.page = Math.max(1, parseInt(rawState.page, 10) || this.defaultState.page);
            state.search = String(rawState.search || '');
            state.sortField = this.getSortableFields().includes(rawState.sortField) ? rawState.sortField : this.defaultState.sortField;
            state.sortDir = String(rawState.sortDir || '').toLowerCase() === 'desc' ? 'desc' : 'asc';
            state.statusFilter = this.normaliseStatusFilter(rawState.statusFilter);

            let pageSizeValue = String(source.pageSizeValue || rawState.pageSizeValue || rawState.perPage || this.defaultState.perPage);
            const savedPreviousPageSize = rowOptions.includes(String(source.previousPageSize || ''))
                ? String(source.previousPageSize)
                : String(this.defaultState.perPage);

            if (pageSizeValue === '-1') {
                pageSizeValue = savedPreviousPageSize;
                state.page = 1;
                state.perPage = parseInt(pageSizeValue, 10) || this.defaultState.perPage;
            } else {
                if (!rowOptions.includes(pageSizeValue)) {
                    pageSizeValue = String(this.defaultState.perPage);
                }
                state.perPage = parseInt(pageSizeValue, 10);
            }

            const previousPageSize = rowOptions.includes(pageSizeValue)
                ? pageSizeValue
                : savedPreviousPageSize;

            return { state, pageSizeValue, previousPageSize };
        }

        restoreState() {
            const key = this.storageKey();
            if (!key || !window.localStorage) return;

            try {
                const raw = window.localStorage.getItem(key);
                if (!raw) return;

                const saved = JSON.parse(raw);
                if (!saved || Number(saved.version || 0) !== this.stateVersion()) {
                    window.localStorage.removeItem(key);
                    return;
                }

                const restored = this.normaliseSavedState(saved);
                this.state = restored.state;
                this.restoredPageSizeValue = restored.pageSizeValue;
                this.previousPageSize = restored.previousPageSize;
            } catch (error) {
                window.localStorage.removeItem(key);
            }
        }

        saveState() {
            const key = this.storageKey();
            if (!key || !window.localStorage) return;

            try {
                let pageSizeValue = this.currentPageSizeValue();
                const rowOptions = ['10', '25', '50', '100', '250'];
                const previousPageSize = rowOptions.includes(String(this.previousPageSize || ''))
                    ? String(this.previousPageSize)
                    : String(this.defaultState.perPage);
                const state = Object.assign({}, this.state);

                if (pageSizeValue === '-1') {
                    pageSizeValue = previousPageSize;
                    state.page = 1;
                }
                state.perPage = parseInt(pageSizeValue, 10) || this.defaultState.perPage;

                window.localStorage.setItem(key, JSON.stringify({
                    version: this.stateVersion(),
                    state,
                    pageSizeValue,
                    previousPageSize
                }));
            } catch (error) {
                // Storage can be unavailable in private browsing; the grid still works without it.
            }
        }

        clearSavedState() {
            const key = this.storageKey();
            if (!key || !window.localStorage) return;

            try {
                window.localStorage.removeItem(key);
            } catch (error) {
                // Ignore storage failures; reset should still update the live grid state.
            }
        }

        applyStateToControls() {
            const pageSizeValue = this.restoredPageSizeValue || String(this.state.perPage || this.defaultState.perPage);
            this.previousPageSize = this.previousPageSize || (pageSizeValue === '-1' ? String(this.defaultState.perPage) : pageSizeValue);
            this.syncSearchControls(this.state.search);
            this.syncPageSizeControls(pageSizeValue);
            this.renderActiveFilters();
        }

        handlePageSizeChange(value, selectEl) {
            if (this.isInteractionLocked()) {
                this.syncPageSizeControls(this.currentPageSizeValue());
                return;
            }

            if (value === '-1') {
                if (selectEl) selectEl.value = this.previousPageSize || '25';
                this.syncPageSizeControls(this.previousPageSize || '25');
                this.openLoadAllConfirm();
                return;
            }

            this.previousPageSize = value;
            this.state.perPage = Number(value);
            this.state.page = 1;
            this.syncPageSizeControls(value);
            this.saveState();
            this.load();
        }

        currentPageSizeValue() {
            if ((this.pageSize && this.pageSize.value === '-1') || (this.mobilePageSize && this.mobilePageSize.value === '-1')) {
                return '-1';
            }
            return this.previousPageSize || String(this.state.perPage || 25);
        }

        syncPageSizeControls(value) {
            if (this.pageSize) this.pageSize.value = value;
            if (this.mobilePageSize) this.mobilePageSize.value = value;
        }

        syncSearchControls(value) {
            if (this.desktopSearch && this.desktopSearch.value !== value) this.desktopSearch.value = value;
            if (this.mobileSearch && this.mobileSearch.value !== value) this.mobileSearch.value = value;
            this.renderActiveFilters();
        }

        normaliseStatusFilter(value) {
            const source = Array.isArray(value) ? String(value[0] || '') : String(value || '');
            return STATUS_FILTER_ORDER.includes(source) ? source : '';
        }

        statusFiltersParam() {
            return this.normaliseStatusFilter(this.state.statusFilter);
        }

        setStatusFilter(status) {
            if (!STATUS_FILTER_ORDER.includes(status)) return;
            this.state.statusFilter = this.normaliseStatusFilter(this.state.statusFilter) === status ? '' : status;
            this.renderActiveFilters();
        }

        clearFilter(type) {
            if (type === 'search') {
                this.state.search = '';
                this.state.page = 1;
                this.syncSearchControls('');
            } else if (type === 'sort') {
                this.state.sortField = this.defaultState.sortField;
                this.state.sortDir = this.defaultState.sortDir;
                this.state.page = 1;
                this.renderSortIndicators();
            } else if (type === 'status') {
                this.state.statusFilter = '';
                this.state.page = 1;
            } else {
                return;
            }

            this.renderActiveFilters();
            this.saveState();
            this.load();
        }

        renderActiveFilters() {
            const search = String(this.state.search || '').trim();
            const statusFilter = this.normaliseStatusFilter(this.state.statusFilter);
            const hasSort = this.state.sortField && this.state.sortDir &&
                (this.state.sortField !== this.defaultState.sortField || this.state.sortDir !== this.defaultState.sortDir);

            document.querySelectorAll('[data-asset-status-filter]').forEach((btn) => {
                const status = btn.getAttribute('data-asset-status-filter') || '';
                const active = statusFilter === status;
                btn.classList.toggle('asset-status-filter-active', active);
                btn.setAttribute('aria-pressed', active ? 'true' : 'false');
            });

            if (!this.activeFilterHosts || !this.activeFilterHosts.length) return;

            const labels = (this.config && this.config.labels) ? this.config.labels : {};
            const sortLabel = labels[this.state.sortField] || this.state.sortField || '';
            const sortDir = String(this.state.sortDir || '').toLowerCase() === 'desc' ? 'desc' : 'asc';
            const chips = [
                search ? this.renderActiveFilterChip('search', 'Search', search, 'asset-active-filter-search') : '',
                hasSort && sortLabel ? this.renderActiveFilterChip('sort', 'Sort', `${sortLabel} ${sortDir}`, 'asset-active-filter-sort') : '',
                statusFilter ? this.renderActiveFilterChip('status', 'Status', STATUS_FILTER_META[statusFilter].label, `asset-active-filter-status asset-active-filter-status-${statusFilter}`) : ''
            ].join('');

            this.activeFilterHosts.forEach((host) => {
                host.innerHTML = chips;
                host.setAttribute('aria-label', chips ? 'Active filters' : '');
            });
        }

        renderActiveFilterChip(type, label, value, className) {
            const safeLabel = escapeHtml(label);
            const safeValue = escapeHtml(value);
            return `
<span class="asset-active-filter-chip ${className}">
    <span class="asset-active-filter-label">${safeLabel}</span>
    <span class="asset-active-filter-value">${safeValue}</span>
    <button type="button" class="asset-active-filter-clear" data-asset-clear-filter="${escapeHtml(type)}" aria-label="Clear ${safeLabel}">
        <i class="fad fa-times"></i>
    </button>
</span>`;
        }

        effectiveSearchValue() {
            const search = String(this.state.search || '').trim();
            return search.length >= 2 ? search : '';
        }

        changePage(delta) {
            if (this.isInteractionLocked()) return;
            const maxPage = Math.max(1, Math.ceil(this.filtered / this.state.perPage));
            this.state.page = Math.max(1, Math.min(maxPage, this.state.page + delta));
            this.saveState();
            this.load();
        }

        applySortField(field) {
            if (this.isInteractionLocked()) return;
            if (!this.getSortableFields().includes(field)) return;

            if (this.state.sortField === field) {
                this.state.sortDir = this.state.sortDir === 'asc' ? 'desc' : 'asc';
            } else {
                this.state.sortField = field;
                this.state.sortDir = 'asc';
            }

            this.state.page = 1;
            this.renderSortIndicators();
            this.renderActiveFilters();
            this.syncMobileActionPanel();
            this.saveState();
            this.load();
        }

        buildRequestParams(extra) {
            return Object.assign({
                tenant: this.config.tenant,
                clientnumber: String(this.config.clientNumber),
                status: String(this.config.status),
                page: String(this.state.page),
                per_page: String(this.state.perPage),
                search: this.effectiveSearchValue(),
                sort_field: this.state.sortField,
                sort_dir: this.state.sortDir,
                status_filter: this.statusFiltersParam()
            }, extra || {});
        }

        requestLoad() {
            if (this.isInteractionLocked()) {
                this.queuedPageLoad = true;
                return;
            }

            this.load();
        }

        renderSortIndicators() {
            document.querySelectorAll('#assetGridTable th[data-sort-field]').forEach((th) => {
                const field = th.getAttribute('data-sort-field');
                const active = field === this.state.sortField;
                th.classList.toggle('asset-sort-active', active);
                th.classList.toggle('asset-sort-asc', active && this.state.sortDir === 'asc');
                th.classList.toggle('asset-sort-desc', active && this.state.sortDir === 'desc');
                th.setAttribute('aria-sort', active ? (this.state.sortDir === 'asc' ? 'ascending' : 'descending') : 'none');
            });
        }

        async load() {
            if (!this.beginLoad('page')) return;

            try {
                const response = await fetch(this.config.endpoint, {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                    body: new URLSearchParams(this.buildRequestParams())
                });
                const payload = await response.json();
                if (!payload.success) {
                    throw new Error(payload.message || 'Unable to load assets');
                }
                this.rows = Array.isArray(payload.data) ? payload.data : (payload.rows || []);
                this.total = Number(payload.recordsTotal ?? payload.total ?? 0);
                this.filtered = Number(payload.recordsFiltered ?? payload.filtered ?? 0);
                const maxPage = Math.max(1, Math.ceil(this.filtered / this.state.perPage));
                if (this.state.page > maxPage) {
                    this.state.page = maxPage;
                    this.saveState();
                    this.reloadAfterPageClamp = true;
                    return;
                }
                this.renderRows();
                this.updatePager();
            } catch (error) {
                console.error(error);
                if (typeof showResponse === 'function') {
                    showResponse('danger', 'Unable to load the asset schedule.');
                }
            } finally {
                this.endLoad();
                if (this.overlay) this.overlay.style.display = 'none';
                if (this.container) this.container.style.visibility = 'visible';
                if (this.reloadAfterPageClamp) {
                    this.reloadAfterPageClamp = false;
                    window.setTimeout(() => this.load(), 0);
                }
            }
        }

        openLoadAllConfirm() {
            if (this.isInteractionLocked()) return;

            const count = Number(this.filtered || this.config.scheduleItemCount || 0);
            const url = `${this.config.rootUrl}/includes/asset/asset_all_rows_confirm.php?count=${encodeURIComponent(count)}&tenant=${encodeURIComponent(this.config.tenant)}`;

            if (typeof loadModalContent === 'function') {
                loadModalContent(url, 'popupBox', 'modal-md', 1060, 'true', 'Load all rows', 'cancelConfirm', () => this.confirmLoadAllRows());
                return;
            }

            if (window.confirm('Load all matching assets? This may take some time for large schedules.')) {
                this.loadAllRows();
            }
        }

        confirmLoadAllRows() {
            if (typeof killModal === 'function') {
                killModal('popupBox');
            }

            if (this.isInteractionLocked()) {
                if (this.loadingMode !== 'all') this.queuedLoadAll = true;
                return;
            }

            window.setTimeout(() => this.loadAllRows(), 0);
        }

        openMobileModal(mode, itemnumber) {
            const cleanMode = mode === 'actions' ? 'actions' : 'details';
            const cleanItem = String(itemnumber || '').replace(/\D+/g, '');
            if (!cleanItem || typeof loadModalContent !== 'function') return;

            const title = cleanMode === 'actions' ? 'Asset Actions' : 'Asset Details';
            const url =
                `${this.config.rootUrl}/includes/asset/asset_mobile_modal.php` +
                `?mode=${encodeURIComponent(cleanMode)}` +
                `&clientnumber=${encodeURIComponent(this.config.clientNumber)}` +
                `&itemnumber=${encodeURIComponent(cleanItem)}` +
                `&filter=${encodeURIComponent(this.config.filter)}` +
                `&tenant=${encodeURIComponent(this.config.tenant)}`;

            loadModalContent(url, cleanMode === 'actions' ? 'assetActionBox' : 'popupBox', 'modal-md', 1060, 'true', title, 'close');
        }

        async loadAllRows() {
            if (!this.beginLoad('all')) return;

            const chunkSize = Number(this.config.allRowsChunkSize || 250);
            const allRows = [];
            let offset = 0;
            let total = 0;
            let hasMore = true;

            this.setAllProgress(true, 0, 0);

            try {
                while (hasMore) {
                    const response = await fetch(this.config.endpoint, {
                        method: 'POST',
                        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
                        body: new URLSearchParams(this.buildRequestParams({
                            mode: 'all',
                            offset: String(offset),
                            limit: String(chunkSize)
                        }))
                    });

                    const payload = await response.json();
                    if (!payload.success) {
                        throw new Error(payload.message || 'Unable to load all assets');
                    }

                    const chunk = Array.isArray(payload.data) ? payload.data : (payload.rows || []);
                    total = Number(payload.recordsFiltered ?? payload.filtered ?? total);
                    allRows.push(...chunk);
                    offset += chunk.length;
                    hasMore = Boolean(payload.hasMore) && chunk.length > 0;
                    this.setAllProgress(true, allRows.length, total);
                }

                this.rows = allRows;
                this.filtered = total || allRows.length;
                this.total = Math.max(this.total, Number(this.filtered || 0));
                this.state.page = 1;
                this.state.perPage = Math.max(1, allRows.length || 1);
                if (this.pageSize) {
                    this.pageSize.value = '-1';
                }
                if (this.mobilePageSize) {
                    this.mobilePageSize.value = '-1';
                }
                this.saveState();
                this.renderRows();
                this.updatePager();

            } catch (error) {
                console.error(error);
                if (typeof showResponse === 'function') {
                    showResponse('danger', 'Unable to load all matching assets.');
                }
            } finally {
                this.endLoad();
                this.setAllProgress(false);
                if (this.overlay) this.overlay.style.display = 'none';
                if (this.container) this.container.style.visibility = 'visible';
            }
        }

        setAllProgress(active, loaded, total) {
            if (!this.allProgress) return;
            this.allProgress.style.display = active ? 'flex' : 'none';

            if (!active) return;

            const cleanLoaded = Number(loaded || 0);
            const cleanTotal = Number(total || 0);
            const percent = cleanTotal > 0 ? Math.min(100, Math.round((cleanLoaded / cleanTotal) * 100)) : 0;

            if (this.allProgressText) {
                this.allProgressText.textContent = cleanTotal > 0 ?
                    `Loading ${cleanLoaded} of ${cleanTotal} matching assets` :
                    `Loading ${cleanLoaded} matching assets`;
            }
            if (this.allProgressBar) {
                this.allProgressBar.style.width = `${percent}%`;
                this.allProgressBar.setAttribute('aria-valuenow', String(percent));
            }
        }

        setProcessing(active) {
            if (this.processing) this.processing.style.display = active ? '' : 'none';
        }

        isInteractionLocked() {
            return Boolean(this.isLoading);
        }

        beginLoad(mode) {
            if (this.isLoading) return false;
            this.isLoading = true;
            this.loadingMode = mode || 'page';
            this.setProcessing(true);
            this.setControlsLocked(true);
            return true;
        }

        endLoad() {
            const shouldRunLoadAll = this.queuedLoadAll && this.loadingMode !== 'all';
            const shouldRunPageLoad = this.queuedPageLoad && !shouldRunLoadAll;
            this.queuedLoadAll = false;
            this.queuedPageLoad = false;
            this.isLoading = false;
            this.loadingMode = '';
            this.setProcessing(false);
            this.setControlsLocked(false);
            this.updatePager();

            if (shouldRunLoadAll) {
                window.setTimeout(() => this.loadAllRows(), 0);
            } else if (shouldRunPageLoad) {
                window.setTimeout(() => this.load(), 0);
            }
        }

        setControlsLocked(active) {
            const selectors = [
                '#assetGridPageSize',
                '#assetGridPageSizeMobile',
                '#assetGridPrev',
                '#assetGridNext',
                '#assetGridSelectVisible',
                '#assetGridClearVisible',
                '[data-asset-clear-search]',
                '[data-asset-status-filter]',
                '[data-asset-mobile-sort]',
                'button[onclick="clearSearch()"]',
                '[data-mdb-target="#assetFiltersCanvas"]'
            ];

            document.querySelectorAll(selectors.join(',')).forEach((el) => {
                el.disabled = active;
                el.classList.toggle('disabled', active);
                el.setAttribute('aria-disabled', active ? 'true' : 'false');
            });

            document.querySelectorAll('#assetGridTable th[data-sort-field]').forEach((th) => {
                th.classList.toggle('asset-sort-disabled', active);
                th.setAttribute('aria-disabled', active ? 'true' : 'false');
            });

            if (this.container) this.container.classList.toggle('asset-grid-loading', active);
        }

        resetFilters() {
            if (this.isInteractionLocked()) return;
            this.clearSavedState();
            this.state = Object.assign({}, this.defaultState);
            this.restoredPageSizeValue = String(this.defaultState.perPage);
            this.previousPageSize = String(this.defaultState.perPage);
            this.applyStateToControls();
            this.renderSortIndicators();
            this.load();
        }

        applyInlineSearch(value) {
            const clean = String(value || '').trim();
            if (!clean) return;

            this.state.search = clean;
            this.state.page = 1;
            this.syncSearchControls(clean);
            this.saveState();
            this.load();
        }

        renderRows() {
            const renderDesktop = this.isDesktopLayout !== false;
            if (this.desktopBody) {
                this.desktopBody.innerHTML = renderDesktop ? this.rows.map((row, index) => this.renderDesktopRow(row, index)).join('') : '';
            }
            if (this.mobileBody) {
                this.mobileBody.innerHTML = renderDesktop ? '' : this.rows.map((row) => this.renderMobileRow(row)).join('');
            }
            this.renderSelectionState();
            this.syncMobileActionPanel();
        }

        renderDesktopRow(row, index) {
            const itemnumber = String(row.itemnumber);
            const checked = this.isItemSelected(itemnumber) ? 'checked' : '';
            const statusClass = this.getStatusClass(getStatusFromDue(row.duedate));
            const statusTitle = this.getStatusTitle(getStatusFromDue(row.duedate));
            const actions = createActionHtml(row, this.config);
            const rowStyle = getPlantRowStyle(row.row_colour);

            return `
<tr data-itemnumber="${escapeHtml(itemnumber)}" class="${index % 2 ? 'asset-row-striped' : ''}${rowStyle.className}"${rowStyle.style}>
    <td class="asset-status-cell status_colour ${statusClass}" title="${escapeHtml(statusTitle)}"><span class="asset-strip ${statusClass}"><span class="visually-hidden">${escapeHtml(statusTitle)}</span></span></td>
    <td><input type="checkbox" class="form-check-input asset-row-select" data-itemnumber="${escapeHtml(itemnumber)}" ${checked}></td>
    ${DESKTOP_COLUMN_ORDER.map((key) => this.renderDesktopDataCell(key, row)).join('')}
    <td class="text-end">${actions}</td>
</tr>`;
        }

        renderDesktopDataCell(key, row) {
            if (!this.isColumnVisible(key)) {
                return '';
            }

            let value = '';
            switch (key) {
                case 'itemnumber':
                    value = renderItemNumberHtml(row, this.config);
                    break;
                case 'description':
                    value = renderDescriptionHtml(row);
                    break;
                case 'manufacturer':
                    value = this.renderInlineFilterLink(row.manf || '');
                    break;
                case 'make_model':
                    value = this.renderInlineFilterLink(row.model || '');
                    break;
                case 'condition':
                    value = formatAssetCondition(row.asset_condition);
                    value = value === '-' ? escapeHtml(value) : this.renderInlineFilterLink(value);
                    break;
                case 'plantnumber':
                    value = escapeHtml(row.plantnumber || '');
                    break;
                case 'serialnumber':
                    value = escapeHtml(row.serialnumber || '');
                    break;
                case 'locname':
                    value = renderLocationPopoverHtml(row, row.locname || '', 'Site');
                    break;
                case 'subloc':
                    value = this.renderInlineFilterLink(row.subloc || '');
                    break;
                case 'postcode':
                    value = renderLocationPopoverHtml(row, row.postcode || '', 'Site');
                    break;
                case 'lastReport':
                    value = this.renderResultBadge(row.lastReport || '', row.pna_streak, true);
                    break;
                case 'duedate': {
                    const dueDate = formatDueDate(row.duedate);
                    value = dueDate ? this.renderInlineFilterLink(dueDate) : '<span class="badge bg-light text-dark">NDS</span>';
                    break;
                }
            }

            return `<td data-asset-column="${escapeHtml(key)}">${value}</td>`;
        }

        renderInlineFilterLink(value) {
            const clean = String(value || '').trim();
            if (!clean) {
                return '';
            }

            const safe = escapeHtml(clean);
            return `<span class="asset-inline-filter sFilter2" role="button" tabindex="0" data-asset-inline-filter="${safe}">${safe}</span>`;
        }

        renderMobileRow(row) {
            const itemnumber = String(row.itemnumber);
            const selected = this.isItemSelected(itemnumber);
            const dueDate = formatDueDate(row.duedate);
            const statusClass = this.getStatusClass(getStatusFromDue(row.duedate));
            const statusTitle = this.getStatusTitle(getStatusFromDue(row.duedate));
            const rowStyle = getPlantRowStyle(row.row_colour);
            const statusBorderClass = `asset-status-${statusClass}`;
            const labels = (this.config && this.config.labels) ? this.config.labels : {};
            const plantLabel = labels.plantnumber || 'Plant';
            const serialLabel = labels.serialnumber || 'Serial';
            const locationLabel = labels.locname || 'Location';
            const sublocLabel = labels.subloc || 'Sub-location';
            const dueLabel = labels.duedate || 'Due';
            const schemeBadges = renderSchemeBadges(row, this.config);
            const chainBtn = renderChainButtonHtml(row, this.config, 'asset-mobile-chain-btn');

            return `
<div class="asset-mobile-row card shadow-0 border mb-2 ${statusBorderClass} ${selected ? 'selected' : ''}${rowStyle.className}" data-itemnumber="${escapeHtml(itemnumber)}" title="${escapeHtml(statusTitle)}"${rowStyle.style}>
    <div class="card-body p-2">
        <div class="asset-mobile-r1">
            <input class="form-check-input asset-row-select" type="checkbox" data-itemnumber="${escapeHtml(itemnumber)}" ${selected ? 'checked' : ''}>
            <div>
                <div class="fw-bold asset-mobile-title">${escapeHtml(row.description || 'No description')}</div>
                ${schemeBadges ? `<div class="asset-itemnumber-badges asset-mobile-scheme-badges mt-1">${schemeBadges}</div>` : ''}
                <div class="small text-muted">#${escapeHtml(row.itemnumber)} �� ${escapeHtml(row.locname || '')}</div>
                <div class="small">
                    Plant: ${escapeHtml(row.plantnumber || '')} �� Serial: ${escapeHtml(row.serialnumber || '')}
                </div>
            </div>
            <div class="asset-mobile-result">
                <div class="asset-mobile-result-line">${this.renderResultBadge(row.lastReport || '', row.pna_streak)}${chainBtn}</div>
                <span class="asset-mobile-due">
                    <span class="asset-mobile-label asset-mobile-due-label">${escapeHtml(dueLabel)}:</span>
                    <span class="asset-mobile-value">${escapeHtml(dueDate || 'NDS')}</span>
                </span>
            </div>
        </div>
        <div class="asset-mobile-summary mt-2">
            <div><span class="asset-mobile-label">${escapeHtml(plantLabel)}:</span> <span class="asset-mobile-value">${escapeHtml(row.plantnumber || '')}</span></div>
            <div><span class="asset-mobile-label">${escapeHtml(serialLabel)}:</span> <span class="asset-mobile-value">${escapeHtml(row.serialnumber || '')}</span></div>
            <div><span class="asset-mobile-label">${escapeHtml(locationLabel)}:</span> <span class="asset-mobile-value">${escapeHtml(row.locname || '')}</span></div>
            <div><span class="asset-mobile-label">${escapeHtml(sublocLabel)}:</span> <span class="asset-mobile-value">${escapeHtml(row.subloc || '')}</span></div>
            <div><span class="asset-mobile-label">${escapeHtml(dueLabel)}</span><span class="asset-mobile-value">${escapeHtml(dueDate || 'NDS')}</span></div>
        </div>
        <div class="asset-mobile-r3 mt-2">
            <button type="button" class="btn btn-info btn-sm asset-mobile-footer-btn" data-asset-mobile-modal="details" data-itemnumber="${escapeHtml(itemnumber)}">
                <i class="fad fa-circle-info"></i> Item Details
            </button>
            <button type="button" class="btn btn-secondary btn-sm asset-mobile-footer-btn" data-asset-mobile-modal="actions" data-itemnumber="${escapeHtml(itemnumber)}">
                <i class="fad fa-ellipsis-h"></i> Item Actions
            </button>
        </div>
    </div>
</div>`;
        }

        renderResultBadge(result, pnaStreak, filterable) {
            const clean = String(result || '').trim();
            if (!clean) return '<span class="badge bg-light text-dark">N/E</span>';
            let cls = 'bg-secondary';
            const upper = clean.toUpperCase();
            if (['PASS', 'SATISFACTORY'].includes(upper)) cls = 'success-color-dark';
            if (['FAIL', 'UNSATISFACTORY', 'URGENT'].includes(upper)) cls = 'danger-color-dark';
            if (upper === 'DEFECTS') cls = 'warning-color-dark';
            if (upper === 'OBS') cls = 'special-color';
            if (upper === 'POBS') cls = 'success-color-dark';
            if (upper === 'PNA') cls = 'stylish-color-dark';
            const streak = Number(pnaStreak || 0);
            const badge = streak > 1 ? `<span class="asset-pna-streak">${streak}</span>` : '';
            const filterAttr = filterable ? ` role="button" tabindex="0" data-asset-inline-filter="${escapeHtml(clean)}"` : '';
            return `<span class="asset-result-badge"${filterAttr}><span class="badge ${cls} text-light">${escapeHtml(clean)}</span>${badge}</span>`;
        }

        getResultBorderClass(result) {
            const upper = String(result || '').trim().toUpperCase();
            if (['PASS', 'DEFECTS', 'URGENT', 'OBS', 'POBS', 'PNA'].includes(upper)) {
                return `asset-result-${upper.toLowerCase()}`;
            }
            return '';
        }

        getStatusClass(status) {
            if (status === 'overdue') return 'overdue';
            if (status === 'pending') return 'pending';
            if (status === 'not_due') return 'not-due';
            return 'unknown';
        }

        getStatusTitle(status) {
            if (status === 'overdue') return 'Overdue';
            if (status === 'pending') return 'Pending';
            if (status === 'not_due') return 'Not Due';
            return 'Status Unknown';
        }

        isColumnVisible(key) {
            const prefs = (this.config && this.config.prefs) ? this.config.prefs : {};
            if (!prefs[key] || typeof prefs[key] !== 'object') {
                return true;
            }
            return prefs[key].visible !== false;
        }

        updatePager() {
            const maxPage = Math.max(1, Math.ceil(this.filtered / this.state.perPage));
            const locked = this.isInteractionLocked();
            const summaryText = `${this.filtered} matching asset${this.filtered === 1 ? '' : 's'} (${this.total} total)`;
            if (this.pageInfo) this.pageInfo.textContent = `Page ${this.state.page} of ${maxPage}`;
            if (this.summary) this.summary.textContent = summaryText;
            if (this.mobileSummary) this.mobileSummary.textContent = summaryText;
            if (this.prevBtn) this.prevBtn.disabled = locked || this.state.page <= 1;
            if (this.nextBtn) this.nextBtn.disabled = locked || this.state.page >= maxPage;
        }

        setCurrentRowsSelected(selected) {
            this.rows.forEach((row) => {
                const itemnumber = String(row.itemnumber);
                if (selected) {
                    this.selectedRows.add(itemnumber);
                } else {
                    this.selectedRows.delete(itemnumber);
                }
            });
            this.syncSelectionUi();
            this.renderRows();
        }

        isItemSelected(itemnumber) {
            return this.selectedRows.has(String(itemnumber));
        }

        getSelectedItemArray() {
            return Array.from(this.selectedRows);
        }

        syncSelectionUi() {
            const items = this.getSelectedItemArray();
            if (this.selectedItems) this.selectedItems.textContent = items.join(',');
            if (this.mainToolbar) this.mainToolbar.style.display = items.length > 0 ? 'none' : '';
            if (this.massToolbar) this.massToolbar.style.display = items.length > 0 ? '' : 'none';
            this.syncMobileActionPanel();
        }

        syncMobileActionPanel() {
            if (!this.mobileActionBody || this.isDesktopLayout !== false) return;

            const source = this.selectedRows.size > 0 ? this.massToolbar : this.mainToolbar;
            if (!source) return;

            const clone = source.cloneNode(true);
            clone.querySelectorAll('script').forEach((el) => el.remove());
            clone.querySelectorAll('[id]').forEach((el) => el.removeAttribute('id'));
            clone.querySelectorAll('.mb-3').forEach((el) => el.classList.remove('mb-3'));

            this.mobileActionBody.innerHTML = clone.innerHTML + this.renderMobileSortPanel();
            this.initUiHelpers(this.mobileActionBody);
        }

        renderMobileSortPanel() {
            const labels = (this.config && this.config.labels) ? this.config.labels : {};
            const fields = this.getSortableFields();
            if (!fields.length) return '';

            const buttons = fields.map((field) => {
                const active = this.state.sortField === field;
                const dir = String(this.state.sortDir || '').toLowerCase() === 'desc' ? 'desc' : 'asc';
                const icon = active
                    ? (dir === 'asc' ? 'fad fa-sort-amount-up-alt' : 'fad fa-sort-amount-down')
                    : 'fad fa-sort';
                const label = labels[field] || field;
                const activeClass = active ? ' btn-info text-light asset-mobile-sort-active' : ' btn-light';
                const direction = active ? `<span class="asset-mobile-sort-dir">${escapeHtml(dir.toUpperCase())}</span>` : '';

                return `
<button type="button" class="btn asset-mobile-sort-btn${activeClass}" data-asset-mobile-sort="${escapeHtml(field)}" aria-pressed="${active ? 'true' : 'false'}">
    <span><i class="${icon}"></i> ${escapeHtml(label)}</span>
    ${direction}
</button>`;
            }).join('');

            return `
<div class="asset-mobile-sort-panel">
    <div class="asset-mobile-sort-title">Sort Schedule</div>
    <div class="asset-mobile-sort-grid">${buttons}</div>
</div>`;
        }

        getSortableFields() {
            const fields = Array.from(document.querySelectorAll('#assetGridTable th[data-sort-field]'))
                .map((th) => th.getAttribute('data-sort-field') || '')
                .filter(Boolean);

            if (fields.length) {
                return fields;
            }

            return DESKTOP_COLUMN_ORDER.filter((field) => this.isColumnVisible(field));
        }

        renderSelectionState() {
            document.querySelectorAll('.asset-row-select').forEach((checkbox) => {
                checkbox.checked = this.isItemSelected(checkbox.getAttribute('data-itemnumber'));
            });
            document.querySelectorAll('.asset-mobile-row').forEach((row) => {
                const selected = this.isItemSelected(row.getAttribute('data-itemnumber'));
                row.classList.toggle('selected', selected);
            });
        }

        initUiHelpers(scope) {
            const root = scope || document;
            if (window.mdb && mdb.Tooltip) {
                root.querySelectorAll('[data-mdb-tooltip-init]').forEach((el) => {
                    if (!mdb.Tooltip.getInstance(el)) new mdb.Tooltip(el);
                });
            }
            if (window.mdb && mdb.Popover) {
                root.querySelectorAll('[data-mdb-popover-init]').forEach((el) => {
                    if (!mdb.Popover.getInstance(el)) new mdb.Popover(el);
                });
            }
        }
    }

    window.createAssetGrid = function (config) {
        return new AssetGrid(config);
    };
})();
