{{--
    POS terminal — see App\Filament\Pages\Pos and docs/migration-plan.md §7.

    Presentation parity with the source React SPA (leolanka-inertia
    resources/js/Pages/pos.tsx + components/pos/*): full-height two-panel
    layout (products left, fixed cart column right), category/subcategory
    filters, combos strip, a fixed packaging/add-ons strip, image product
    cards, per-line quantity/price/discount/serial controls, and a checkout
    sheet with payment method tiles, custom method fields, credit notes,
    vouchers and loyalty redemption.

    Cart state (lines, quantities, discounts, running totals) is ENTIRELY
    client-side Alpine. Adding a line or nudging a quantity must never call the
    server: this replaces a zero-latency React SPA, and a Livewire round-trip
    per tap would be a severe regression. Server calls happen only at real
    boundaries — checkout, hold/resume, till/session changes, cash movements,
    amend, and the customer-dependent credit/loyalty lookup.

    Display state (expanded vs. minimized) is a persisted client-side flag in
    localStorage — deliberately NOT the browser Fullscreen API, which exits on
    print dialogs, focus changes, navigation and reloads. The flag drives a
    pure CSS overlay, survives every full-page reload (branch switch, till
    open/close, session start/end) and every print workflow, and is only ever
    changed by the minimize control (see toggleExpanded()).
--}}
<x-filament-panels::page>
    <div
        wire:ignore
        x-data="posTerminal({
            products: @js($products),
            bundles: @js($bundles),
            customers: @js($customers),
            packagingProducts: @js($packagingProducts),
            heldSales: @js($heldSales),
            tills: @js($tills),
            selectedTillId: @js($selectedTillId),
            openingBalance: @js($openingBalance),
            previousClosingBalance: @js($previousClosingBalance),
            activeSession: @js($activeSession),
            branches: @js($branches),
            selectedBranchId: @js($selectedBranchId),
            branchRequired: @js($branchRequired),
            cashMovementReasons: @js($cashMovementReasons),
            serverPaymentMethods: @js($paymentMethods),
            canCartDiscount: @js($canCartDiscount),
            canLineDiscount: @js($canLineDiscount),
            canVoucher: @js($canVoucher),
            canRedeemLoyalty: @js($canRedeemLoyalty),
            canCheckout: @js($canCheckout),
            canWithdrawCash: @js($canWithdrawCash),
            canOverrideBatch: @js($canOverrideBatch),
            allowAmend: @js($allowAmend),
            walkinOnly: @js($walkinOnly),
            loyaltyEnabled: @js($loyaltyEnabled),
            vouchersEnabled: @js($vouchersEnabled),
            cartRules: @js($cartRules),
            vatEnabled: @js($vatEnabled),
            businessName: @js($businessName),
            labelPrefix: @js($labelPrefix),
            cashMovementsUrl: @js($cashMovementsUrl),
            tillsUrl: @js($tillsUrl),
        })"
        class="fi-pos"
    >
        <div
            :class="expanded ? 'fixed inset-0 z-50 h-full w-full rounded-none shadow-none' : 'h-[calc(100dvh-10.5rem)] min-h-[30rem] rounded-xl shadow-sm'"
            class="flex overflow-hidden border border-gray-200 bg-white dark:border-white/10 dark:bg-gray-900"
        >
            {{-- ── Products panel ─────────────────────────────────────────── --}}
            <div
                :class="cartExpanded ? 'w-1/2 shrink-0' : 'flex-1'"
                class="flex min-w-0 flex-col border-r border-gray-200 dark:border-white/10"
            >
                <div class="shrink-0 border-b border-gray-200 dark:border-white/10">
                    <div class="p-3 pb-2">
                        <div class="relative">
                            <x-filament::icon icon="heroicon-m-magnifying-glass" class="pointer-events-none absolute inset-y-0 start-3 my-auto h-4 w-4 text-gray-400" />
                            <input
                                x-ref="searchInput"
                                type="search"
                                x-model="search"
                                x-on:keydown.enter="scanAdd()"
                                placeholder="Search or scan barcode… (F2)"
                                class="fi-input w-full rounded-lg border-gray-300 ps-9 dark:bg-gray-800"
                                {{-- This box already owns Enter (scanAdd() above) — the
                                     page-wide scan listener must stay out of its way, or a
                                     scan while it is focused would be handled twice. See
                                     resources/views/filament/partials/scan-capture-script.blade.php. --}}
                                data-scan-ignore
                            >
                        </div>
                    </div>
                    <div class="flex gap-2 px-3 pb-3">
                        <div class="relative flex-1" x-data="alpineCombobox({
                                get options() { return mainCatOptions },
                                get value() { return mainCat },
                                placeholder: 'Category',
                                searchPlaceholder: 'Search categories…',
                                onchange: (id) => setMainCat(id),
                            })" x-on:click.outside="open = false">
                            <button
                                type="button"
                                x-on:click="toggle"
                                class="flex h-9 w-full items-center justify-between rounded-lg border border-gray-300 bg-white px-3 text-sm text-gray-700 shadow-sm hover:border-primary-400 dark:border-white/10 dark:bg-gray-800 dark:text-gray-200"
                            >
                                <span class="truncate text-gray-500 dark:text-gray-400" x-text="selected ? selected.name : placeholder"></span>
                                <x-filament::icon icon="heroicon-m-chevron-up-down" class="ms-2 h-4 w-4 shrink-0 text-gray-400" />
                            </button>
                            <div x-show="open" x-cloak class="absolute start-0 end-0 top-[calc(100%+4px)] z-30 overflow-hidden rounded-lg border border-gray-200 bg-white shadow-lg dark:border-white/10 dark:bg-gray-900">
                                <div class="flex items-center gap-2 border-b border-gray-100 px-3 py-2 dark:border-white/10">
                                    <x-filament::icon icon="heroicon-m-magnifying-glass" class="h-4 w-4 shrink-0 text-gray-400" />
                                    <input
                                        x-ref="search"
                                        type="text"
                                        x-model="query"
                                        x-on:keydown.enter="select(filtered[highlighted] ?? null)"
                                        x-on:keydown.arrow-down.prevent="highlighted = Math.min(highlighted + 1, filtered.length - 1)"
                                        x-on:keydown.arrow-up.prevent="highlighted = Math.max(highlighted - 1, 0)"
                                        x-on:keydown.escape="open = false"
                                        :placeholder="searchPlaceholder"
                                        class="flex-1 bg-transparent text-sm outline-none"
                                        autocomplete="off"
                                    >
                                </div>
                                <div class="max-h-56 overflow-y-auto py-1">
                                    <template x-if="filtered.length === 0">
                                        <p class="px-3 py-2 text-center text-sm text-gray-500">No results found.</p>
                                    </template>
                                    <template x-for="(option, i) in filtered" :key="option.id">
                                        <button
                                            type="button"
                                            role="option"
                                            x-on:mousedown.prevent
                                            x-on:click="select(option)"
                                            x-on:mouseenter="highlighted = i"
                                            class="flex w-full items-center gap-2 px-3 py-1.5 text-sm hover:bg-gray-50 dark:hover:bg-white/5"
                                            :class="highlighted === i ? 'bg-gray-50 dark:bg-white/5' : ''"
                                        >
                                            <x-filament::icon icon="heroicon-m-check" class="h-4 w-4 shrink-0" x-bind:class="selected && String(option.id) === String(selected.id) ? '' : 'opacity-0'" />
                                            <span x-text="option.name"></span>
                                        </button>
                                    </template>
                                </div>
                            </div>
                        </div>
                        <div class="relative flex-1" x-data="alpineCombobox({
                                get options() { return subCatOptions },
                                get value() { return subCat },
                                placeholder: 'Subcategory',
                                searchPlaceholder: 'Search subcategories…',
                                onchange: (id) => setSubCat(id),
                            })" x-on:click.outside="open = false">
                            <button
                                type="button"
                                x-on:click="toggle"
                                :disabled="! mainCat"
                                class="flex h-9 w-full items-center justify-between rounded-lg border border-gray-300 bg-white px-3 text-sm text-gray-700 shadow-sm hover:border-primary-400 disabled:cursor-not-allowed disabled:opacity-50 dark:border-white/10 dark:bg-gray-800 dark:text-gray-200"
                            >
                                <span class="truncate text-gray-500 dark:text-gray-400" x-text="selected ? selected.name : placeholder"></span>
                                <x-filament::icon icon="heroicon-m-chevron-up-down" class="ms-2 h-4 w-4 shrink-0 text-gray-400" />
                            </button>
                            <div x-show="open" x-cloak class="absolute start-0 end-0 top-[calc(100%+4px)] z-30 overflow-hidden rounded-lg border border-gray-200 bg-white shadow-lg dark:border-white/10 dark:bg-gray-900">
                                <div class="flex items-center gap-2 border-b border-gray-100 px-3 py-2 dark:border-white/10">
                                    <x-filament::icon icon="heroicon-m-magnifying-glass" class="h-4 w-4 shrink-0 text-gray-400" />
                                    <input
                                        x-ref="search"
                                        type="text"
                                        x-model="query"
                                        x-on:keydown.enter="select(filtered[highlighted] ?? null)"
                                        x-on:keydown.arrow-down.prevent="highlighted = Math.min(highlighted + 1, filtered.length - 1)"
                                        x-on:keydown.arrow-up.prevent="highlighted = Math.max(highlighted - 1, 0)"
                                        x-on:keydown.escape="open = false"
                                        :placeholder="searchPlaceholder"
                                        class="flex-1 bg-transparent text-sm outline-none"
                                        autocomplete="off"
                                    >
                                </div>
                                <div class="max-h-56 overflow-y-auto py-1">
                                    <template x-if="filtered.length === 0">
                                        <p class="px-3 py-2 text-center text-sm text-gray-500">No results found.</p>
                                    </template>
                                    <template x-for="(option, i) in filtered" :key="option.id">
                                        <button
                                            type="button"
                                            role="option"
                                            x-on:mousedown.prevent
                                            x-on:click="select(option)"
                                            x-on:mouseenter="highlighted = i"
                                            class="flex w-full items-center gap-2 px-3 py-1.5 text-sm hover:bg-gray-50 dark:hover:bg-white/5"
                                            :class="highlighted === i ? 'bg-gray-50 dark:bg-white/5' : ''"
                                        >
                                            <x-filament::icon icon="heroicon-m-check" class="h-4 w-4 shrink-0" x-bind:class="selected && String(option.id) === String(selected.id) ? '' : 'opacity-0'" />
                                            <span x-text="option.name"></span>
                                        </button>
                                    </template>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>

                {{-- Combos strip ── --}}
                <template x-if="bundles.length > 0">
                    <div class="shrink-0 border-b border-gray-200 px-3 py-2 dark:border-white/10">
                        <div class="mb-1.5 text-[11px] font-semibold tracking-wide text-gray-500 uppercase dark:text-gray-400">Combos</div>
                        <div class="flex gap-2 overflow-x-auto pb-1">
                            <template x-for="bundle in bundles" :key="bundle.id">
                                <button
                                    type="button"
                                    x-on:click="addBundle(bundle)"
                                    :title="bundle.components.map(c => c.quantity + 'x ' + c.name).join(', ')"
                                    class="shrink-0 rounded-lg border border-primary-400/40 bg-primary-500/5 px-3 py-2 text-start hover:bg-primary-500/10"
                                >
                                    <div class="text-xs font-medium" x-text="bundle.name"></div>
                                    <div class="text-xs font-semibold text-primary-600 tabular-nums dark:text-primary-400" x-text="money(bundle.sale_price)"></div>
                                </button>
                            </template>
                        </div>
                    </div>
                </template>

                {{-- Packaging & add-ons strip ──
                    Ordinary products under the "Packaging & Add-ons" system
                    category, pinned here instead of scrolling away inside the
                    grid: a cashier reaches for a bag at the END of a sale, so
                    it has to stay put. Hidden entirely when the tenant has no
                    active packaging products. Cards are deliberately smaller
                    than the grid's, grouped under their subcategory, and add
                    to the cart through addProduct() like anything else. --}}
                <template x-if="packagingProducts.length > 0">
                    <div class="shrink-0 border-b border-gray-200 bg-amber-50/50 px-3 py-2 dark:border-white/10 dark:bg-amber-400/[0.04]">
                        <div class="mb-1.5 flex items-center gap-1.5">
                            <x-filament::icon icon="heroicon-m-shopping-bag" class="h-3.5 w-3.5 text-amber-600 dark:text-amber-400" />
                            <span class="text-[11px] font-semibold tracking-wide text-amber-700 uppercase dark:text-amber-400">Packaging &amp; Add-ons</span>
                        </div>
                        <div class="flex gap-3 overflow-x-auto pb-1">
                            {{-- The separator is bound off the loop index, not
                                a `first:` variant: the x-for <template> is
                                itself the container's first child, so
                                :first-child would never match a real group. --}}
                            <template x-for="(group, i) in packagingGroups" :key="group.name">
                                <div
                                    class="flex shrink-0 flex-col gap-1"
                                    :class="i === 0 ? '' : 'border-s border-amber-500/20 ps-3'"
                                >
                                    <div class="text-[10px] font-medium tracking-wide text-gray-500 uppercase dark:text-gray-400" x-text="group.name"></div>
                                    <div class="flex gap-1.5">
                                        <template x-for="product in group.products" :key="'pk' + product.id">
                                            <button
                                                type="button"
                                                x-on:click="product.quantity > 0 && addProduct(product)"
                                                :disabled="product.quantity <= 0"
                                                :title="product.name"
                                                class="relative w-24 shrink-0 rounded-lg border border-amber-500/30 bg-white px-2 py-2 text-start transition-colors disabled:cursor-not-allowed disabled:opacity-50 dark:bg-gray-800"
                                                :class="product.quantity > 0 ? 'hover:border-amber-500/60 hover:bg-amber-50 dark:hover:bg-amber-400/10' : ''"
                                            >
                                                <template x-if="cartLineFor(product)">
                                                    <span class="absolute -top-1.5 -end-1.5 flex h-4 min-w-4 items-center justify-center rounded-full bg-primary-600 px-1 text-[9px] font-bold text-white" :title="formatQtyWithUnit(cartLineFor(product).quantity, product.base_unit) + ' in cart'" x-text="formatQty(cartLineFor(product).quantity)"></span>
                                                </template>
                                                <div class="line-clamp-2 text-[11px] leading-tight font-medium" x-text="product.name"></div>
                                                <div class="mt-0.5 text-[11px] font-bold text-amber-700 tabular-nums dark:text-amber-400" x-text="priceLabel(product)"></div>
                                                <template x-if="product.quantity <= 0">
                                                    <div class="text-[9px] font-bold text-red-500">OUT OF STOCK</div>
                                                </template>
                                            </button>
                                        </template>
                                    </div>
                                </div>
                            </template>
                        </div>
                    </div>
                </template>

                <div class="min-h-0 flex-1 overflow-y-auto">
                    <div class="grid grid-cols-2 gap-3 p-3 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
                        <template x-for="p in filteredProducts" :key="'p' + p.id">
                            @include('filament.pos.product-card', [
                                'dimClass' => "p.quantity <= 0 ? 'pointer-events-none opacity-50' : 'cursor-pointer hover:shadow-md'",
                                'showSku' => false,
                                'noLotExpr' => "p.quantity > 0 && p.lot_count === 0",
                            ])
                        </template>
                        <template x-if="filteredProducts.length === 0">
                            <div class="col-span-full py-12 text-center text-sm text-gray-500">No products found</div>
                        </template>
                    </div>
                </div>
            </div>

            {{-- ── Cart panel ────────────────────────────────────────────── --}}
            <div
                :class="cartExpanded ? 'w-1/2' : 'w-72 xl:w-80'"
                class="flex shrink-0 flex-col bg-gray-50/50 dark:bg-gray-950/30"
            >
                {{-- Tenant identity — the configured business name, never the
                     system default (Master Control → Invoice → Business name). --}}
                <div class="shrink-0 border-b border-gray-200 bg-white px-3 py-2 dark:border-white/10 dark:bg-gray-900">
                    <div class="truncate text-center text-sm font-bold text-gray-800 dark:text-gray-100" x-text="businessName || 'POS'"></div>
                </div>
                {{-- Branch section ── --}}
                <div class="shrink-0 border-b border-gray-200 bg-gray-100/50 px-3 py-2 dark:border-white/10 dark:bg-white/5">
                    <div class="mb-1 flex items-center gap-1.5">
                        <x-filament::icon icon="heroicon-m-building-library" class="h-3.5 w-3.5 text-gray-500 dark:text-gray-400" />
                        <span class="flex-1 text-xs font-semibold tracking-wide text-gray-500 uppercase dark:text-gray-400">Branch</span>
                        <button
                            type="button"
                            x-on:click="heldOpen = true"
                            class="flex h-full min-h-[35px] w-32 shrink-0 items-center justify-center gap-2 rounded-lg border px-4 text-sm font-semibold transition"
                            :class="heldSales.length > 0
                                ? 'border-amber-500 bg-amber-100 text-amber-700 hover:bg-amber-200 dark:border-amber-400 dark:bg-amber-500/20 dark:text-amber-300 dark:hover:bg-amber-500/30'
                                : 'border-gray-300 bg-gray-100 text-gray-700 hover:bg-gray-200 dark:border-white/10 dark:bg-white/5 dark:text-gray-300 dark:hover:bg-white/10'"
                        >
                            <x-filament::icon
                                icon="heroicon-m-pause-circle"
                                class="h-5 w-5"
                            />

                            <span>Held</span>

                            <template x-if="heldSales.length > 0">
                                <span
                                    class="rounded-full bg-amber-500 px-2 py-0.5 text-xs font-bold text-white"
                                    x-text="heldSales.length"
                                ></span>
                            </template>
                        </button>
                        <button
                            type="button"
                            x-on:click="toggleCartExpanded()"
                            :title="cartExpanded ? 'Switch to compact cart' : 'Switch to full table view'"
                            class="flex h-8.5 w-9 items-center justify-center rounded border transition-colors"
                            :class="cartExpanded
                                ? 'border-primary-500 bg-primary-500/10 text-primary-600 dark:border-primary-400/40 dark:text-primary-400'
                                : 'border-gray-300 text-gray-500 hover:bg-gray-200 dark:border-white/10 dark:text-gray-400 dark:hover:bg-white/10'"
                        >
                            <x-filament::icon icon="heroicon-m-table-cells" class="h-4 w-4" />
                        </button>
                                                <button
                            type="button"
                            x-on:click="toggleExpanded()"
                            :title="expanded ? 'Exit fullscreen' : 'Enter fullscreen'"
                            class="flex h-8.5 w-9 items-center justify-center rounded border border-red-500 bg-red-500/10 text-red-400 hover:bg-red-500/20 dark:border-red-400/40 dark:text-red-400 dark:hover:bg-red-500/20"
                        >
                            <x-filament::icon x-show="!expanded" icon="heroicon-m-arrows-pointing-out" class="h-4 w-4" />
                            <x-filament::icon x-show="expanded" icon="heroicon-m-arrows-pointing-in" class="h-4 w-4" />
                        </button>
                    </div>
                    <template x-if="! activeSession && branches.length > 1">
                        <select
                            class="fi-input h-7 w-full rounded-md border-gray-300 text-xs dark:bg-gray-800"
                            :value="selectedBranchId ?? ''"
                            x-on:change="switchBranch($event.target.value)"
                        >
                            <option value="" x-text="selectedBranchId ? branches.find(b => b.id === selectedBranchId)?.name ?? 'Branch' : 'Select branch…'"></option>
                            <template x-for="branch in branches" :key="branch.id">
                                <option :value="branch.id" x-text="branch.name"></option>
                            </template>
                        </select>
                    </template>
                    <template x-if="activeSession || branches.length <= 1">
                        <div class="text-xs font-medium" x-text="activeSession?.branch_name ?? branches.find(b => b.id === selectedBranchId)?.name ?? 'No branch assigned'"></div>
                    </template>
                </div>

                {{-- Till section ── --}}
                <div class="shrink-0 border-b border-gray-200 bg-gray-100/50 px-3 py-2 dark:border-white/10 dark:bg-white/5">
                    <div class="mb-1 flex items-center gap-1.5">
                        <x-filament::icon icon="heroicon-m-banknotes" class="h-3.5 w-3.5 text-gray-500 dark:text-gray-400" />
                        <span class="flex-1 text-xs font-semibold tracking-wide text-gray-500 uppercase dark:text-gray-400">Till</span>
                        <template x-if="activeSession && canWithdrawCash">
                            <div class="flex gap-1">
                                <template x-if="allowAmend">
                                    <button
                                        type="button"
                                        x-on:click="openAmend()"
                                        class="flex items-center gap-1 rounded border border-gray-300 px-2 py-1 text-xs font-medium text-gray-500 hover:bg-gray-200 dark:border-white/10 dark:text-gray-400 dark:hover:bg-white/10"
                                    >
                                        <x-filament::icon icon="heroicon-m-pencil-square" class="h-3.5 w-3.5" />
                                        Exchange / Fix
                                    </button>
                                </template>
                                <button
                                    type="button"
                                    x-on:click="openWithdraw()"
                                    class="flex items-center gap-1 rounded border border-gray-300 px-2 py-1 text-xs font-medium text-gray-500 hover:bg-gray-200 dark:border-white/10 dark:text-gray-400 dark:hover:bg-white/10"
                                >
                                    <x-filament::icon icon="heroicon-m-arrow-path" class="h-3.5 w-3.5" />
                                    Cash
                                </button>
                                <button
                                    type="button"
                                    x-on:click="openCloseTill()"
                                    class="flex items-center gap-1 rounded border border-gray-300 px-2 py-1 text-xs font-medium text-gray-500 hover:bg-gray-200 dark:border-white/10 dark:text-gray-400 dark:hover:bg-white/10"
                                >
                                    <x-filament::icon icon="heroicon-m-lock-closed" class="h-3.5 w-3.5" />
                                    Close
                                </button>
                            </div>
                        </template>
                    </div>
                    <template x-if="activeSession">
                        <div class="flex items-center justify-between text-xs">
                            <span class="flex items-center gap-1 font-medium">
                                <x-filament::icon icon="heroicon-m-lock-closed" class="h-3 w-3" />
                                <span x-text="activeSession.till_name"></span>
                            </span>
                            <a :href="cashMovementsUrl" class="text-primary-600 hover:underline dark:text-primary-400">
                                Expected: <span class="tabular-nums" x-text="money(activeSession.expected_cash)"></span>
                            </a>
                        </div>
                    </template>
                    <template x-if="! activeSession && tills.length === 0">
                        <div class="text-xs leading-tight text-gray-500 dark:text-gray-400">
                            No till available for this branch.
                            <br>
                            <a :href="tillsUrl" class="font-medium text-primary-600 hover:underline dark:text-primary-400">Create a till</a> to start selling.
                        </div>
                    </template>
                    <template x-if="! activeSession && tills.length === 1">
                        <div class="text-xs font-medium" x-text="tills[0].name"></div>
                    </template>
                    <template x-if="! activeSession && tills.length > 1">
                        <select
                            class="fi-input h-7 w-full rounded-md border-gray-300 text-xs dark:bg-gray-800"
                            :value="selectedTillId ?? ''"
                            x-on:change="tillId = Number($event.target.value) || null; assignTill()"
                        >
                            <option value="">Select till…</option>
                            <template x-for="till in tills" :key="till.id">
                                <option :value="till.id" x-text="till.name"></option>
                            </template>
                        </select>
                    </template>
                </div>

                {{-- Customer section ── --}}
                <template x-if="! walkinOnly">
                    <div class="shrink-0 border-b border-gray-200 bg-gray-100/50 px-3 py-2 dark:border-white/10 dark:bg-white/5">
                        <div class="mb-1 flex items-center gap-2">
                            <span class="min-w-0 flex-1 truncate text-sm font-semibold" x-text="selectedCustomer?.business_name ?? `Customer's Name`"></span>
                            <template x-if="customerId">
                                <button type="button" class="shrink-0 text-xs text-primary-600 hover:underline dark:text-primary-400" x-on:click="selectCustomer('')">Change</button>
                            </template>
                            <button
                                type="button"
                                wire:click="mountAction('newCustomer')"
                                class="flex h-7 w-7 shrink-0 items-center justify-center rounded-md border border-primary-500 bg-primary-50 text-primary-600 transition hover:bg-primary-100 dark:border-primary-400/50 dark:bg-primary-500/10 dark:text-primary-400"
                                title="Add new customer"
                            >
                                <x-filament::icon icon="heroicon-m-plus" class="h-4 w-4" />
                            </button>
                        </div>
                        <div class="relative" x-data="alpineCombobox({
                                get options() { return customers.map(c => ({ id: c.id, name: c.business_name, phone: c.phone, nic: c.nic })) },
                                get value() { return customerId },
                                placeholder: 'Walk-in Customer',
                                searchPlaceholder: 'Search customers…',
                                onchange: (id) => selectCustomer(id),
                            })" x-on:click.outside="open = false">
                            <button
                                type="button"
                                x-on:click="toggle"
                                class="flex h-7 w-full items-center justify-between rounded-md border border-gray-300 bg-white px-2 text-xs shadow-sm hover:border-primary-400 dark:border-white/10 dark:bg-gray-800"
                            >
                                <span class="truncate text-gray-500 dark:text-gray-400" x-text="selected ? selected.name : placeholder"></span>
                                <x-filament::icon icon="heroicon-m-chevron-up-down" class="ms-2 h-3.5 w-3.5 shrink-0 text-gray-400" />
                            </button>
                            <div x-show="open" x-cloak class="absolute start-0 end-0 top-[calc(100%+4px)] z-30 overflow-hidden rounded-lg border border-gray-200 bg-white shadow-lg dark:border-white/10 dark:bg-gray-900">
                                <div class="flex items-center gap-2 border-b border-gray-100 px-3 py-2 dark:border-white/10">
                                    <x-filament::icon icon="heroicon-m-magnifying-glass" class="h-4 w-4 shrink-0 text-gray-400" />
                                    <input
                                        x-ref="search"
                                        type="text"
                                        x-model="query"
                                        x-on:keydown.enter="select(filtered[highlighted] ?? null)"
                                        x-on:keydown.arrow-down.prevent="highlighted = Math.min(highlighted + 1, filtered.length - 1)"
                                        x-on:keydown.arrow-up.prevent="highlighted = Math.max(highlighted - 1, 0)"
                                        x-on:keydown.escape="open = false"
                                        :placeholder="searchPlaceholder"
                                        class="flex-1 bg-transparent text-sm outline-none"
                                        autocomplete="off"
                                    >
                                </div>
                                <div class="max-h-56 overflow-y-auto py-1">
                                    <template x-if="filtered.length === 0">
                                        <p class="px-3 py-2 text-center text-sm text-gray-500">No results found.</p>
                                    </template>
                                    <template x-for="(option, i) in filtered" :key="option.id">
                                        <button
                                            type="button"
                                            role="option"
                                            x-on:mousedown.prevent
                                            x-on:click="select(option)"
                                            x-on:mouseenter="highlighted = i"
                                            class="flex w-full items-center gap-2 px-3 py-1.5 text-sm hover:bg-gray-50 dark:hover:bg-white/5"
                                            :class="highlighted === i ? 'bg-gray-50 dark:bg-white/5' : ''"
                                        >
                                            <x-filament::icon icon="heroicon-m-check" class="h-4 w-4 shrink-0" x-bind:class="selected && String(option.id) === String(selected.id) ? '' : 'opacity-0'" />
                                            <span x-text="option.name"></span>
                                        </button>
                                    </template>
                                </div>
                            </div>
                        </div>
                    </div>
                </template>

                {{-- Cart lines ── --}}
                <div class="min-h-0 flex-1 overflow-y-auto">
                    <template x-if="cart.length === 0">
                        <div class="py-12 text-center text-sm text-gray-500">No Item Selected</div>
                    </template>

                    {{-- Compact card view (default, narrow column) ── --}}
                    <template x-if="! cartExpanded">
                    <div>
                    <template x-for="line in cart" :key="line.key">
                        <div class="border-b border-gray-200 px-3 py-2 hover:bg-gray-100/50 dark:border-white/10 dark:hover:bg-white/5">
                            {{-- Bundle line ── --}}
                            <template x-if="line.bundle_id">
                                <div class="flex items-center gap-2">
                                    <div class="min-w-0 flex-1">
                                        <div class="text-sm leading-snug font-medium" x-text="line.name + ' (combo)'"></div>
                                        <div class="text-sm font-semibold text-primary-600 tabular-nums dark:text-primary-400" x-text="money(line.price)"></div>
                                    </div>
                <div class="flex items-center gap-1">
                                        <button
                                            type="button"
                                            x-on:click="bumpQty(line, -1)"
                                            class="flex h-9 w-9 items-center justify-center rounded border border-gray-300 text-gray-500 hover:bg-gray-200 dark:border-white/10 dark:hover:bg-white/10"
                                        >
                                            <x-filament::icon icon="heroicon-m-minus" class="h-4 w-4" />
                                        </button>
                                        <span class="w-8 text-center text-sm font-semibold tabular-nums" x-text="formatQty(line.quantity)"></span>
                                        <button type="button" x-on:click="bumpQty(line, 1)" class="flex h-9 w-9 items-center justify-center rounded border border-gray-300 text-gray-500 hover:bg-gray-200 dark:border-white/10 dark:hover:bg-white/10">
                                            <x-filament::icon icon="heroicon-m-plus" class="h-4 w-4" />
                                        </button>
                                    </div>
                                    <button type="button" x-on:click="removeItem(line)" class="flex h-8 w-8 shrink-0 items-center justify-center text-red-600 hover:opacity-70 dark:text-red-400">
                                        <x-filament::icon icon="heroicon-m-trash" class="h-4 w-4" />
                                    </button>
                                </div>
                            </template>

                            {{-- Product line ── --}}
                            <template x-if="! line.bundle_id">
                                <div>
                                    <div class="flex items-center gap-2">
                                        <div class="min-w-0 flex-1">
                                            <div class="line-clamp-2 text-sm leading-snug font-medium" x-text="line.name"></div>
                                            <div class="flex items-center gap-1.5">
                                                <template x-if="lineDiscount(line) > 0 || lineDealDiscount(line) > 0">
                                                    <div class="flex items-center gap-1.5">
                                                        <span class="text-xs text-gray-500 line-through" x-text="money(lineGross(line))"></span>
                                                        <span class="text-sm font-semibold text-green-700 tabular-nums dark:text-green-400" x-text="money(lineNet(line))"></span>
                                                    </div>
                                                </template>
                                                <template x-if="! (lineDiscount(line) > 0 || lineDealDiscount(line) > 0)">
                                                    <span class="text-sm font-semibold text-primary-600 tabular-nums dark:text-primary-400" x-text="money(line.price)"></span>
                                                </template>
                                            </div>
                                        </div>
                                        <div class="flex shrink-0 flex-col items-center gap-0.5">
                                            <div class="flex items-center gap-1">
                                                <button type="button" x-on:click="bumpQty(line, -stepFor(line))" class="flex h-9 w-9 items-center justify-center rounded border border-gray-300 text-gray-500 hover:bg-gray-200 dark:border-white/10 dark:hover:bg-white/10">
                                                    <x-filament::icon icon="heroicon-m-minus" class="h-4 w-4" />
                                                </button>
                                                <input
                                                    type="text" inputmode="decimal"
                                                    :value="line.quantity"
                                                    x-on:input="line._qtyText = $event.target.value"
                                                    x-on:change="setQty(line, $event.target.value)"
                                                    x-on:blur="line._qtyText = null"
                                                    class="fi-input h-9 w-14 rounded-md border-gray-300 py-0 px-1 text-center text-sm tabular-nums dark:bg-gray-800"
                                                >
                                                <button type="button" x-on:click="bumpQty(line, stepFor(line))" class="flex h-9 w-9 items-center justify-center rounded border border-gray-300 text-gray-500 hover:bg-gray-200 dark:border-white/10 dark:hover:bg-white/10">
                                                    <x-filament::icon icon="heroicon-m-plus" class="h-4 w-4" />
                                                </button>
                                            </div>
                                            <span x-show="line.base_unit" class="text-[9px] leading-none text-gray-500 dark:text-gray-400" x-text="line.base_unit?.code || line.base_unit?.name"></span>
                                        </div>
                                        <button type="button" x-on:click="removeItem(line)" class="flex h-8 w-8 shrink-0 items-center justify-center text-red-600 hover:opacity-70 dark:text-red-400">
                                            <x-filament::icon icon="heroicon-m-trash" class="h-4 w-4" />
                                        </button>
                                    </div>

                                    <div class="mt-1 flex items-center gap-2">
                                        <template x-if="line.deal">
                                            <span class="truncate rounded border border-amber-600 bg-amber-600/10 px-1.5 py-0.5 text-[11px] font-medium text-amber-700 dark:text-amber-400" x-text="line.deal.name + ' − ' + money(lineDealDiscount(line))"></span>
                                        </template>
                                        <template x-if="canLineDiscount">
                                            <div class="relative inline-block" x-data="discountPopover({
                                                    get type() { return line.discount_type },
                                                    get value() { return line.discount_value },
                                                    onapply: (type, value) => { line.discount_type = type; line.discount_value = value },
                                                    onclear: () => { line.discount_type = null; line.discount_value = 0 },
                                                })">
                                                <button
                                                    type="button"
                                                    x-on:click="toggle"
                                                    class="rounded border px-2 py-1 text-xs font-medium transition-colors"
                                                    :class="hasDiscount ? 'border-green-600 bg-green-600/10 text-green-700' : 'text-gray-500 hover:bg-gray-200 dark:text-gray-400 dark:hover:bg-white/10'"
                                                >
<span x-text="hasDiscount ? (type === 'percentage' ? '-' + value + '%' : '-' + money(value) + '/u') : 'Discount'"></span>
                                                </button>
                                                <div x-show="open" x-cloak class="absolute z-30 mt-1 w-56 space-y-2 rounded-lg border border-gray-200 bg-white p-3 shadow-lg dark:border-white/10 dark:bg-gray-900">
                                                    <div class="text-xs font-semibold">Discount per unit</div>
                                                    <p class="text-[10px] leading-snug text-gray-400 dark:text-gray-500">Rs. applies to each unit — total off = Rs. × quantity</p>
                                                    <div class="flex gap-1">
                                                        <div class="flex shrink-0 overflow-hidden rounded-md border border-gray-300 dark:border-white/10">
                                                            <button
                                                                type="button"
                                                                x-on:click="draftType = 'fixed'"
                                                                class="px-2 py-1 text-xs font-semibold"
                                                                :class="draftType === 'fixed' ? 'bg-primary-600 text-white' : 'bg-white text-gray-500 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-white/10'"
                                                            >Rs.</button>
                                                            <button
                                                                type="button"
                                                                x-on:click="draftType = 'percentage'"
                                                                class="px-2 py-1 text-xs font-semibold"
                                                                :class="draftType === 'percentage' ? 'bg-primary-600 text-white' : 'bg-white text-gray-500 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-white/10'"
                                                            >%</button>
                                                        </div>
                                                        <input
                                                            type="number" min="0" x-model="draftValue"
                                                            x-on:keydown.enter="apply"
                                                            class="fi-input h-7 flex-1 rounded-md border-gray-300 py-0 text-sm dark:bg-gray-800"
                                                            placeholder="0.00"
                                                        >
                                                    </div>
                                                    <div class="flex justify-end gap-1.5">
                                                        <template x-if="hasDiscount">
                                                            <button type="button" x-on:click="clear()" class="rounded-md px-2 py-1 text-xs text-gray-500 hover:bg-gray-100 dark:hover:bg-white/10">Remove</button>
                                                        </template>
                                                        <button type="button" x-on:click="apply" class="rounded-md bg-primary-600 px-3 py-1 text-xs font-medium text-white hover:bg-primary-500">Apply</button>
                                                    </div>
                                                </div>
                                            </div>
                                        </template>

                                        {{-- Price-amount input: type the amount, quantity is back-solved ── --}}
                                        <div class="flex shrink-0 items-center gap-1">
                                            <span class="text-lg font-medium text-gray-500 dark:text-gray-400">Rs.</span>
                                            <input
                                                type="text" inputmode="decimal"
                                                :value="(line.quantity * line.price).toFixed(2)"
                                                x-on:input="line._amtText = $event.target.value"
                                                x-on:change="setLineAmount(line, $event.target.value)"
                                                x-on:blur="line._amtText = null"
                                                class="fi-input h-9 w-24 rounded-md border-gray-300 py-0 px-1 text-center text-lg tabular-nums dark:bg-gray-800"
                                            >
                                        </div>
                                    </div>

                                    {{-- Selected lot — every product line prices from a lot, so the
                                         consignment the units come from is always shown and changeable.
                                         The WHOLE row is the click target (a real <button>, not a
                                         label plus a small separate button off to the side) so there's
                                         one obvious, generously-sized place to tap to change it. --}}
                                    <template x-if="line.product_id && line.is_batch_tracked !== false">
                                        <button
                                            type="button"
                                            x-on:click="openLotPicker(line.product_id, line)"
                                            :disabled="lotPickerLoading"
                                            class="mt-1 flex w-full items-center gap-1.5 rounded border p-1.5 text-start text-[11px] transition-colors disabled:cursor-not-allowed disabled:opacity-50"
                                            :class="line.batch_id
                                                ? 'border-gray-200 bg-gray-100/60 hover:border-primary-400 hover:bg-primary-50 dark:border-white/10 dark:bg-white/5 dark:hover:border-primary-500/40 dark:hover:bg-primary-500/10'
                                                : 'border-amber-400/60 bg-amber-50 hover:bg-amber-100 dark:border-amber-400/30 dark:bg-amber-400/10 dark:hover:bg-amber-400/20'"
                                        >
                                            <template x-if="line.batch_id">
                                                <span class="min-w-0 flex-1 truncate text-gray-600 dark:text-gray-400" :title="lotTitle(line)">
                                                    <span class="font-semibold text-gray-800 dark:text-gray-200" x-text="lotNumber(line)"></span>
                                                    <template x-if="line._lot_expiry"> · exp <span x-text="line._lot_expiry"></span></template>
                                                    <template x-if="line._lot_avail !== undefined"> · <span x-text="formatQty(line._lot_avail)"></span> avail</template>
                                                </span>
                                            </template>
                                            <template x-if="! line.batch_id">
                                                <span class="min-w-0 flex-1 truncate font-medium text-amber-700 dark:text-amber-400">No lot selected — choose the consignment to sell from</span>
                                            </template>
                                            <span class="flex shrink-0 items-center gap-0.5 font-semibold text-primary-600 dark:text-primary-400">
                                                <span x-text="line.batch_id ? 'Change' : 'Select'"></span>
                                                <x-filament::icon icon="heroicon-m-chevron-right" class="h-3 w-3" />
                                            </span>
                                        </button>
                                    </template>
                                    <template x-if="line.product_id && line.is_batch_tracked === false">
                                        <div class="mt-1 flex items-center gap-1.5 rounded border border-gray-200 bg-gray-100/60 p-1.5 text-[11px] dark:border-white/10 dark:bg-white/5">
                                            <span class="min-w-0 flex-1 truncate text-gray-600 dark:text-gray-400">Unified stock · <span x-text="formatQty(line.stock)"></span> avail</span>
                                            <span class="shrink-0 rounded-md bg-gray-200 px-2 py-0.5 font-medium text-gray-600 dark:bg-white/10 dark:text-gray-300">Unified pool</span>
                                        </div>
                                    </template>
                                    <template x-if="requiresSerial(line)">
                                        <div class="mt-1 space-y-1 rounded border border-gray-200 bg-gray-100/60 p-1.5 text-[11px] dark:border-white/10 dark:bg-white/5">
                                            <div :class="line.serial_ids.length === line.quantity ? 'text-gray-500 dark:text-gray-400' : 'font-medium text-amber-600'" x-text="serialLabel(line)"></div>
                                            <select
                                                multiple
                                                :size="serialsSize(line)"
                                                class="w-full rounded border border-gray-300 bg-white px-1 py-0.5 dark:border-white/10 dark:bg-gray-800"
                                                x-on:change="setLineSerials(line, Array.from($event.target.selectedOptions).map(o => Number(o.value)).slice(0, Math.max(1, Math.round(line.quantity))))"
                                            >
                                                <template x-for="serial in serialsFor(line)" :key="serial.id">
                                                    <option :value="serial.id" :selected="line.serial_ids.includes(serial.id)" x-text="serial.serial_number"></option>
                                                </template>
                                            </select>
                                        </div>
                                    </template>

                                    {{-- Item price breakdown when a deal is on the line ── --}}
                                    <template x-if="line.deal && lineDealDiscount(line) > 0">
                                        <div class="mt-1 space-y-0.5 border-l-2 border-amber-500/40 ps-2">
                                            <div class="flex justify-between text-[10px] text-gray-500 dark:text-gray-400">
                                                <span>Original price</span>
                                                <span class="tabular-nums" x-text="money(lineGross(line))"></span>
                                            </div>
                                            <div class="flex justify-between text-[10px] text-amber-600 dark:text-amber-400">
                                                <span class="truncate" x-text="'Deal discount · ' + line.deal.name"></span>
                                                <span class="tabular-nums" x-text="'-' + money(lineDealDiscount(line))"></span>
                                            </div>
                                            <div class="flex justify-between text-[10px] font-semibold text-gray-700 dark:text-gray-200">
                                                <span>Final price</span>
                                                <span class="tabular-nums" x-text="money(lineNet(line))"></span>
                                            </div>
                                        </div>
                                    </template>

                                </div>
                            </template>
                        </div>
                    </template>
                    </div>
                    </template>

                    {{-- Expanded table view — 50/50 layout, columns matching
                         the tablet-POS reference (item / qty / price /
                         discount / net amount). Every control here is the
                         same client-side mutation the card view uses
                         (bumpQty/setQty/setLineAmount/discountPopover/
                         removeItem/openLotPicker/setLineSerials), just laid
                         out horizontally — nothing here is read-only.

                         table-fixed + an explicit <colgroup> is deliberate:
                         with the default `table-layout: auto`, a sticky
                         <thead> can size its columns independently of the
                         <tbody> in some browsers (the header recalculates
                         column widths from its own — much shorter — content
                         instead of the widest cell per column), which is
                         what drifts the numbers out from under their
                         headings. Fixed column widths remove that ambiguity
                         entirely: header and body always share the exact
                         same grid. ── --}}
                    <template x-if="cartExpanded && cart.length > 0">
                        <table class="w-full table-fixed border-collapse text-sm">
                            <colgroup>
                                <col style="width: 26%">
                                <col style="width: 15%">
                                <col style="width: 11%">
                                <col style="width: 13%">
                                <col style="width: 27%">
                                <col style="width: 8%">
                            </colgroup>
                            <thead class="sticky top-0 z-10 bg-gray-100 dark:bg-gray-900">
                                <tr class="border-b-2 border-gray-200 text-start text-[11px] font-semibold tracking-wide text-gray-500 uppercase dark:border-white/10 dark:text-gray-400">
                                    <th class="px-3 py-2.5 text-start">Item</th>
                                    <th class="px-3 py-2.5 text-center">Qty</th>
                                    <th class="px-3 py-2.5 text-end">Price</th>
                                    <th class="px-3 py-2.5 text-end">Discount</th>
                                    <th class="px-3 py-2.5 text-end">Net Amount</th>
                                    <th class="px-3 py-2.5"></th>
                                </tr>
                            </thead>
                            <tbody>
                                <template x-for="line in cart" :key="line.key">
                                    <tr class="border-b border-gray-200 odd:bg-white even:bg-gray-50/60 hover:bg-primary-50/40 dark:border-white/10 dark:odd:bg-gray-900 dark:even:bg-white/[0.03] dark:hover:bg-white/5">
                                        {{-- Item ── --}}
                                        <td class="px-3 py-2.5 align-top">
                                            <template x-if="line.bundle_id">
                                                <div class="font-medium leading-snug" x-text="line.name + ' (combo)'"></div>
                                            </template>
                                            <template x-if="! line.bundle_id">
                                                <div>
                                                    <div class="font-mono text-[10px] text-gray-400" x-text="line.code || '—'"></div>
                                                    <div class="line-clamp-2 leading-snug font-medium" x-text="line.name"></div>
                                                    <template x-if="line.deal">
                                                        <div class="mt-0.5">
                                                            <span class="truncate rounded border border-amber-600 bg-amber-600/10 px-1 py-0 text-[10px] font-medium text-amber-700 dark:text-amber-400" x-text="line.deal.name + ' − ' + money(lineDealDiscount(line))"></span>
                                                        </div>
                                                    </template>

                                                    {{-- Selected lot — same whole-row-clickable control as
                                                         the card view (see the card's comment for why): a
                                                         real <button> spanning the row, not a label plus a
                                                         small separate button off to the side. --}}
                                                    <template x-if="line.is_batch_tracked !== false">
                                                        <button
                                                            type="button"
                                                            x-on:click="openLotPicker(line.product_id, line)"
                                                            :disabled="lotPickerLoading"
                                                            class="mt-1 flex w-full items-center gap-1.5 rounded border px-1.5 py-1 text-start text-[10px] transition-colors disabled:cursor-not-allowed disabled:opacity-50"
                                                            :class="line.batch_id
                                                                ? 'border-gray-200 bg-gray-100/60 hover:border-primary-400 hover:bg-primary-50 dark:border-white/10 dark:bg-white/5 dark:hover:border-primary-500/40 dark:hover:bg-primary-500/10'
                                                                : 'border-amber-400/60 bg-amber-50 hover:bg-amber-100 dark:border-amber-400/30 dark:bg-amber-400/10 dark:hover:bg-amber-400/20'"
                                                        >
                                                            <template x-if="line.batch_id">
                                                                <span class="min-w-0 flex-1 truncate text-gray-600 dark:text-gray-400" :title="lotTitle(line)">
                                                                    <span class="font-semibold text-gray-800 dark:text-gray-200" x-text="lotNumber(line)"></span>
                                                                    <template x-if="line._lot_avail !== undefined"> · <span x-text="formatQty(line._lot_avail)"></span> avail</template>
                                                                </span>
                                                            </template>
                                                            <template x-if="! line.batch_id">
                                                                <span class="min-w-0 flex-1 truncate font-medium text-amber-700 dark:text-amber-400">No lot selected</span>
                                                            </template>
                                                            <span class="flex shrink-0 items-center gap-0.5 font-semibold text-primary-600 dark:text-primary-400">
                                                                <span x-text="line.batch_id ? 'Change' : 'Select'"></span>
                                                                <x-filament::icon icon="heroicon-m-chevron-right" class="h-3 w-3" />
                                                            </span>
                                                        </button>
                                                    </template>
                                                    <template x-if="line.is_batch_tracked === false">
                                                        <div class="mt-1 flex items-center gap-1.5 rounded border border-gray-200 bg-gray-100/60 px-1.5 py-1 text-[10px] dark:border-white/10 dark:bg-white/5">
                                                            <span class="min-w-0 flex-1 truncate text-gray-600 dark:text-gray-400">Unified stock · <span x-text="formatQty(line.stock)"></span> avail</span>
                                                        </div>
                                                    </template>

                                                    <template x-if="requiresSerial(line)">
                                                        <div class="mt-1 space-y-1 rounded border border-gray-200 bg-gray-100/60 p-1.5 text-[11px] dark:border-white/10 dark:bg-white/5">
                                                            <div :class="line.serial_ids.length === line.quantity ? 'text-gray-500 dark:text-gray-400' : 'font-medium text-amber-600'" x-text="serialLabel(line)"></div>
                                                            <select
                                                                multiple
                                                                :size="serialsSize(line)"
                                                                class="w-full rounded border border-gray-300 bg-white px-1 py-0.5 dark:border-white/10 dark:bg-gray-800"
                                                                x-on:change="setLineSerials(line, Array.from($event.target.selectedOptions).map(o => Number(o.value)).slice(0, Math.max(1, Math.round(line.quantity))))"
                                                            >
                                                                <template x-for="serial in serialsFor(line)" :key="serial.id">
                                                                    <option :value="serial.id" :selected="line.serial_ids.includes(serial.id)" x-text="serial.serial_number"></option>
                                                                </template>
                                                            </select>
                                                        </div>
                                                    </template>
                                                </div>
                                            </template>
                                        </td>

                                        {{-- Qty ── --}}
                                        <td class="px-3 py-2.5 align-top">
                                            <template x-if="line.bundle_id">
                                                <div class="flex items-center justify-center gap-1">
                                                    <button type="button" x-on:click="bumpQty(line, -1)" class="flex h-7 w-7 items-center justify-center rounded border border-gray-300 text-gray-500 hover:bg-gray-200 dark:border-white/10 dark:hover:bg-white/10">
                                                        <x-filament::icon icon="heroicon-m-minus" class="h-3.5 w-3.5" />
                                                    </button>
                                                    <span class="w-8 text-center text-xs font-semibold tabular-nums" x-text="formatQty(line.quantity)"></span>
                                                    <button type="button" x-on:click="bumpQty(line, 1)" class="flex h-7 w-7 items-center justify-center rounded border border-gray-300 text-gray-500 hover:bg-gray-200 dark:border-white/10 dark:hover:bg-white/10">
                                                        <x-filament::icon icon="heroicon-m-plus" class="h-3.5 w-3.5" />
                                                    </button>
                                                </div>
                                            </template>
                                            <template x-if="! line.bundle_id">
                                                <div class="flex flex-col items-center gap-0.5">
                                                    <div class="flex items-center gap-1">
                                                        <button type="button" x-on:click="bumpQty(line, -stepFor(line))" class="flex h-7 w-7 shrink-0 items-center justify-center rounded border border-gray-300 text-gray-500 hover:bg-gray-200 dark:border-white/10 dark:hover:bg-white/10">
                                                            <x-filament::icon icon="heroicon-m-minus" class="h-3.5 w-3.5" />
                                                        </button>
                                                        <input
                                                            type="text" inputmode="decimal"
                                                            :value="line.quantity"
                                                            x-on:input="line._qtyText = $event.target.value"
                                                            x-on:change="setQty(line, $event.target.value)"
                                                            x-on:blur="line._qtyText = null"
                                                            class="fi-input h-7 w-10 min-w-0 rounded border-gray-300 px-0.5 py-0 text-center text-xs tabular-nums dark:bg-gray-800"
                                                        >
                                                        <button type="button" x-on:click="bumpQty(line, stepFor(line))" class="flex h-7 w-7 shrink-0 items-center justify-center rounded border border-gray-300 text-gray-500 hover:bg-gray-200 dark:border-white/10 dark:hover:bg-white/10">
                                                            <x-filament::icon icon="heroicon-m-plus" class="h-3.5 w-3.5" />
                                                        </button>
                                                    </div>
                                                    <span x-show="line.base_unit" class="text-[9px] leading-none text-gray-500 dark:text-gray-400" x-text="line.base_unit?.code || line.base_unit?.name"></span>
                                                </div>
                                            </template>
                                        </td>

                                        {{-- Price ── --}}
                                        <td class="px-3 py-2.5 text-end align-top whitespace-nowrap font-semibold tabular-nums text-primary-600 dark:text-primary-400" x-text="money(line.price)"></td>

                                        {{-- Discount ── --}}
                                        <td class="px-3 py-2.5 text-end align-top">
                                            <template x-if="line.bundle_id">
                                                <span class="text-gray-400">—</span>
                                            </template>
                                            <template x-if="! line.bundle_id && canLineDiscount">
                                                <div class="relative inline-block" x-data="discountPopover({
                                                        get type() { return line.discount_type },
                                                        get value() { return line.discount_value },
                                                        onapply: (type, value) => { line.discount_type = type; line.discount_value = value },
                                                        onclear: () => { line.discount_type = null; line.discount_value = 0 },
                                                    })">
                                                    <button
                                                        type="button"
                                                        x-on:click="toggle"
                                                        class="whitespace-nowrap rounded border px-2 py-1 text-xs font-medium transition-colors"
                                                        :class="hasDiscount ? 'border-green-600 bg-green-600/10 text-green-700' : 'text-gray-500 hover:bg-gray-200 dark:text-gray-400 dark:hover:bg-white/10'"
                                                    >
                                                        <span x-text="hasDiscount ? (type === 'percentage' ? '-' + value + '%' : '-' + money(value) + '/u') : 'Discount'"></span>
                                                    </button>
                                                    <div x-show="open" x-cloak class="absolute end-0 z-30 mt-1 w-56 space-y-2 rounded-lg border border-gray-200 bg-white p-3 text-start shadow-lg dark:border-white/10 dark:bg-gray-900">
                                                        <div class="text-xs font-semibold">Discount per unit</div>
                                                        <div class="flex gap-1">
                                                            <div class="flex shrink-0 overflow-hidden rounded-md border border-gray-300 dark:border-white/10">
                                                                <button type="button" x-on:click="draftType = 'fixed'" class="px-2 py-1 text-xs font-semibold" :class="draftType === 'fixed' ? 'bg-primary-600 text-white' : 'bg-white text-gray-500 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-white/10'">Rs.</button>
                                                                <button type="button" x-on:click="draftType = 'percentage'" class="px-2 py-1 text-xs font-semibold" :class="draftType === 'percentage' ? 'bg-primary-600 text-white' : 'bg-white text-gray-500 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-white/10'">%</button>
                                                            </div>
                                                            <input type="number" min="0" x-model="draftValue" x-on:keydown.enter="apply" class="fi-input h-7 flex-1 rounded-md border-gray-300 py-0 text-sm dark:bg-gray-800" placeholder="0.00">
                                                        </div>
                                                        <div class="flex justify-end gap-1.5">
                                                            <template x-if="hasDiscount">
                                                                <button type="button" x-on:click="clear()" class="rounded-md px-2 py-1 text-xs text-gray-500 hover:bg-gray-100 dark:hover:bg-white/10">Remove</button>
                                                            </template>
                                                            <button type="button" x-on:click="apply" class="rounded-md bg-primary-600 px-3 py-1 text-xs font-medium text-white hover:bg-primary-500">Apply</button>
                                                        </div>
                                                    </div>
                                                </div>
                                            </template>
                                            <template x-if="! line.bundle_id && ! canLineDiscount">
                                                <span class="text-gray-400">—</span>
                                            </template>
                                        </td>

                                        {{-- Net Amount ── --}}
                                        <td class="px-3 py-2.5 text-end align-top">
                                            <template x-if="line.bundle_id">
                                                <span class="text-base font-bold tabular-nums" x-text="money(line.price * line.quantity)"></span>
                                            </template>
                                            <template x-if="! line.bundle_id">
                                                <div>
                                                    <div class="flex items-center justify-end gap-1.5 whitespace-nowrap">
                                                        <span class="shrink-0 text-sm font-medium text-gray-400 dark:text-gray-500">Rs.</span>
                                                        <input
                                                            type="text" inputmode="decimal"
                                                            :value="(line.quantity * line.price).toFixed(2)"
                                                            x-on:input="line._amtText = $event.target.value"
                                                            x-on:change="setLineAmount(line, $event.target.value)"
                                                            x-on:blur="line._amtText = null"
                                                            class="fi-input h-10 w-full min-w-0 rounded-md border-gray-300 px-2 py-0 text-end text-base font-bold tabular-nums text-gray-900 dark:bg-gray-800 dark:text-gray-100"
                                                        >
                                                    </div>
                                                    <template x-if="lineDiscount(line) > 0 || lineDealDiscount(line) > 0">
                                                        <div class="mt-0.5 text-[11px] font-semibold text-green-700 dark:text-green-400" x-text="'net ' + money(lineNet(line))"></div>
                                                    </template>
                                                </div>
                                            </template>
                                        </td>

                                        {{-- Actions ── --}}
                                        <td class="px-2 py-2.5 align-top">
                                            <div class="flex items-center justify-end">
                                                <button type="button" x-on:click="removeItem(line)" class="flex h-7 w-7 shrink-0 items-center justify-center text-red-600 hover:opacity-70 dark:text-red-400" title="Remove">
                                                    <x-filament::icon icon="heroicon-m-trash" class="h-3.5 w-3.5" />
                                                </button>
                                            </div>
                                        </td>
                                    </tr>
                                </template>
                            </tbody>
                        </table>
                    </template>
                </div>

                {{-- Totals + actions ── --}}
                <div class="shrink-0 space-y-2 border-t border-gray-200 p-3 dark:border-white/10">
                    <div class="flex justify-between text-lg">
                        <span class="text-gray-500 dark:text-gray-400">Subtotal</span>
                        <span class="font-medium tabular-nums" x-text="money(grossSubtotal)"></span>
                    </div>
                    <template x-if="lineDiscountTotal > 0">
                        <div class="flex justify-between text-lg text-green-700 dark:text-green-400">
                            <span>Product discounts</span>
                            <span class="font-medium tabular-nums" x-text="'-' + money(lineDiscountTotal)"></span>
                        </div>
                    </template>
                    <template x-if="dealDiscountTotal > 0">
                        <div class="flex justify-between text-lg text-amber-600 dark:text-amber-400">
                            <span>Deal discounts</span>
                            <span class="font-medium tabular-nums" x-text="'-' + money(dealDiscountTotal)"></span>
                        </div>
                    </template>
                    <template x-if="cartRuleDiscount > 0">
                        <div class="flex justify-between text-lg text-sky-600 dark:text-sky-400">
                            <span class="truncate" x-text="'Spend &amp; save · ' + (cartRule?.name ?? '')"></span>
                            <span class="font-medium tabular-nums" x-text="'-' + money(cartRuleDiscount)"></span>
                        </div>
                    </template>
                    <template x-if="canCartDiscount">
                        <div class="flex items-center justify-between gap-2">
                            <span class="text-base text-gray-500 dark:text-gray-400">Invoice discount</span>
                            <div class="relative inline-block shrink-0" x-data="discountPopover({
                                    get type() { return discountType },
                                    get value() { return discountValue },
                                    onapply: (type, value) => { discountType = type; discountValue = value },
                                    onclear: () => { discountType = 'fixed'; discountValue = 0 },
                                })">
                                <button
                                    type="button"
                                    x-on:click="toggle"
                                    class="rounded border px-1.5 py-0.5 text-[11px] font-medium transition-colors"
                                    :class="hasDiscount ? 'border-green-600 bg-green-600/10 text-green-700' : 'text-gray-500 hover:bg-gray-200 dark:text-gray-400 dark:hover:bg-white/10'"
                                >
                                    <span x-text="hasDiscount ? (type === 'percentage' ? '-' + value + '%' : '-' + money(value)) : 'Discount'"></span>
                                </button>
                                <div x-show="open" x-cloak class="absolute end-0 bottom-full z-30 mb-1 w-56 space-y-2 rounded-lg border border-gray-200 bg-white p-3 shadow-lg dark:border-white/10 dark:bg-gray-900">
                                    <div class="text-xs font-semibold">Invoice discount</div>
                                    <div class="flex gap-1">
                                        <div class="flex shrink-0 overflow-hidden rounded-md border border-gray-300 dark:border-white/10">
                                            <button
                                                type="button"
                                                x-on:click="draftType = 'fixed'"
                                                class="px-2 py-1 text-xs font-semibold"
                                                :class="draftType === 'fixed' ? 'bg-primary-600 text-white' : 'bg-white text-gray-500 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-white/10'"
                                            >Rs.</button>
                                            <button
                                                type="button"
                                                x-on:click="draftType = 'percentage'"
                                                class="px-2 py-1 text-xs font-semibold"
                                                :class="draftType === 'percentage' ? 'bg-primary-600 text-white' : 'bg-white text-gray-500 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-white/10'"
                                            >%</button>
                                        </div>
                                        <input
                                            type="number" min="0" x-model="draftValue"
                                            x-on:keydown.enter="apply"
                                            class="fi-input h-7 flex-1 rounded-md border-gray-300 py-0 text-sm dark:bg-gray-800"
                                            placeholder="0.00"
                                        >
                                    </div>
                                    <div class="flex justify-end gap-1.5">
                                        <template x-if="hasDiscount">
                                            <button type="button" x-on:click="clear()" class="rounded-md px-2 py-1 text-xs text-gray-500 hover:bg-gray-100 dark:hover:bg-white/10">Remove</button>
                                        </template>
                                        <button type="button" x-on:click="apply" class="rounded-md bg-primary-600 px-3 py-1 text-xs font-medium text-white hover:bg-primary-500">Apply</button>
                                    </div>
                                </div>
                            </div>
                        </div>
                    </template>
                    <div class="flex justify-between text-gray-500 dark:text-gray-400" x-show="vatEnabled">
                        <span>VAT</span>
                        <span class="tabular-nums" x-text="money(vatTotal)"></span>
                    </div>
                    <div class="flex items-baseline justify-between gap-2 border-t border-gray-200 pt-2 dark:border-white/10">
                        <span class="shrink-0 text-lg font-bold tracking-wide text-gray-500 uppercase dark:text-gray-400">Total</span>
                        <span class="whitespace-nowrap text-2xl font-bold tabular-nums text-primary-600 dark:text-primary-400" x-text="money(netTotal)"></span>
                    </div>
                    <template x-if="cartHasIncompleteTracking">
                        <p class="text-center text-xs font-medium text-amber-600">Select serial numbers for all items with serials on file before checkout.</p>
                    </template>
                    <template x-if="errors.length > 0 && ! checkoutOpen">
                        <ul class="rounded-lg bg-red-50 p-3 text-sm text-red-700 dark:bg-red-500/10">
                            <template x-for="error in errors" :key="error"><li x-text="error"></li></template>
                        </ul>
                    </template>
                    <template x-if="canCheckout">
                        <button
                            type="button"
                            x-on:click="openCheckout()"
                            :disabled="cart.length === 0"
                            class="w-full rounded-lg bg-primary-600 py-3.5 text-sm font-semibold text-white hover:bg-primary-500 disabled:cursor-not-allowed disabled:opacity-50"
                        >
                            Checkout <span class="ml-1.5 opacity-60">(F8)</span>
                        </button>
                    </template>
                    <div class="flex gap-2">
<button
    type="button"
    x-on:click="hold()"
    :disabled="cart.length === 0 || holding"
    class="flex flex-1 items-center justify-center gap-1 rounded-lg border-2 border-green-600 bg-green-100 py-3 text-sm font-semibold text-green-700 transition hover:bg-green-200 disabled:cursor-not-allowed disabled:opacity-100 dark:border-green-400 dark:bg-green-500/15 dark:text-green-300 dark:hover:bg-green-500/25 dark:disabled:opacity-50"
>
    <x-filament::icon
        icon="heroicon-m-pause-circle"
        class="h-4 w-4"
    />
    Hold <span class="opacity-70">(F4)</span>
</button>

<button
    type="button"
    x-on:click="resetCart()"
    :disabled="cart.length === 0"
    class="flex flex-1 items-center justify-center gap-1 rounded-lg border-2 border-purple-600 bg-purple-100 py-3 text-sm font-semibold text-purple-700 transition hover:bg-purple-200 disabled:cursor-not-allowed disabled:opacity-100 dark:border-purple-400 dark:bg-purple-500/15 dark:text-purple-300 dark:hover:bg-purple-500/25 dark:disabled:opacity-50"
>
    <x-filament::icon
        icon="heroicon-m-x-mark"
        class="h-4 w-4"
    />
    Clear Cart
</button>
                    </div>
                </div>
            </div>
        </div>

        {{-- ── Branch picker (blocking: no branch, no selling) ──────────────
             A sale, its stock movements and its held sales all belong to
             exactly one branch, so the terminal is inert until one is chosen.
             This never renders when a till session is open (the branch is
             locked to the till) or when the user has a single branch (it was
             selected automatically server-side).

             Sits at z-[70]: above the z-[60] dialogs — including "Open Till" —
             so a remembered till can never prompt ahead of the branch choice.
             All three utilities exist in the compiled stylesheet; do not swap
             in an arbitrary value without rebuilding (see docs/migration-plan.md). --}}
        <template x-if="branchRequired">
            <div class="fixed inset-0 z-[70] flex items-center justify-center bg-gray-900/60 p-4 backdrop-blur-sm">
                <div class="w-full max-w-sm rounded-xl bg-white p-6 shadow-xl dark:bg-gray-900">
                    <div class="mb-4 flex items-center gap-2">
                        <x-filament::icon icon="heroicon-m-building-library" class="h-5 w-5 text-primary-600 dark:text-primary-400" />
                        <h3 class="text-lg font-semibold">Select a branch</h3>
                    </div>
                    <p class="mb-4 text-sm text-gray-500 dark:text-gray-400">
                        Sales, stock and held sales are recorded against one branch. Choose the branch you are selling from to start.
                    </p>
                    <div class="space-y-2">
                        <template x-for="branch in branches" :key="branch.id">
                            <button
                                type="button"
                                x-on:click="chooseBranch(branch.id)"
                                :disabled="submitting"
                                class="flex w-full items-center justify-between rounded-lg border border-gray-300 px-4 py-3 text-start text-sm font-medium hover:border-primary-500 hover:bg-primary-50 disabled:opacity-50 dark:border-white/10 dark:hover:bg-primary-500/10"
                            >
                                <span x-text="branch.name"></span>
                                <x-filament::icon icon="heroicon-m-chevron-right" class="h-4 w-4 text-gray-400" />
                            </button>
                        </template>
                        <template x-if="branches.length === 0">
                            <p class="rounded-lg bg-amber-50 p-3 text-sm text-amber-700 dark:bg-amber-500/10 dark:text-amber-400">
                                You have no branch access. Ask an administrator to assign you to a branch before using the terminal.
                            </p>
                        </template>
                        <template x-if="errors.length">
                            <ul class="rounded-lg bg-red-50 p-3 text-sm text-red-700 dark:bg-red-500/10">
                                <template x-for="error in errors" :key="error"><li x-text="error"></li></template>
                            </ul>
                        </template>
                    </div>
                </div>
            </div>
        </template>

        {{-- ── Open till dialog (blocking until the shift is started) ─────── --}}
        <template x-if="! branchRequired && ! activeSession && !! selectedTillId">
            <div class="fixed inset-0 z-[60] flex items-center justify-center bg-gray-900/60 p-4 backdrop-blur-sm">
                <div class="w-full max-w-sm rounded-xl bg-white p-6 shadow-xl dark:bg-gray-900">

                    <h3
                        class="mb-1 text-lg font-semibold"
                        x-text="'Open Till' + (tills.find(t => t.id === selectedTillId)?.name ? ' — ' + tills.find(t => t.id === selectedTillId).name : '')"
                    ></h3>

                    <p class="mb-5 text-sm text-gray-500 dark:text-gray-400">
                        <template x-if="previousClosingBalance !== null">
                            <span>The opening balance carries forward from this till's previous closing balance.</span>
                        </template>
                        <template x-if="previousClosingBalance === null">
                            <span>Enter the amount of cash you are putting in the till at the start of this shift.</span>
                        </template>
                    </p>

                    <div class="space-y-4">

                        {{-- Carried-forward balance: the default, locked source of truth --}}
                        <template x-if="previousClosingBalance !== null && ! overrideOpeningBalance">
                            <div class="rounded-lg border border-primary-500/40 bg-primary-500/5 p-4 dark:border-primary-400/30">
                                <span class="block text-xs font-medium text-gray-500 dark:text-gray-400">
                                    Opening cash balance
                                </span>

                                <span
                                    class="mt-0.5 block text-2xl font-bold text-primary-600 dark:text-primary-400"
                                    x-text="money(previousClosingBalance)"
                                ></span>

                                <p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
                                    Carried forward from the previous shift's closing count.
                                </p>

                                <button
                                    type="button"
                                    x-on:click="overrideOpeningBalance = true"
                                    class="mt-3 text-xs font-semibold text-primary-600 hover:underline dark:text-primary-400"
                                >
                                    Use a different amount instead
                                </button>
                            </div>
                        </template>

                        {{-- Manual entry: only when there's nothing to carry forward, or the cashier is overriding it --}}
                        <template x-if="previousClosingBalance === null || overrideOpeningBalance">
                            <div class="rounded-lg border border-gray-200 p-4 dark:border-white/10">

                                <label class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-200">
                                    Opening cash balance
                                </label>

                                <input
                                    type="number"
                                    min="0"
                                    step="0.01"
                                    x-model.number="openingBalance"
                                    x-on:keydown.enter="startSession()"
                                    placeholder="Enter amount"
                                    class="fi-input h-12 w-full rounded-lg border-gray-300 text-xl font-semibold dark:bg-gray-800"
                                    autofocus
                                >

                                <template x-if="needsOpeningReason">
                                    <div class="mt-3">
                                        <label class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-200">
                                            Reason for the difference
                                        </label>

                                        <textarea
                                            x-model="openingNote"
                                            rows="2"
                                            placeholder="Why does this differ from the previous closing balance?"
                                            class="fi-input w-full rounded-lg border-gray-300 text-sm dark:bg-gray-800"
                                        ></textarea>
                                    </div>
                                </template>

                                <button
                                    type="button"
                                    x-show="previousClosingBalance !== null"
                                    x-on:click="overrideOpeningBalance = false; openingNote = ''; openingBalance = previousClosingBalance"
                                    class="mt-3 text-xs font-semibold text-gray-500 hover:underline dark:text-gray-400"
                                >
                                    Use previous closing balance instead
                                </button>
                            </div>
                        </template>

                        <template x-if="errors.length">
                            <ul class="rounded-lg bg-red-50 p-3 text-sm text-red-700 dark:bg-red-500/10">
                                <template x-for="error in errors" :key="error">
                                    <li x-text="error"></li>
                                </template>
                            </ul>
                        </template>

                        <button
                            type="button"
                            x-on:click="startSession()"
                            :disabled="submitting || !canOpenTill"
                            class="w-full rounded-lg bg-primary-600 py-2.5 text-sm font-semibold text-white hover:bg-primary-500 disabled:opacity-50"
                        >
                            <span x-text="submitting ? 'Opening…' : 'Open Till'"></span>
                        </button>

                    </div>
                </div>
            </div>
        </template>

        {{-- ── Close till dialog (cash reconciliation) ───────────────────── --}}
        <div x-show="closeTillOpen" x-cloak class="fixed inset-0 z-[60] flex items-center justify-center bg-gray-900/60 p-4 backdrop-blur-sm">
            <div x-on:click.outside="closeTillOpen = false" class="max-h-[90vh] w-full max-w-md overflow-y-auto rounded-xl bg-white p-6 shadow-xl dark:bg-gray-900">
                <h3 class="mb-4 text-lg font-semibold">Close Till Session</h3>
                <template x-if="reconciliation">
                    <div class="space-y-4">
                        <div class="rounded-lg border border-gray-200 bg-gray-50 dark:border-white/10 dark:bg-white/5">
                            <div class="border-b border-gray-200 px-4 py-3 text-sm font-medium dark:border-white/10">Cash Reconciliation</div>
                            <div class="space-y-2 px-4 py-3 text-sm">
                                <div class="flex justify-between text-gray-500 dark:text-gray-400"><span>Opening Float</span><span class="tabular-nums" x-text="money(reconciliation.opening_balance)"></span></div>
                                <div class="flex justify-between text-gray-500 dark:text-gray-400"><span>Cash Sales</span><span class="tabular-nums" x-text="'+' + money(reconciliation.sales_cash)"></span></div>
                                <div class="flex justify-between text-gray-500 dark:text-gray-400"><span>Cash Refunds</span><span class="tabular-nums" x-text="'-' + money(Math.abs(reconciliation.refunds))"></span></div>
                                <div class="flex justify-between text-gray-500 dark:text-gray-400"><span>Manual Cash In</span><span class="tabular-nums" x-text="'+' + money(reconciliation.cash_in)"></span></div>
                                <div class="flex justify-between text-gray-500 dark:text-gray-400"><span>Manual Cash Out</span><span class="tabular-nums" x-text="'-' + money(Math.abs(reconciliation.cash_out))"></span></div>
                                <div class="mt-2 flex justify-between border-t border-gray-200 pt-2 font-semibold dark:border-white/10"><span>Expected Balance</span><span class="tabular-nums" x-text="money(reconciliation.expected_balance)"></span></div>
                            </div>
                        </div>

                        <div>
                            <label class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-200">Counted cash (Actual in drawer)</label>
                            <input
                                type="number" min="0" step="0.01"
                                x-model.number="countedCash"
                                placeholder="Leave blank to use expected balance"
                                class="fi-input h-12 w-full rounded-lg border-gray-300 text-xl font-semibold dark:bg-gray-800"
                                autofocus
                            >
                        </div>
                        <template x-if="countedCash !== '' && countedCash !== null">
                            <div class="flex justify-between rounded px-3 py-2 text-sm font-medium" :class="hasVariance ? 'bg-red-50 text-red-700 dark:bg-red-500/10' : 'bg-green-50 text-green-700 dark:bg-green-500/10'">
                                <span>Variance</span>
                                <span class="tabular-nums" x-text="(variance > 0 ? '+' : '') + money(variance)"></span>
                            </div>
                        </template>

                        <div class="flex justify-end gap-2 pt-2">
                            <x-filament::button color="gray" x-on:click="closeTillOpen = false">Cancel</x-filament::button>
                            <x-filament::button color="danger" x-on:click="closeSession()" x-bind:disabled="submitting">
                                <span x-text="submitting ? 'Closing Till…' : 'Confirm & Close Till'"></span>
                            </x-filament::button>
                        </div>
                    </div>
                </template>
                <template x-if="! reconciliation">
                    <div class="py-8 text-center text-sm text-gray-500">
                        <x-filament::icon icon="heroicon-m-arrow-path" class="mx-auto mb-2 h-6 w-6 animate-spin text-gray-400" />
                        Loading reconciliation…
                    </div>
                </template>
            </div>
        </div>

        {{-- ── Withdraw / add cash dialog ────────────────────────────────── --}}
        <div x-show="withdrawOpen" x-cloak class="fixed inset-0 z-[60] flex items-center justify-center bg-gray-900/60 p-4 backdrop-blur-sm">
            <div x-on:click.outside="withdrawOpen = false" class="w-full max-w-sm rounded-xl bg-white p-6 shadow-xl dark:bg-gray-900">
                <h3 class="mb-4 text-lg font-semibold">Cash Movement</h3>
                <div class="space-y-3">
                    <div class="flex rounded-lg border border-gray-200 bg-gray-100/60 p-1 dark:border-white/10 dark:bg-white/5">
                        <button
                            type="button"
                            x-on:click="cmType = 'out'"
                            class="flex-1 rounded-md py-1.5 text-sm font-medium transition-colors"
                            :class="cmType === 'out' ? 'bg-white text-gray-900 shadow-sm dark:bg-gray-800 dark:text-gray-100' : 'text-gray-500 hover:bg-gray-200/60 dark:text-gray-400 dark:hover:bg-white/10'"
                        >Withdraw Cash</button>
                        <button
                            type="button"
                            x-on:click="cmType = 'in'"
                            class="flex-1 rounded-md py-1.5 text-sm font-medium transition-colors"
                            :class="cmType === 'in' ? 'bg-white text-gray-900 shadow-sm dark:bg-gray-800 dark:text-gray-100' : 'text-gray-500 hover:bg-gray-200/60 dark:text-gray-400 dark:hover:bg-white/10'"
                        >Add Cash</button>
                    </div>

                    <div>
                        <label class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-200">Amount</label>
                        <input type="number" min="0.01" step="0.01" x-model.number="cmAmount" placeholder="0.00" class="fi-input h-12 w-full rounded-lg border-gray-300 text-xl font-semibold dark:bg-gray-800" autofocus>
                        <template x-if="cmType === 'out'">
                            <p class="mt-1 flex items-center justify-between text-xs" :class="cmExceedsAvailable ? 'text-red-600 dark:text-red-400' : 'text-gray-500 dark:text-gray-400'">
                                <span x-text="'Available balance: ' + money(availableCash)"></span>
                                <span x-show="cmExceedsAvailable">exceeds available balance</span>
                            </p>
                        </template>
                    </div>

                    <template x-if="cmType === 'out'">
                        <div>
                            <label class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-200">Reason</label>
                            <select x-model.number="cmReasonId" class="fi-input w-full rounded-lg border-gray-300 dark:bg-gray-800">
                                <option :value="null">Select a reason…</option>
                                <template x-for="reason in cashMovementReasons" :key="reason.id">
                                    <option :value="reason.id" x-text="reason.name"></option>
                                </template>
                            </select>
                        </div>
                    </template>

                    <div>
                        <label class="mb-1 block text-sm font-medium text-gray-700 dark:text-gray-200" x-text="noteRequired ? 'Note (required for this reason)' : 'Note (optional)'"></label>
                        <textarea x-model="cmNote" rows="2" class="fi-input w-full rounded-lg border-gray-300 dark:bg-gray-800"></textarea>
                    </div>

                    <template x-if="errors.length">
                        <ul class="rounded-lg bg-red-50 p-3 text-sm text-red-700 dark:bg-red-500/10">
                            <template x-for="error in errors" :key="error"><li x-text="error"></li></template>
                        </ul>
                    </template>

                    <div class="flex justify-end gap-2 pt-1">
                        <x-filament::button color="gray" x-on:click="withdrawOpen = false">Cancel</x-filament::button>
                        <x-filament::button icon="heroicon-m-check" x-on:click="submitCashMovement()" x-bind:disabled="submitting || ! cmCanSubmit">
                            <span x-text="submitting ? 'Recording…' : (cmType === 'out' ? 'Withdraw Cash' : 'Add Cash')"></span>
                        </x-filament::button>
                    </div>
                </div>
            </div>
        </div>

        {{-- ── Cash movement recorded: 80mm receipt preview ───────────────
             The same cash-movements.print.thermal80 partial
             CashMovementPrintController serves as a bare document, rendered
             inline via cashMovementReceiptDocument() so the modal preview is
             exactly what lands on the roll. Printing is scoped to just
             #pos-cm-print by the shared print-only CSS below. --}}
        <div x-show="cmReceiptOpen" x-cloak class="fixed inset-0 z-[70] flex items-center justify-center bg-gray-900/60 p-4 backdrop-blur-sm">
            <div x-on:click.outside="closeCmReceipt()" class="flex max-h-[90vh] w-full max-w-sm flex-col overflow-hidden rounded-xl bg-white shadow-xl dark:bg-gray-900">
                <div class="flex items-center justify-between gap-3 border-b border-gray-200 px-5 py-4 dark:border-white/10">
                    <div class="flex min-w-0 items-center gap-3">
                        <span class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-green-100 text-green-600 dark:bg-green-500/10 dark:text-green-400">
                            <x-filament::icon icon="heroicon-o-banknotes" class="h-6 w-6" />
                        </span>
                        <div class="min-w-0">
                            <h3 class="text-base font-semibold leading-tight">Cash Movement Recorded</h3>
                            <span class="truncate font-mono text-sm text-gray-500 dark:text-gray-400" x-text="'Receipt #CM-' + String(cmMovement?.id ?? '').padStart(6, '0')"></span>
                        </div>
                    </div>
                    <button
                        type="button"
                        x-on:click="closeCmReceipt()"
                        class="flex h-9 w-9 shrink-0 items-center justify-center rounded-full text-gray-400 hover:bg-gray-100 hover:text-gray-600 dark:hover:bg-white/10 dark:hover:text-gray-200"
                        aria-label="Close"
                    >
                        <x-filament::icon icon="heroicon-o-x-mark" class="h-5 w-5" />
                    </button>
                </div>

                <div class="min-h-0 flex-1 overflow-auto bg-gray-100 p-4 dark:bg-gray-950">
                    <p x-show="cmReceiptLoading" class="px-5 py-6 text-center text-sm text-gray-500 dark:text-gray-400">
                        Loading receipt…
                    </p>
                    <div x-show="! cmReceiptLoading" class="mx-auto bg-white shadow" style="width: 80mm">
                        <div id="pos-cm-print" x-html="cmReceiptHtml"></div>
                    </div>
                </div>

                <div class="flex items-center justify-end gap-2 border-t border-gray-200 px-5 py-4 dark:border-white/10">
                    <x-filament::button color="gray" x-on:click="closeCmReceipt()">
                        Close
                    </x-filament::button>
                    <x-filament::button icon="heroicon-m-printer" x-on:click="printCmReceipt()">
                        Print
                    </x-filament::button>
                </div>
            </div>
        </div>

        {{-- ── Exchange / Fix (amend last sale) dialog ───────────────────── --}}
        <div x-show="amendOpen" x-cloak class="fixed inset-0 z-[60] flex items-center justify-center bg-gray-900/60 p-4 backdrop-blur-sm">
            <div x-on:click.outside="amendOpen = false" class="w-full max-w-md rounded-xl bg-white p-6 shadow-xl dark:bg-gray-900">
                <h3 class="text-lg font-semibold">Exchange or Correct Last Sale</h3>
                <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
                    Swap an item, fix a quantity, or change the payment method on the sale you just completed — no credit note needed.
                    The original receipt is voided and its items are restored to your cart so you can adjust and reprint a corrected receipt.
                </p>

                <div class="py-4">
                    <template x-if="amendLoading">
                        <div class="flex items-center justify-center p-4">
                            <x-filament::icon icon="heroicon-m-arrow-path" class="h-6 w-6 animate-spin text-gray-400" />
                        </div>
                    </template>
                    <template x-if="! amendLoading && amendError">
                        <p class="text-sm font-medium text-red-600 dark:text-red-400" x-text="amendError"></p>
                    </template>
                    <template x-if="! amendLoading && ! amendError && amendLastSale">
                        <div class="space-y-4">
                            <div class="rounded-lg border border-gray-200 p-4 text-sm dark:border-white/10">
                                <div class="flex justify-between font-medium">
                                    <span x-text="amendLastSale.invoice_number"></span>
                                    <span class="tabular-nums" x-text="money(Number(amendLastSale.net_total) || 0)"></span>
                                </div>
                                <div class="mt-1 flex justify-between text-xs text-gray-500 dark:text-gray-400">
                                    <span x-text="amendLastSale.customer?.business_name ?? 'Walk-in Customer'"></span>
                                    <span x-text="amendLastSale.created_at ? new Date(amendLastSale.created_at).toLocaleTimeString() : ''"></span>
                                </div>
                            </div>
                            <template x-if="! amendCanAmend">
                                <p class="text-sm font-medium text-red-600 dark:text-red-400">This sale is past the allowed amendment window or amendment is disabled.</p>
                            </template>
                        </div>
                    </template>
                    <template x-if="! amendLoading && ! amendError && ! amendLastSale">
                        <p class="text-sm text-gray-500 dark:text-gray-400">No recent sales found for this session.</p>
                    </template>
                </div>

                <div class="flex justify-end gap-2">
                    <x-filament::button color="gray" x-on:click="amendOpen = false">Cancel</x-filament::button>
                    <x-filament::button color="danger" x-on:click="confirmAmend()" x-bind:disabled="! amendLastSale || ! amendCanAmend || amendLoading">
                        <span x-text="amendLoading ? 'Processing…' : 'Confirm Exchange / Correction'"></span>
                    </x-filament::button>
                </div>
            </div>
        </div>

        {{-- ── Held sales sheet ──────────────────────────────────────────── --}}
        <div x-show="heldOpen" x-cloak class="fixed inset-0 z-[60] flex justify-end bg-gray-900/60 backdrop-blur-sm">
            <div x-on:click.outside="heldOpen = false" class="flex h-full w-full max-w-md flex-col bg-white shadow-xl dark:bg-gray-900">
                <div class="flex items-center justify-between border-b border-gray-200 px-5 py-4 dark:border-white/10">
                    <h3 class="text-lg font-semibold">Held Sales</h3>
                    <button type="button" class="text-gray-500 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-100" x-on:click="heldOpen = false">
                        <x-filament::icon icon="heroicon-m-x-mark" class="h-5 w-5" />
                    </button>
                </div>
                <div class="flex-1 overflow-y-auto p-4">
                    <template x-if="heldSales.length === 0">
                        <div class="flex flex-col items-center justify-center py-16 text-sm text-gray-500 dark:text-gray-400">
                            <x-filament::icon icon="heroicon-m-pause-circle" class="mb-2 h-8 w-8 opacity-40" />
                            No held sales
                        </div>
                    </template>
                    <div class="space-y-2">
                        <template x-for="sale in heldSales" :key="sale.id">
                            <div class="space-y-2 rounded-lg border border-gray-200 bg-gray-50/50 p-3 dark:border-white/10 dark:bg-white/5">
                                <div class="flex items-center justify-between">
                                    <span class="font-mono text-sm font-semibold" x-text="sale.reference"></span>
                                    <span class="text-sm font-semibold text-primary-600 tabular-nums dark:text-primary-400" x-text="money(sale.total)"></span>
                                </div>
                                <div class="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-gray-500 dark:text-gray-400">
                                    <span x-text="formatQty(sale.item_count) + ' item(s)'"></span>
                                    <template x-if="sale.customer_name">
                                        <span class="flex items-center gap-1">
                                            <x-filament::icon icon="heroicon-m-user" class="h-3 w-3" />
                                            <span x-text="sale.customer_name"></span>
                                        </span>
                                    </template>
                                    <span class="flex items-center gap-1">
                                        <x-filament::icon icon="heroicon-m-clock" class="h-3 w-3" />
                                        <span x-text="timeAgo(sale.held_at)"></span>
                                        <template x-if="sale.cashier_name"><span x-text="'· ' + sale.cashier_name"></span></template>
                                    </span>
                                </div>
                                <div class="flex gap-2">
                                    <button type="button" x-on:click="resumeRequest(sale)" class="flex-1 rounded-md bg-primary-600 px-3 py-2 text-sm font-medium text-white hover:bg-primary-500">Resume</button>
                                    <button type="button" x-on:click="confirmDiscardId = sale.id" class="rounded-md px-2 py-1.5 text-red-600 hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-500/10">
                                        <x-filament::icon icon="heroicon-m-trash" class="h-4 w-4" />
                                    </button>
                                </div>
                            </div>
                        </template>
                    </div>
                </div>
            </div>
        </div>

        {{-- Resume-with-cart confirm ── --}}
        <div x-show="confirmResumeId !== null" x-cloak class="fixed inset-0 z-[100] flex items-center justify-center bg-gray-900/60 p-4 backdrop-blur-sm">
            <div class="w-full max-w-sm rounded-xl bg-white p-6 shadow-xl dark:bg-gray-900">
                <h3 class="text-lg font-semibold">Replace the current cart?</h3>
                <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">The cart in progress has items in it. Resuming this held sale will replace it — nothing in the current cart is saved.</p>
                <div class="mt-5 flex justify-end gap-2">
                    <x-filament::button color="gray" x-on:click="confirmResumeId = null">Cancel</x-filament::button>
                    <x-filament::button x-on:click="resume(heldSales.find(s => s.id === confirmResumeId)); confirmResumeId = null">Resume</x-filament::button>
                </div>
            </div>
        </div>

        {{-- Discard confirm ── --}}
        <div x-show="confirmDiscardId !== null" x-cloak class="fixed inset-0 z-[100] flex items-center justify-center bg-gray-900/60 p-4 backdrop-blur-sm">
            <div class="w-full max-w-sm rounded-xl bg-white p-6 shadow-xl dark:bg-gray-900">
                <h3 class="text-lg font-semibold">Discard this held sale?</h3>
                <p class="mt-1 text-sm text-gray-500 dark:text-gray-400">This cannot be undone — the held cart will be permanently removed.</p>
                <div class="mt-5 flex justify-end gap-2">
                    <x-filament::button color="gray" x-on:click="confirmDiscardId = null">Cancel</x-filament::button>
                    <x-filament::button color="danger" x-on:click="discard(heldSales.find(s => s.id === confirmDiscardId)); confirmDiscardId = null">Discard</x-filament::button>
                </div>
            </div>
        </div>

        {{-- ── Checkout sheet ────────────────────────────────────────────── --}}
        <div x-show="checkoutOpen" x-cloak class="fixed inset-0 z-[60] flex justify-end bg-gray-900/60 backdrop-blur-sm">
            <div x-on:click.outside="checkoutOpen = false" class="flex h-full w-full max-w-xl flex-col bg-white shadow-xl dark:bg-gray-900">
                <div class="flex items-center justify-between border-b border-gray-200 px-5 py-4 dark:border-white/10">
                    <h3 class="text-lg font-semibold">Checkout</h3>
                    <button type="button" class="text-gray-500 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-100" x-on:click="checkoutOpen = false">
                        <x-filament::icon icon="heroicon-m-x-mark" class="h-5 w-5" />
                    </button>
                </div>

                <div class="flex-1 space-y-4 overflow-y-auto p-5">
                    <template x-if="errors.length">
                        <ul class="rounded-lg bg-red-50 p-3 text-sm text-red-700 dark:bg-red-500/10">
                            <template x-for="error in errors" :key="error"><li x-text="error"></li></template>
                        </ul>
                    </template>

                    {{-- Order summary ── --}}
                    <section class="rounded-xl border border-gray-200 p-4 dark:border-white/10">
                        <div class="mb-2 flex items-center gap-2">
                            <x-filament::icon icon="heroicon-m-squares-2x2" class="h-4 w-4 text-gray-500 dark:text-gray-400" />
                            <span class="text-sm font-semibold" x-text="formatQty(cartCount) + ' item(s)'"></span>
                        </div>
                        <div class="space-y-1.5 text-base">
                            <div class="flex justify-between text-gray-500 dark:text-gray-400">
                                <span>Subtotal</span>
                                <span class="tabular-nums" x-text="money(grossSubtotal)"></span>
                            </div>
                            <template x-if="lineDiscountTotal > 0">
                                <div class="flex justify-between text-green-700 dark:text-green-400">
                                    <span>Product discounts</span>
                                    <span class="tabular-nums" x-text="'-' + money(lineDiscountTotal)"></span>
                                </div>
                            </template>
                            <div class="flex items-center gap-2 pt-1">
                                <x-filament::icon icon="heroicon-m-tag" class="h-3.5 w-3.5 shrink-0 text-gray-500 dark:text-gray-400" />
                                <span class="flex-1 text-base text-gray-500 dark:text-gray-400">Invoice discount</span>
                                <template x-if="canCartDiscount">
                                    <div class="relative inline-block" x-data="discountPopover({
                                            get type() { return discountType },
                                            get value() { return discountValue },
                                            onapply: (type, value) => { discountType = type; discountValue = value },
                                            onclear: () => { discountType = 'fixed'; discountValue = 0 },
                                        })">
                                        <button
                                            type="button"
                                            x-on:click="toggle"
                                            class="rounded border px-2 py-1 text-xs font-medium transition-colors"
                                            :class="hasDiscount ? 'border-green-600 bg-green-600/10 text-green-700' : 'text-gray-500 hover:bg-gray-200 dark:text-gray-400 dark:hover:bg-white/10'"
                                        >
                                            <span x-text="hasDiscount ? (type === 'percentage' ? '-' + value + '%' : '-' + money(value)) : 'Discount'"></span>
                                        </button>
                                        <div x-show="open" x-cloak class="absolute z-30 mt-1 w-56 space-y-2 rounded-lg border border-gray-200 bg-white p-3 shadow-lg dark:border-white/10 dark:bg-gray-900">
                                            <div class="text-xs font-semibold">Invoice discount</div>
                                            <div class="flex gap-1">
                                                <div class="flex shrink-0 overflow-hidden rounded-md border border-gray-300 dark:border-white/10">
                                                    <button type="button" x-on:click="draftType = 'fixed'" class="px-2 py-1 text-xs font-semibold" :class="draftType === 'fixed' ? 'bg-primary-600 text-white' : 'bg-white text-gray-500 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-white/10'">Rs.</button>
                                                    <button type="button" x-on:click="draftType = 'percentage'" class="px-2 py-1 text-xs font-semibold" :class="draftType === 'percentage' ? 'bg-primary-600 text-white' : 'bg-white text-gray-500 hover:bg-gray-100 dark:bg-gray-800 dark:hover:bg-white/10'">%</button>
                                                </div>
                                                <input type="number" min="0" x-model="draftValue" x-on:keydown.enter="apply" class="fi-input h-7 flex-1 rounded-md border-gray-300 py-0 text-sm dark:bg-gray-800" placeholder="0.00">
                                            </div>
                                            <div class="flex justify-end gap-1.5">
                                                <template x-if="hasDiscount">
                                                    <button type="button" x-on:click="clear()" class="rounded-md px-2 py-1 text-xs text-gray-500 hover:bg-gray-100 dark:hover:bg-white/10">Remove</button>
                                                </template>
                                                <button type="button" x-on:click="apply" class="rounded-md bg-primary-600 px-3 py-1 text-xs font-medium text-white hover:bg-primary-500">Apply</button>
                                            </div>
                                        </div>
                                    </div>
                                </template>
                            </div>
                        </div>
                    </section>

                    {{-- Customer ── --}}
                    <template x-if="! walkinOnly">
                        <section class="space-y-3 rounded-xl border border-gray-200 p-4 dark:border-white/10">
                            <div class="flex items-center justify-between">
                                <div class="flex items-center gap-2">
                                    <x-filament::icon icon="heroicon-m-user" class="h-4 w-4 text-green-600" />
                                    <span class="text-sm font-semibold">Customer</span>
                                </div>
                                <template x-if="selectedCustomer">
                                    <button type="button" class="text-xs text-green-700 hover:underline" x-on:click="selectCustomer('')">Change</button>
                                </template>
                            </div>
                            <template x-if="selectedCustomer">
                                <div class="flex items-center gap-3">
                                    <div class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-green-100 text-sm font-bold text-green-700" x-text="(selectedCustomer.business_name[0] || '?').toUpperCase()"></div>
                                    <div>
                                        <div class="text-sm font-semibold" x-text="selectedCustomer.business_name"></div>
                                        <template x-if="selectedCustomer.phone"><div class="mt-0.5 text-xs text-gray-500 dark:text-gray-400" x-text="selectedCustomer.phone"></div></template>
                                    </div>
                                </div>
                            </template>
                            <template x-if="! selectedCustomer">
                                <div class="relative" x-data="alpineCombobox({
                                        get options() { return customers.map(c => ({ id: c.id, name: c.business_name, phone: c.phone, nic: c.nic })) },
                                        get value() { return customerId },
                                        placeholder: 'Walk-in Customer',
                                        searchPlaceholder: 'Search customers…',
                                        onchange: (id) => selectCustomer(id),
                                    })" x-on:click.outside="open = false">
                                    <button
                                        type="button"
                                        x-on:click="toggle"
                                        class="flex h-9 w-full items-center justify-between rounded-md border border-gray-300 bg-white px-3 text-sm shadow-sm hover:border-primary-400 dark:border-white/10 dark:bg-gray-800"
                                    >
                                        <span class="truncate text-gray-500 dark:text-gray-400" x-text="selected ? selected.name : placeholder"></span>
                                        <x-filament::icon icon="heroicon-m-chevron-up-down" class="ms-2 h-4 w-4 shrink-0 text-gray-400" />
                                    </button>
                                    <div x-show="open" x-cloak class="absolute start-0 end-0 top-[calc(100%+4px)] z-30 overflow-hidden rounded-lg border border-gray-200 bg-white shadow-lg dark:border-white/10 dark:bg-gray-900">
                                        <div class="flex items-center gap-2 border-b border-gray-100 px-3 py-2 dark:border-white/10">
                                            <x-filament::icon icon="heroicon-m-magnifying-glass" class="h-4 w-4 shrink-0 text-gray-400" />
                                            <input
                                                x-ref="search"
                                                type="text"
                                                x-model="query"
                                                x-on:keydown.enter="select(filtered[highlighted] ?? null)"
                                                x-on:keydown.arrow-down.prevent="highlighted = Math.min(highlighted + 1, filtered.length - 1)"
                                                x-on:keydown.arrow-up.prevent="highlighted = Math.max(highlighted - 1, 0)"
                                                x-on:keydown.escape="open = false"
                                                :placeholder="searchPlaceholder"
                                                class="flex-1 bg-transparent text-sm outline-none"
                                                autocomplete="off"
                                            >
                                        </div>
                                        <div class="max-h-56 overflow-y-auto py-1">
                                            <template x-if="filtered.length === 0">
                                                <p class="px-3 py-2 text-center text-sm text-gray-500">No results found.</p>
                                            </template>
                                            <template x-for="(option, i) in filtered" :key="option.id">
                                                <button
                                                    type="button"
                                                    role="option"
                                                    x-on:mousedown.prevent
                                                    x-on:click="select(option)"
                                                    x-on:mouseenter="highlighted = i"
                                                    class="flex w-full items-center gap-2 px-3 py-1.5 text-sm hover:bg-gray-50 dark:hover:bg-white/5"
                                                    :class="highlighted === i ? 'bg-gray-50 dark:bg-white/5' : ''"
                                                >
                                                    <x-filament::icon icon="heroicon-m-check" class="h-4 w-4 shrink-0" x-bind:class="selected && String(option.id) === String(selected.id) ? '' : 'opacity-0'" />
                                                    <span x-text="option.name"></span>
                                                </button>
                                            </template>
                                        </div>
                                    </div>
                                </div>
                            </template>
                        </section>
                    </template>

                    {{-- Credit notes ── --}}
                    <template x-if="! walkinOnly && creditNotes.length > 0">
                        <section class="space-y-3 rounded-xl border border-gray-200 p-4 dark:border-white/10">
                            <div class="flex items-center justify-between">
                                <div class="flex items-center gap-2">
                                    <x-filament::icon icon="heroicon-m-document-check" class="h-4 w-4 text-green-600" />
                                    <span class="text-sm font-semibold">Apply Credit Notes</span>
                                </div>
                                <span class="rounded-full bg-gray-100 px-2 py-0.5 text-xs font-medium text-gray-600 dark:bg-white/10 dark:text-gray-300" x-text="creditNotes.length + ' available'"></span>
                            </div>
                            <div class="space-y-2">
                                <template x-for="cn in creditNotes" :key="cn.id">
                                    <label
                                        class="flex cursor-pointer items-center gap-3 rounded-lg border p-3 transition-colors"
                                        :class="selectedCreditNotes.includes(cn.id) ? 'border-green-600 bg-green-50 dark:bg-green-950/30' : 'hover:bg-gray-50 dark:hover:bg-white/5'"
                                    >
                                        <input
                                            type="checkbox"
                                            class="h-4 w-4 accent-green-700"
                                            :checked="selectedCreditNotes.includes(cn.id)"
                                            x-on:change="toggleCreditNote(cn.id, $event.target.checked)"
                                        >
                                        <div class="flex-1 text-sm font-medium">
                                            <span x-text="cn.cn_number"></span>
                                            {{-- `cn.amount` is what is LEFT on the note, not its
                                                 face value: a note part-spent on an earlier sale
                                                 stays available for its remainder. Show where the
                                                 rest went so the figure never looks like a
                                                 mistake to the cashier. --}}
                                            <template x-if="Number(cn.applied_amount) > 0">
                                                <span class="block text-[11px] font-normal text-gray-500 dark:text-gray-400"
                                                      x-text="'of ' + money(cn.face_amount) + ' — ' + money(cn.applied_amount) + ' already used'"></span>
                                            </template>
                                        </div>
                                        <span class="text-sm font-semibold tabular-nums" :class="selectedCreditNotes.includes(cn.id) ? 'text-green-700' : 'text-gray-500 dark:text-gray-400'" x-text="money(cn.amount)"></span>
                                    </label>
                                </template>
                                <template x-if="creditNotesAmount > 0">
                                    <div class="flex justify-between border-t border-gray-200 pt-1 text-sm dark:border-white/10">
                                        <span class="text-gray-500 dark:text-gray-400">Total credit applied</span>
                                        <span class="font-semibold text-green-700 tabular-nums" x-text="'-' + money(creditNotesAmount)"></span>
                                    </div>
                                </template>
                                <template x-if="creditRemainder > 0">
                                    {{-- The unused part is simply not spent: a
                                         note keeps its own remaining balance
                                         now, rather than being retired and
                                         reissued as a fresh note for the
                                         leftover. --}}
                                    <p class="pt-0.5 text-xs text-gray-500 dark:text-gray-400" x-text="money(creditRemainder) + ' stays on the credit note for next time.'"></p>
                                </template>
                            </div>
                        </section>
                    </template>

                    {{-- Voucher — unmounts entirely when the Master Control
                         "Vouchers & promotions" switch is off OR the cashier
                         lacks the `pos.voucher` permission, so a cashier
                         without it can never see or type into a dead field.
                         (The checkout payload is refused server-side too.) --}}
                    <template x-if="vouchersEnabled && canVoucher">
                        <section class="space-y-3 rounded-xl border border-gray-200 p-4 dark:border-white/10">
                            <div class="flex items-center gap-2">
                                <x-filament::icon icon="heroicon-m-tag" class="h-4 w-4 text-primary-600 dark:text-primary-400" />
                                <span class="text-sm font-semibold">Voucher Code</span>
                            </div>
                            <template x-if="appliedVoucher">
                            <div class="flex items-center justify-between rounded-lg border border-primary-500/40 bg-primary-500/5 p-3">
                                <div>
                                    <div class="text-sm font-medium" x-text="appliedVoucher.name"></div>
                                    <div class="font-mono text-xs" x-text="appliedVoucher.code"></div>
                                </div>
                                <div class="flex items-center gap-2">
                                    <span class="text-sm font-semibold text-primary-600 tabular-nums dark:text-primary-400" x-text="'-' + money(appliedVoucher.discount)"></span>
                                    <button type="button" x-on:click="clearVoucher()" class="text-gray-500 hover:text-red-600 dark:text-gray-400 dark:hover:text-red-400">
                                        <x-filament::icon icon="heroicon-m-x-mark" class="h-4 w-4" />
                                    </button>
                                </div>
                            </div>
                        </template>
                        <template x-if="! appliedVoucher">
                            <div class="flex gap-2">
                                <input
                                    type="text"
                                    x-model="voucherCode"
                                    x-on:keydown.enter="checkVoucher()"
                                    placeholder="Enter voucher code"
                                    class="fi-input flex-1 rounded-lg border-gray-300 text-sm dark:bg-gray-800"
                                >
                                <button
                                    type="button"
                                    x-on:click="checkVoucher()"
                                    :disabled="voucherChecking || ! voucherCode.trim()"
                                    class="rounded-lg border border-gray-300 px-4 py-2 text-sm font-medium hover:bg-gray-100 disabled:opacity-50 dark:border-white/10 dark:hover:bg-white/5"
                                >
                                    <span x-text="voucherChecking ? 'Checking…' : 'Apply'"></span>
                                </button>
                            </div>
                        </template>
                        <template x-if="voucherError">
                            <p class="text-xs text-red-600 dark:text-red-400" x-text="voucherError"></p>
                        </template>
                        </section>
                    </template>

                    {{-- Loyalty points — same rule: with the Master Control
                         "Loyalty programs" switch off, or without the
                         `pos.loyalty_redeem` permission, the whole section is
                         absent from the DOM, not merely disabled. --}}
                    <template x-if="loyaltyEnabled && canRedeemLoyalty && loyaltyProgram && selectedCustomer && loyaltyBalance > 0">
                        <section class="space-y-3 rounded-xl border border-gray-200 p-4 dark:border-white/10">
                            <div class="flex items-center justify-between">
                                <div class="flex items-center gap-2">
                                    <x-filament::icon icon="heroicon-m-trophy" class="h-4 w-4 text-primary-600 dark:text-primary-400" />
                                    <span class="text-sm font-semibold">Redeem Loyalty Points</span>
                                </div>
                                <span class="text-xs text-gray-500 dark:text-gray-400" x-text="'Balance: ' + loyaltyBalance.toLocaleString() + ' pts'"></span>
                            </div>
                            <div class="flex gap-2">
                                <input
                                    type="number" min="0"
                                    :max="loyaltyBalance"
                                    x-model="redeemPointsInput"
                                    :placeholder="'Up to ' + loyaltyBalance.toLocaleString() + ' pts'"
                                    class="fi-input flex-1 rounded-lg border-gray-300 text-sm dark:bg-gray-800"
                                >
                                <template x-if="redeemPoints > 0">
                                    <span class="flex items-center text-sm font-semibold text-primary-600 whitespace-nowrap tabular-nums dark:text-primary-400" x-text="'-' + money(loyaltyDiscount)"></span>
                                </template>
                            </div>
                            <template x-if="redeemPointsRequested > 0 && ! meetsLoyaltyMinimum">
                                <p class="text-xs text-red-600 dark:text-red-400" x-text="'Minimum ' + loyaltyProgram.minimum_points_to_redeem + ' points required to redeem.'"></p>
                            </template>
                        </section>
                    </template>

                    {{-- Payment ── --}}
                    <section class="space-y-4 rounded-xl border border-gray-200 p-4 dark:border-white/10">
                        <div class="flex items-center justify-between">
                            <div class="flex items-center gap-2">
                                <x-filament::icon icon="heroicon-m-banknotes" class="h-4 w-4 text-green-600" />
                                <span class="text-sm font-semibold">Payment</span>
                            </div>
                            <span class="text-xs text-gray-500 dark:text-gray-400">
                                Remaining:
                                <span :class="remaining > 0 ? 'font-semibold text-red-600 dark:text-red-400' : 'font-semibold text-green-700'">
                                    <span class="tabular-nums" x-text="money(remaining)"></span>
                                </span>
                            </span>
                        </div>

                        <template x-if="payments.length > 0">
                            <div class="space-y-1.5">
                                <template x-for="(payment, i) in payments" :key="i">
                                    <div class="flex items-center gap-2 rounded-lg border border-gray-200 bg-gray-50/60 px-3 py-2 dark:border-white/10 dark:bg-white/5">
                                        <span class="flex items-center">
                                            <x-filament::icon x-show="methodIcon(payment.method) === 'heroicon-m-banknotes'" icon="heroicon-m-banknotes" class="h-4 w-4 text-green-700" />
                                            <x-filament::icon x-show="methodIcon(payment.method) === 'heroicon-m-credit-card'" icon="heroicon-m-credit-card" class="h-4 w-4 text-green-700" />
                                            <x-filament::icon x-show="methodIcon(payment.method) === 'heroicon-m-building-library'" icon="heroicon-m-building-library" class="h-4 w-4 text-green-700" />
                                            <x-filament::icon x-show="methodIcon(payment.method) === 'heroicon-m-document-text'" icon="heroicon-m-document-text" class="h-4 w-4 text-green-700" />
                                            <x-filament::icon x-show="methodIcon(payment.method) === 'heroicon-m-wallet'" icon="heroicon-m-wallet" class="h-4 w-4 text-green-700" />
                                        </span>
                                        <span class="flex-1 text-sm" x-text="methodLabel(payment.method)"></span>
                                        <template x-if="payment.cheque_number"><span class="text-xs text-gray-500 dark:text-gray-400" x-text="payment.cheque_number"></span></template>
                                        <span class="text-sm font-semibold tabular-nums" x-text="money(payment.amount)"></span>
                                        <button type="button" x-on:click="payments.splice(i, 1)" class="text-red-600 hover:opacity-70 dark:text-red-400">
                                            <x-filament::icon icon="heroicon-m-x-mark" class="h-3.5 w-3.5" />
                                        </button>
                                    </div>
                                </template>
                                <div class="flex justify-between border-t border-gray-200 pt-1 text-xs dark:border-white/10">
                                    <span class="text-gray-500 dark:text-gray-400">Total paid</span>
                                    <span class="font-semibold text-green-700 tabular-nums" x-text="money(tendered)"></span>
                                </div>
                            </div>
                        </template>

                        <div class="grid grid-cols-4 gap-1.5">
                            <template x-for="method in paymentMethods" :key="method.code">
                                <button
                                    type="button"
                                    x-on:click="activeMethod = method.code; customFieldValues = {}"
                                    class="flex flex-col items-center gap-1 rounded-lg border px-2 py-3 text-xs font-medium transition-colors"
                                    :class="activeMethod === method.code ? 'border-green-600 bg-green-700/10 text-green-700' : 'border-gray-200 text-gray-500 hover:bg-gray-50 dark:border-white/10 dark:text-gray-400 dark:hover:bg-white/5'"
                                >
                                    <span class="flex items-center">
                                        <x-filament::icon x-show="method.icon === 'heroicon-m-banknotes'" icon="heroicon-m-banknotes" class="h-4 w-4" />
                                        <x-filament::icon x-show="method.icon === 'heroicon-m-credit-card'" icon="heroicon-m-credit-card" class="h-4 w-4" />
                                        <x-filament::icon x-show="method.icon === 'heroicon-m-building-library'" icon="heroicon-m-building-library" class="h-4 w-4" />
                                        <x-filament::icon x-show="method.icon === 'heroicon-m-document-text'" icon="heroicon-m-document-text" class="h-4 w-4" />
                                        <x-filament::icon x-show="method.icon === 'heroicon-m-wallet'" icon="heroicon-m-wallet" class="h-4 w-4" />
                                    </span>
                                    <span x-text="method.label"></span>
                                </button>
                            </template>
                        </div>

                        <div class="space-y-3 rounded-lg border border-gray-200 bg-gray-50/60 p-4 dark:border-white/10 dark:bg-white/5">
                            <template x-if="activeMethod === 'cheque'">
                                <div class="grid grid-cols-2 gap-3">
                                    <div>
                                        <label class="mb-1 block text-xs text-gray-500 dark:text-gray-400">
                                            Cheque No.
                                            <template x-if="activeMethodConfig?.requiresChequeFields"><span class="text-red-600"> *</span></template>
                                        </label>
                                        <input x-model="pmChequeNumber" class="fi-input w-full rounded border-gray-300 px-2 py-1.5 text-sm dark:bg-gray-800">
                                    </div>
                                    <div>
                                        <label class="mb-1 block text-xs text-gray-500 dark:text-gray-400">
                                            Bank
                                            <template x-if="activeMethodConfig?.requiresChequeFields"><span class="text-red-600"> *</span></template>
                                        </label>
                                        <input x-model="pmChequeBank" class="fi-input w-full rounded border-gray-300 px-2 py-1.5 text-sm dark:bg-gray-800">
                                    </div>
                                    <div>
                                        <label class="mb-1 block text-xs text-gray-500 dark:text-gray-400">Branch</label>
                                        <input x-model="pmChequeBranch" class="fi-input w-full rounded border-gray-300 px-2 py-1.5 text-sm dark:bg-gray-800">
                                    </div>
                                    <div>
                                        <label class="mb-1 block text-xs text-gray-500 dark:text-gray-400">
                                            Maturity Date
                                            <template x-if="activeMethodConfig?.requiresChequeFields"><span class="text-red-600"> *</span></template>
                                        </label>
                                        <input type="date" x-model="pmChequeDate" class="fi-input w-full rounded border-gray-300 px-2 py-1.5 text-sm dark:bg-gray-800">
                                    </div>
                                </div>
                            </template>
                            <template x-if="activeMethod === 'card' || activeMethod === 'bank_transfer' || (activeMethodConfig?.requiresReference && activeMethod !== 'cheque')">
                                <div>
                                    <label class="mb-1 block text-xs text-gray-500 dark:text-gray-400">
                                        Reference / Transaction ID
                                        <template x-if="activeMethodConfig?.requiresReference"><span class="text-red-600"> *</span></template>
                                    </label>
                                    <input x-model="pmReference" class="fi-input w-full rounded border-gray-300 px-2 py-1.5 text-sm dark:bg-gray-800">
                                </div>
                            </template>
                            <template x-if="activeCustomMethod">
                                <template x-for="field in activeCustomMethod.fields ?? []" :key="field.key">
                                    <div>
                                        <label class="mb-1 block text-xs text-gray-500 dark:text-gray-400">
                                            <span x-text="field.label"></span>
                                            <template x-if="field.required"><span class="text-red-600"> *</span></template>
                                        </label>
                                        <template x-if="field.type === 'dropdown'">
                                            <select x-model="customFieldValues[field.key]" class="fi-input w-full rounded border-gray-300 px-2 py-1.5 text-sm dark:bg-gray-800">
                                                <option value="">Select…</option>
                                                <template x-for="opt in field.options ?? []" :key="opt">
                                                    <option :value="opt" x-text="opt"></option>
                                                </template>
                                            </select>
                                        </template>
                                        <template x-if="field.type === 'file'">
                                            <div class="flex items-center gap-2">
                                                <input
                                                    type="file"
                                                    class="fi-input w-full rounded border-gray-300 px-2 py-1.5 text-sm dark:bg-gray-800"
                                                    :disabled="uploadingField !== null"
                                                    x-on:change="uploadCustomFieldFile(field, $event.target.files[0])"
                                                >
                                                <template x-if="uploadingField === field.key">
                                                    <x-filament::icon icon="heroicon-m-arrow-path" class="h-4 w-4 shrink-0 animate-spin text-gray-400" />
                                                </template>
                                                <template x-if="customFieldValues[field.key] && uploadingField !== field.key">
                                                    <x-filament::icon icon="heroicon-m-check-circle" class="h-4 w-4 shrink-0 text-green-600" />
                                                </template>
                                            </div>
                                        </template>
                                        <template x-if="field.type !== 'dropdown' && field.type !== 'file'">
                                            <input
                                                :type="field.type === 'number' ? 'number' : 'text'"
                                                x-model="customFieldValues[field.key]"
                                                class="fi-input w-full rounded border-gray-300 px-2 py-1.5 text-sm dark:bg-gray-800"
                                            >
                                        </template>
                                    </div>
                                </template>
                            </template>
                            <div class="rounded-xl border-2 border-green-600 bg-green-50 p-4 dark:border-green-950/30">
                                <div class="mb-2 flex items-center gap-2">
                                    <x-filament::icon icon="heroicon-m-banknotes" class="h-5 w-5 shrink-0 text-green-700 dark:text-green-400" />
                                    <label class="text-base font-black tracking-wide text-green-800 uppercase dark:text-green-400">Enter Amount Paid</label>
                                </div>
                                <input
                                    type="number" min="0.01" step="0.01"
                                    x-model="pmAmount"
                                    x-ref="paymentAmountInput"
                                    :placeholder="remaining.toFixed(2)"
                                    x-on:wheel="$event.target.blur()"
                                    class="w-full rounded-lg border-2 border-green-600 bg-white px-4 py-4 text-center text-3xl font-black tabular-nums text-gray-900 shadow-sm outline-none placeholder:text-gray-300 focus:ring-4 focus:ring-green-600/30 dark:bg-gray-800 dark:border-green-500 dark:text-gray-100"
                                >
                                <div class="mt-2 flex items-center justify-between gap-2">
                                    <span class="text-sm font-bold text-green-800 dark:text-green-400">
                                        Remaining:
                                        <span class="tabular-nums" x-text="money(remaining)"></span>
                                    </span>
                                    <button
                                        type="button"
                                        x-on:click="pmAmount = remaining.toFixed(2)"
                                        class="rounded-md border-2 border-green-600 bg-white px-4 py-1.5 text-xs font-bold text-green-700 hover:bg-green-100 dark:border-green-400 dark:bg-gray-800 dark:text-green-400 dark:hover:bg-white/10"
                                    >
                                        Exact
                                    </button>
                                </div>
                                <template x-if="cashChange > 0">
                                    <div class="mt-3 rounded-lg border-2 border-green-600 bg-white p-3 text-center dark:bg-gray-800">
                                        <div class="text-xs font-black tracking-widest text-green-700 uppercase dark:text-green-400">Change to Return</div>
                                        <div class="text-4xl leading-tight font-black tabular-nums text-green-700 dark:text-green-400" x-text="money(cashChange)"></div>
                                    </div>
                                </template>
                            </div>
                            <button
                                type="button"
                                x-on:click="addPayment()"
                                :disabled="remaining <= 0 || enteredAmount <= 0 || (activeMethod !== 'cash' && enteredAmount > remaining + 0.001) || !! missingRequiredCustomField || missingRequiredMethodFields || uploadingField !== null"
                                class="flex w-full items-center justify-center gap-2 rounded-lg border border-gray-300 bg-white py-3 text-sm font-medium hover:bg-gray-100 disabled:opacity-40 dark:border-white/10 dark:bg-gray-800 dark:hover:bg-white/5"
                            >
                                <span class="flex items-center">
                                    <x-filament::icon x-show="methodIcon(activeMethod) === 'heroicon-m-banknotes'" icon="heroicon-m-banknotes" class="h-4 w-4" />
                                    <x-filament::icon x-show="methodIcon(activeMethod) === 'heroicon-m-credit-card'" icon="heroicon-m-credit-card" class="h-4 w-4" />
                                    <x-filament::icon x-show="methodIcon(activeMethod) === 'heroicon-m-building-library'" icon="heroicon-m-building-library" class="h-4 w-4" />
                                    <x-filament::icon x-show="methodIcon(activeMethod) === 'heroicon-m-document-text'" icon="heroicon-m-document-text" class="h-4 w-4" />
                                    <x-filament::icon x-show="methodIcon(activeMethod) === 'heroicon-m-wallet'" icon="heroicon-m-wallet" class="h-4 w-4" />
                                </span>
                                <span x-text="'Add ' + methodLabel(activeMethod) + ' — ' + money(enteredAmount > 0 ? enteredAmount : remaining)"></span>
                            </button>
                        </div>

                        <template x-if="overpaid > 0">
                            <div class="space-y-2 rounded-lg border border-green-500/30 bg-green-50 p-3 dark:bg-green-950/20">
                                <div class="flex justify-between text-sm">
                                    <span class="font-medium text-green-700">Overpayment</span>
                                    <span class="font-bold text-green-700 tabular-nums" x-text="money(overpaid)"></span>
                                </div>
                                <template x-if="selectedCustomer">
                                    <div class="grid grid-cols-2 gap-1.5">
                                        <button
                                            type="button"
                                            x-on:click="overpayAction = 'cash'"
                                            class="flex items-center justify-center gap-1.5 rounded-lg border py-2 text-xs font-medium transition-colors"
                                            :class="overpayAction === 'cash' ? 'border-green-600 bg-green-700/10 text-green-700' : 'border-gray-300 text-gray-500 hover:bg-gray-50 dark:border-white/10 dark:text-gray-400 dark:hover:bg-white/5'"
                                        >
                                            <x-filament::icon icon="heroicon-m-banknotes" class="h-3.5 w-3.5" />
                                            Return as Cash
                                        </button>
                                        <button
                                            type="button"
                                            x-on:click="overpayAction = 'credit_note'"
                                            class="flex items-center justify-center gap-1.5 rounded-lg border py-2 text-xs font-medium transition-colors"
                                            :class="overpayAction === 'credit_note' ? 'border-green-600 bg-green-700/10 text-green-700' : 'border-gray-300 text-gray-500 hover:bg-gray-50 dark:border-white/10 dark:text-gray-400 dark:hover:bg-white/5'"
                                        >
                                            <x-filament::icon icon="heroicon-m-document-check" class="h-3.5 w-3.5" />
                                            Save as Credit Note
                                        </button>
                                    </div>
                                </template>
                                <template x-if="! selectedCustomer">
                                    <p class="text-xs text-green-700">Return change to customer</p>
                                </template>
                            </div>
                        </template>

                        <p class="text-center text-xs text-gray-500 dark:text-gray-400">Leave unpaid to create a credit invoice — payment can be recorded later.</p>
                    </section>

                    {{-- Order notes ── --}}
                    <section class="space-y-2 rounded-xl border border-gray-200 p-4 dark:border-white/10">
                        <label class="text-xs font-medium tracking-wide text-gray-500 uppercase dark:text-gray-400">Order Notes (optional)</label>
                        <textarea
                            x-model="orderNotes"
                            rows="2"
                            placeholder="Add a note to this sale…"
                            class="fi-input w-full resize-none rounded-lg border-gray-300 text-sm dark:bg-gray-800"
                        ></textarea>
                    </section>
                </div>

                <div class="space-y-2 border-t border-gray-200 p-5 dark:border-white/10">
                    <div class="rounded-xl bg-green-700 p-5 text-white">
                        <div class="mb-1 text-base tracking-wide uppercase opacity-90">Amount Payable</div>
                        <div class="text-4xl font-bold tabular-nums" x-text="money(amountPayable)"></div>
                    </div>
                    <template x-if="overpaid > 0">
                        <div class="rounded-xl bg-emerald-600 p-5 text-center text-white">
                            <div class="text-base font-black tracking-wide uppercase opacity-90">Change to Return</div>
                            <div class="text-5xl font-black tabular-nums" x-text="money(overpaid)"></div>
                        </div>
                    </template>
                    <button
                        type="button"
                        x-on:click="completeSale()"
                        :disabled="! canComplete"
                        class="w-full rounded-lg bg-green-700 py-4 text-base font-semibold text-white hover:bg-green-800 disabled:cursor-not-allowed disabled:opacity-50"
                    >
                        <span x-text="submitting ? 'Processing…' : 'Complete Sale — ' + money(amountPayable)"></span>
                    </button>
                    <template x-if="requiresCustomer && ! selectedCustomer">
                        <p class="text-center text-xs text-red-600 dark:text-red-400" x-text="! walkinOnly ? 'Select a customer to leave a balance on account or apply credit.' : 'Take the full ' + money(amountPayable) + ' — this business does not sell on account.'"></p>
                    </template>
                    <template x-if="remaining > 0.005 && selectedCustomer">
                        <p class="text-center text-xs text-gray-500 dark:text-gray-400" x-text="money(remaining) + ' will be left as a balance on this invoice.'"></p>
                    </template>
                    <button type="button" x-on:click="checkoutOpen = false" class="w-full rounded-lg border border-gray-300 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 dark:border-white/10 dark:text-gray-200 dark:hover:bg-white/5">
                        Back to Cart
                    </button>
                </div>
            </div>
        </div>

        {{-- ── Sale complete: receipt rendered right in the modal ─────────────
             Used to embed InvoicePrintController's `?preview=1` document in an
             iframe. That meant a second full HTTP round trip through a
             standalone page (its own toolbar, its own paper switch, its own
             auto-print timer suppressed by the `preview` flag) just to show a
             receipt the cashier already had the data for — and Alpine bindings
             on the iframe itself were fragile against the x-if/x-show timing
             around lastReceipt flipping to null (see git history).

             Instead, receiptDocument() renders the exact same
             invoices.print.{a4,thermal80} partial InvoicePrintController does
             and hands back the HTML, which lands here via x-html — this copy
             is the on-screen preview only. Printing re-renders that same HTML
             as its own document (printDocument() below) rather than trying to
             carve the sheet out of this page with print-only CSS.
             The modal is plain x-show markup (no x-if clone), so nothing here
             reads deeper than `lastReceipt?.` and there's nothing for a
             mid-transition Livewire DOM morph to catch half-built.

             The sheet itself is the exact fixed physical width
             (210mm / 72mm) invoices.print.{a4,thermal80} always render at —
             the same markup the Master Control live preview
             (invoice-preview.blade.php) shows and the same markup that
             prints. Rather than let a 210mm-wide A4 sheet get clipped by
             the modal's own width, or shrink the modal to something absurd,
             this scales it to fit exactly the way that Master Control
             preview does: `transform: scale()` on the doc wrapper (a purely
             visual scale, so nothing re-wraps between preview and print),
             sized by fitReceipt() below. Print bypasses the scale entirely
             (see the print-only CSS after this modal). --}}
        <div
            x-show="saleCompleteOpen"
            x-cloak
            x-init="
                $watch('receiptHtml', () => { $nextTick(() => fitReceipt()); setTimeout(() => fitReceipt(), 400); });
                window.addEventListener('resize', () => fitReceipt());
            "
            class="fixed inset-0 z-[70] flex items-center justify-center bg-gray-900/60 p-4 backdrop-blur-sm"
        >
            <div x-on:click.outside="closeSaleComplete()" class="flex h-full max-h-[90vh] w-full max-w-2xl flex-col overflow-hidden rounded-xl bg-white shadow-xl dark:bg-gray-900">
                <div class="flex items-center justify-between gap-3 border-b border-gray-200 px-5 py-4 dark:border-white/10">
                    <div class="flex min-w-0 items-center gap-3">
                        <span class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-green-100 text-green-600 dark:bg-green-500/10 dark:text-green-400">
                            <x-filament::icon icon="heroicon-o-check-circle" class="h-6 w-6" />
                        </span>
                        <div class="min-w-0">
                            <h3 class="text-base font-semibold leading-tight">Sale Complete</h3>
                            <span class="truncate font-mono text-sm text-gray-500 dark:text-gray-400" x-text="lastReceipt?.invoice_number"></span>
                        </div>
                    </div>
                    <div class="flex shrink-0 items-center gap-2">
                        <template x-if="lastReceipt?.can_print">
                            <div class="flex items-center gap-0.5 rounded-lg bg-gray-100 p-0.5 dark:bg-white/5">
                                <template x-for="[key, label] in Object.entries({ thermal80: '80 mm', a4: 'A4' })" :key="key">
                                    <button
                                        type="button"
                                        x-on:click="setReceiptPaper(key)"
                                        x-bind:class="receiptPaper === key
                                            ? 'bg-white text-gray-900 shadow-sm dark:bg-white/10 dark:text-white'
                                            : 'text-gray-500 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white'"
                                        class="rounded-md px-2.5 py-1 text-xs font-medium transition"
                                        x-text="label"
                                    ></button>
                                </template>
                            </div>
                        </template>
                        <button
                            type="button"
                            x-on:click="closeSaleComplete()"
                            class="flex h-9 w-9 shrink-0 items-center justify-center rounded-full text-gray-400 hover:bg-gray-100 hover:text-gray-600 dark:hover:bg-white/10 dark:hover:text-gray-200"
                            aria-label="Close"
                        >
                            <x-filament::icon icon="heroicon-o-x-mark" class="h-5 w-5" />
                        </button>
                    </div>
                </div>

                <div class="min-h-0 flex-1 overflow-auto bg-gray-100 dark:bg-gray-950">
                    <template x-if="lastReceipt?.can_print">
                        <div>
                            <p x-show="receiptLoading" class="px-5 py-6 text-center text-sm text-gray-500 dark:text-gray-400">
                                Loading receipt…
                            </p>
                            <div
                                x-show="! receiptLoading"
                                x-ref="receiptStage"
                                class="pos-receipt-stage"
                                x-bind:style="'height: ' + receiptStageHeight + 'px'"
                            >
                                <div
                                    x-ref="receiptDoc"
                                    class="pos-receipt-doc"
                                    x-bind:style="'transform: scale(' + receiptScale + '); margin-left: ' + receiptOffset + 'px'"
                                >
                                    <div id="pos-receipt-print" class="bg-white shadow" x-html="receiptHtml"></div>
                                </div>
                            </div>
                        </div>
                    </template>
                    <p x-show="lastReceipt && ! lastReceipt.can_print" class="px-5 py-6 text-center text-sm text-gray-500 dark:text-gray-400">
                        Sale <span class="font-mono" x-text="lastReceipt?.invoice_number"></span> completed, but you don't have permission to view or print invoices.
                    </p>
                </div>

                <div class="flex items-center justify-end gap-2 border-t border-gray-200 px-5 py-4 dark:border-white/10">
                    <x-filament::button color="gray" x-on:click="closeSaleComplete()">
                        Close
                    </x-filament::button>
                    <x-filament::button x-show="lastReceipt?.can_print" x-on:click="printReceipt()">
                        Print
                    </x-filament::button>
                </div>
            </div>
        </div>

        {{-- Lot picker — the consignment (GRN receipt) the units come from.
             Every sale is priced from the selected lot, so this modal drives
             the whole terminal: one sellable lot is taken silently on add,
             more than one always prompts here. The FEFO-front lot is the
             default; picking anything else needs inventory_batches.override,
             which the server enforces on checkout regardless. --}}
        <div x-show="lotPickerOpen" x-cloak class="fixed inset-0 z-[60] flex items-center justify-center bg-gray-900/60 p-4 backdrop-blur-sm" x-on:keydown.escape="closeLotPicker()">
            <div
                x-on:click.outside="closeLotPicker()"
                class="flex max-h-[85vh] w-full max-w-lg flex-col overflow-hidden rounded-xl bg-white shadow-xl dark:bg-gray-900"
            >
                <div class="flex items-center justify-between gap-3 border-b border-gray-200 px-5 py-4 dark:border-white/10">
                    <div class="min-w-0">
                        <h3 class="text-base font-semibold leading-tight">Select lot</h3>
                        <p class="truncate text-sm text-gray-500 dark:text-gray-400" x-text="lotPickerProduct?.name"></p>
                    </div>
                    <button type="button" x-on:click="closeLotPicker()" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200">
                        <x-filament::icon icon="heroicon-o-x-mark" class="h-5 w-5" />
                    </button>
                </div>

                <div class="min-h-0 flex-1 overflow-auto p-4">
                    <p x-show="lotPickerLoading" class="py-8 text-center text-sm text-gray-500 dark:text-gray-400">Loading lots…</p>
                    <p x-show="! lotPickerLoading && ! lotPickerError && lotPickerGroups.length === 0" class="py-8 text-center text-sm text-amber-600">
                        No sellable lot for this product at this branch.
                    </p>

                    <template x-for="group in lotPickerGroups" :key="group.grn_number ?? '__opening__'">
                        <div class="mb-4">
                            <div class="mb-1.5 flex flex-wrap items-center gap-x-2 gap-y-0.5 text-[11px] text-gray-500 dark:text-gray-400">
                                <template x-if="group.grn_number">
                                    <span class="font-mono font-semibold text-gray-700 dark:text-gray-200" x-text="group.grn_number"></span>
                                </template>
                                <template x-if="! group.grn_number">
                                    <span class="font-semibold text-gray-700 dark:text-gray-200">Opening balance</span>
                                </template>
                                <template x-if="group.supplier"> · <span x-text="group.supplier"></span></template>
                                <template x-if="group.confirmed_at"> · <span x-text="timeAgo(group.confirmed_at)"></span></template>
                                <template x-if="group.reference"> · <span x-text="group.reference"></span></template>
                            </div>

                            <div class="space-y-1.5">
                                <template x-for="lot in group.lots" :key="lot.id">
                                    <button
                                        type="button"
                                        x-on:click="pickLot(group, lot)"
                                        :disabled="! canOverrideBatch && ! lot.is_fefo_front"
                                        :title="(! canOverrideBatch && ! lot.is_fefo_front) ? 'You don\u0027t have permission to pick a lot other than the next one to sell (FEFO).' : null"
                                        class="w-full rounded-lg border border-gray-200 bg-white p-3 text-start transition hover:border-primary-400 hover:bg-primary-50/50 disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:border-gray-200 disabled:hover:bg-white dark:border-white/10 dark:bg-gray-800 dark:hover:border-primary-500/40 dark:hover:bg-white/5 dark:disabled:hover:bg-gray-800"
                                    >
                                        <div class="flex items-center justify-between gap-2">
                                            <span class="font-mono text-sm font-semibold text-gray-800 dark:text-gray-200" x-text="lot.internal_batch_number"></span>
                                            <span class="flex shrink-0 items-center gap-2">
                                                <span x-show="lot.is_fefo_front" class="rounded bg-emerald-100 px-1.5 py-0.5 text-[10px] font-semibold text-emerald-700 dark:bg-emerald-500/10 dark:text-emerald-400">Next to sell</span>
                                                <span class="text-sm font-bold text-primary-600 tabular-nums dark:text-primary-400" x-text="money(lot.sale_price)"></span>
                                            </span>
                                        </div>
                                        <div class="mt-1 flex flex-wrap items-center gap-x-2 gap-y-0.5 text-[11px] text-gray-500 dark:text-gray-400">
                                            <span x-text="formatQty(lot.available_quantity) + ' available'"></span>
                                            <template x-if="lot.manufacture_date"> · mfg <span x-text="lot.manufacture_date"></span></template>
                                            <template x-if="lot.expiry_date">
                                                · exp <span class="font-semibold" :class="Number(lot.days_to_expiry) <= 30 ? 'text-amber-600 dark:text-amber-400' : ''" x-text="lot.expiry_date + (Number(lot.days_to_expiry) <= 0 ? ' (expired)' : '')"></span>
                                            </template>
                                        </div>
                                    </button>
                                </template>
                            </div>
                        </div>
                    </template>
                </div>

                <div class="flex items-center justify-between gap-2 border-t border-gray-200 px-5 py-3 dark:border-white/10">
                    <p x-show="lotPickerError" class="min-w-0 flex-1 truncate text-xs text-red-600" x-text="lotPickerError"></p>
                    <div class="flex shrink-0 justify-end gap-2">
                        <x-filament::button color="gray" x-on:click="closeLotPicker()">
                            Cancel
                        </x-filament::button>
                    </div>
                </div>
            </div>
        </div>
    </div>

    <style>
        /* fitReceipt() sizes these — same technique as the Master Control live
           preview (resources/views/filament/central/tenant/invoice-preview.blade.php):
           the stage's height is set explicitly because a CSS transform shrinks
           what the doc *looks like* without shrinking the box it lays out in,
           so without this the stage would leave a tall gap below the visibly
           smaller sheet. */
        .pos-receipt-stage { position: relative; overflow: hidden; }
        .pos-receipt-doc { transform-origin: top left; width: max-content; }

        {{-- No @media print rules here on purpose. Printing used to happen out
             of this document: everything was hidden with `visibility: hidden`
             and the receipt was lifted out of the modal with
             `position: fixed; inset: 0`. Both halves of that were wrong.
             A hidden element still occupies its full layout box, so the POS
             page behind the modal kept generating its own pages — and a
             position:fixed element is painted onto EVERY page of a paged
             medium, so the receipt was stamped once per page. One click on
             Print produced a stack of identical copies (four, at the page
             heights this screen runs to).

             printDocument() below prints an isolated document instead, so the
             sheet is the entire print job: exactly one copy, no trailing
             blanks, and no dependence on what the POS screen happens to lay
             out behind it. --}}
    </style>

    @script
    <script>
        // ── Searchable combobox (category / subcategory / customer) ────────
        Alpine.data('alpineCombobox', (config) => ({
            open: false,
            query: '',
            highlighted: -1,

            get placeholder() { return config.placeholder || 'Select…' },
            get searchPlaceholder() { return config.searchPlaceholder || 'Search…' },

            get selected() {
                return (config.options || []).find((o) => String(o.id) === String(config.value)) || null;
            },

            get filtered() {
                const q = this.query.trim().toLowerCase();
                if (! q) return config.options || [];
                return (config.options || []).filter((o) => String(o.name).toLowerCase().includes(q));
            },

            toggle() {
                this.open = ! this.open;
                if (this.open) {
                    this.query = '';
                    this.highlighted = -1;
                    this.$nextTick(() => this.$refs.search?.focus());
                }
            },

            select(option) {
                if (! option) return;
                config.onchange?.(option.id);
                this.open = false;
            },
        }));

        // ── Fixed/percentage discount popover (cart lines + invoice level) ─
        Alpine.data('discountPopover', (config) => ({
            open: false,
            draftType: 'fixed',
            draftValue: '',

            get hasDiscount() { return !! config.type && Number(config.value) > 0 },

            get type() { return config.type },
            get value() { return config.value },

            init() {
                this.$watch('open', (opened) => {
                    if (opened) {
                        this.draftType = config.type || 'fixed';
                        this.draftValue = Number(config.value) > 0 ? String(config.value) : '';
                    }
                });
            },

            toggle() {
                this.open = ! this.open;
            },

            clear() {
                config.onclear?.();
                this.open = false;
            },

            apply() {
                const value = parseFloat(this.draftValue) || 0;
                if (value <= 0) {
                    config.onclear?.();
                } else {
                    config.onapply?.(this.draftType, value);
                }
                this.open = false;
            },
        }));

        // ── POS terminal ───────────────────────────────────────────────────
        Alpine.data('posTerminal', (config) => ({
            ...config,

            // Client-only cart state
            cart: [],
            search: '',
            mainCat: null,
            subCat: null,

            setMainCat(id) {
                this.mainCat = (id === '__all__' || id === null) ? null : String(id);
                // A subcategory of the previous main category no longer
                // applies once the main category itself changes.
                this.subCat = null;
            },

            setSubCat(id) {
                this.subCat = (id === '__all__' || id === null) ? null : String(id);
            },

            // Display state — deliberately NOT the browser Fullscreen API.
            // Expanding is a persisted CSS overlay (see toggleExpanded()) so a
            // print dialog, focus change, navigation or reload can never
            // collapse the terminal; it restores from localStorage on init.
            expanded: false,
            // Cart panel layout: true (default) = 50/50 table view, false =
            // narrow card list (see toggleCartExpanded()). Independent of
            // `expanded` (fullscreen) so either can be toggled on its own.
            cartExpanded: true,
            customerId: null,
            customerContextData: {},
            discountType: 'fixed',
            discountValue: 0,

            // Checkout state
            checkoutOpen: false,
            saleCompleteOpen: false,
            lastReceipt: null,
            receiptPaper: 'thermal80',
            receiptHtml: '',
            receiptLoading: false,
            receiptScale: 1,
            receiptOffset: 0,
            receiptStageHeight: 0,
            payments: [],
            activeMethod: 'cash',
            pmAmount: '',
            pmReference: '',
            pmChequeNumber: '',
            pmChequeBank: '',
            pmChequeBranch: '',
            pmChequeDate: '',
            customFieldValues: {},
            uploadingField: null,
            selectedCreditNotes: [],
            voucherCode: '',
            appliedVoucher: null,
            voucherError: '',
            voucherChecking: false,
            redeemPointsInput: '',
            overpayAction: 'cash',
            orderNotes: '',

            // Held sales
            heldOpen: false,
            confirmResumeId: null,
            confirmDiscardId: null,

            // Prevent duplicate Hold requests from double-clicks.
            holding: false,

            // Lot picker (modal) state
            lotPickerOpen: false,
            lotPickerLoading: false,
            lotPickerProduct: null,
            lotPickerGroups: [],
            lotPickerTargetLine: null,
            lotPickerError: '',
            // Monotonic token identifying the newest lot fetch. A click that
            // supersedes an in-flight one wins, and the abandoned response is
            // discarded instead of overwriting the modal — a cashier's clicks
            // are never dropped just because a request was still open.
            lotPickerRequest: 0,

            // Till / session dialogs
            tillId: null,
            openingBalance: '',
            overrideOpeningBalance: false,
            openingNote: '',
            closeTillOpen: false,
            reconciliation: null,
            countedCash: null,
            closingNote: '',
            withdrawOpen: false,
            cmType: 'out',
            cmAmount: 0,
            cmReasonId: null,
            cmNote: '',
            cmReceiptOpen: false,
            cmReceiptHtml: '',
            cmReceiptLoading: false,
            cmMovement: null,
            amendOpen: false,
            amendLastSale: null,
            amendCanAmend: false,
            amendLoading: false,
            amendError: null,

            // Tracking lookup caches (keyed by product id)
            _serials: {},

            submitting: false,
            errors: [],

            // Icons stay a client-side lookup keyed by code — the server only
            // owns which methods are active and their requirement flags.
            methodIconFor(code) {
                return {
                    cash: 'heroicon-m-banknotes',
                    card: 'heroicon-m-credit-card',
                    bank_transfer: 'heroicon-m-building-library',
                    cheque: 'heroicon-m-document-text',
                }[code] ?? 'heroicon-m-wallet';
            },

            get paymentMethods() {
                return (this.serverPaymentMethods || []).map((m) => ({
                    code: m.code,
                    label: m.name,
                    icon: this.methodIconFor(m.code),
                    isCustom: !! m.is_custom,
                    requiresReference: !! m.requires_reference,
                    requiresChequeFields: !! m.requires_cheque_fields,
                    fields: m.fields,
                }));
            },

            get activeMethodConfig() {
                return this.paymentMethods.find((m) => m.code === this.activeMethod) ?? null;
            },

            get activeCustomMethod() {
                return this.activeMethodConfig?.isCustom ? this.activeMethodConfig : null;
            },

            get missingRequiredMethodFields() {
                const cfg = this.activeMethodConfig;
                if (! cfg) return false;

                if (cfg.requiresReference && ! String(this.pmReference || '').trim()) return true;

                if (cfg.requiresChequeFields && this.activeMethod === 'cheque') {
                    if (! String(this.pmChequeNumber || '').trim()) return true;
                    if (! String(this.pmChequeBank || '').trim()) return true;
                    if (! String(this.pmChequeDate || '').trim()) return true;
                }

                return false;
            },

            methodLabel(code) {
                return this.paymentMethods.find((m) => m.code === code)?.label ?? code;
            },

            methodIcon(code) {
                return this.paymentMethods.find((m) => m.code === code)?.icon ?? 'heroicon-m-wallet';
            },

            init() {
                this.tillId = this.selectedTillId;
                // Default the opening balance to the till's carried-forward
                // closing balance; the cashier must explicitly opt in to
                // override it (see overrideOpeningBalance).
                this.openingBalance = this.previousClosingBalance !== null
                    ? Number(this.previousClosingBalance)
                    : '';
                // Master-control switches off mid-session must not leave a stale
                // applied voucher or points draft in the checkout payload.
                if (! this.vouchersEnabled) {
                    this.appliedVoucher = null;
                    this.voucherCode = '';
                    this.voucherError = '';
                }
                if (! this.loyaltyEnabled) {
                    this.redeemPointsInput = '';
                }
                // Restore the persisted expanded/minimized state on every load —
                // branch switches, till open/close and session start/end all
                // reload the page, and none of them may collapse the terminal.
                this.expanded = this.readExpanded();
                this.cartExpanded = this.readCartExpanded();
                window.addEventListener('keydown', this.onKeydown);

                // Page-wide scanner capture (see scan-capture-script.blade.php,
                // rendered into every tenant page): a scan reaches handleScan()
                // no matter what has focus, not only while the search box does.
                // Declined while a modal owns the screen — a scan mid-checkout
                // or mid-lot-pick must not silently start a second action
                // behind it.
                window.VellixScan?.register(this.$root, (code) => {
                    if (
                        this.lotPickerOpen || this.checkoutOpen || this.heldOpen
                        || this.closeTillOpen || this.withdrawOpen || this.cmReceiptOpen
                        || this.amendOpen || this.saleCompleteOpen
                    ) {
                        return true;
                    }

                    this.handleScan(code);

                    return true;
                });

                window.addEventListener('pos-customer-created', (event) => {
                    const customer = event.detail?.customer;

                    if (! customer) {
                        return;
                    }

                    this.customers = [
                        ...(this.customers || []).filter((c) => String(c.id) !== String(customer.id)),
                        customer,
                    ];

                    this.selectCustomer(customer.id);
                });
            },

            // ── Expand: an app-level CSS overlay (fixed inset-0 z-50), NOT the
            // browser Fullscreen API. The native API exits fullscreen on print
            // dialogs, focus changes, navigation and reloads — collapsing the
            // terminal mid-shift — so the expanded/minimized flag is persisted
            // in localStorage, survives every reload and print workflow, and
            // only ever changes when the cashier clicks the expand/minimize
            // control. Escape and other keys never touch it.
            readExpanded() {
                try {
                    return localStorage.getItem('pos.expanded') === '1';
                } catch (e) {
                    return false;
                }
            },

            persistExpanded() {
                try {
                    if (this.expanded) {
                        localStorage.setItem('pos.expanded', '1');
                    } else {
                        localStorage.removeItem('pos.expanded');
                    }
                } catch (e) {}
            },

            toggleExpanded() {
                this.expanded = ! this.expanded;
                this.persistExpanded();
            },

            // ── Cart table view: switches the cart column from the narrow
            // card list to a 50/50 table layout (item / qty / price /
            // discount / net amount columns), matching the concept of the
            // tablet-style POS reference. Table view is the default; a
            // cashier who switches to the compact card list has that choice
            // persisted the same way `expanded` is — a page reload (branch
            // switch, till open/close, session start/end) must not silently
            // reset it. Absence of a stored value (first run, or a browser
            // predating this feature) means "no choice made yet", which
            // resolves to the default (table view), not to off.
            readCartExpanded() {
                try {
                    const stored = localStorage.getItem('pos.cartExpanded');
                    return stored === null ? true : stored === '1';
                } catch (e) {
                    return true;
                }
            },

            persistCartExpanded() {
                try {
                    localStorage.setItem('pos.cartExpanded', this.cartExpanded ? '1' : '0');
                } catch (e) {}
            },

            toggleCartExpanded() {
                this.cartExpanded = ! this.cartExpanded;
                this.persistCartExpanded();
            },

            // ── Keyboard shortcuts: F2 search, F4 hold, F5 held, F8 checkout, Esc close ─
            onKeydown(e) {
                if (e.key === 'F2') {
                    e.preventDefault();
                    this.$refs.searchInput?.focus();
                } else if (e.key === 'F4') {
                    e.preventDefault();
                    this.hold();
                } else if (e.key === 'F5') {
                    e.preventDefault();
                    this.heldOpen = true;
                } else if (e.key === 'F8') {
                    e.preventDefault();
                    this.openCheckout();
                } else if (e.key === 'Escape') {
                    if (this.saleCompleteOpen) {
                        this.closeSaleComplete();
                        return;
                    }
                    this.checkoutOpen = false;
                    this.heldOpen = false;
                    this.closeTillOpen = false;
                    this.withdrawOpen = false;
                    this.amendOpen = false;
                    this.confirmResumeId = null;
                    this.confirmDiscardId = null;
                }
            },

            // ── Derived values (pure client-side, no round-trip) ────────────
            get mainCatOptions() {
                const cats = [...new Set((this.products || []).map((p) => p.main_category).filter(Boolean))].sort();
                return [{ id: '__all__', name: 'All' }, ...cats.map((c) => ({ id: c, name: c }))];
            },

            get subCatOptions() {
                const cats = [...new Set(
                    (this.products || [])
                        .filter((p) => ! this.mainCat || p.main_category === this.mainCat)
                        .map((p) => p.sub_category)
                        .filter(Boolean),
                )].sort();
                return [{ id: '__all__', name: 'All' }, ...cats.map((c) => ({ id: c, name: c }))];
            },

            get filteredProducts() {
                const q = this.search.trim().toLowerCase();
                return (this.products || []).filter((p) => {
                    const matchesMain = ! this.mainCat || p.main_category === this.mainCat;
                    const matchesSub = ! this.subCat || p.sub_category === this.subCat;
                    const matchesSearch = ! q
                        || (p.name || '').toLowerCase().includes(q)
                        || (p.sku || '').toLowerCase().includes(q)
                        || (p.barcode || '').toLowerCase().includes(q)
                        || (p.sub_category || '').toLowerCase().includes(q);
                    return matchesMain && matchesSub && matchesSearch;
                });
            },

            /**
             * Packaging/add-ons bucketed under their subcategory, in the order
             * the server sent them (already sorted subcategory-then-name).
             * Products filed straight under the category with no subcategory
             * fall into one trailing "Add-ons" bucket rather than vanishing.
             */
            get packagingGroups() {
                const groups = [];
                for (const product of this.packagingProducts || []) {
                    const name = product.sub_category || 'Add-ons';
                    let group = groups.find((g) => g.name === name);
                    if (! group) {
                        group = { name, products: [] };
                        groups.push(group);
                    }
                    group.products.push(product);
                }
                return groups;
            },

            get selectedCustomer() {
                if (! this.customerId) return null;
                return (this.customers || []).find((c) => String(c.id) === String(this.customerId)) ?? null;
            },

cartLineFor(product) {
const lines = this.cart.filter((line) => line.product_id === product.id);
if (! lines.length) return null;
// A product may sit in the cart twice — once per lot — so the
// card badge shows the combined quantity across lots.
return { quantity: lines.reduce((sum, line) => sum + Number(line.quantity), 0) };
},

// Shared product-card contract (filament.pos.product-card): the POS guard
// lives here so the partial itself stays screen-agnostic.
tapProduct(product) {
if (product.quantity > 0) this.addProduct(product);
},

cartQtyFor(product) {
return this.cartLineFor(product)?.quantity ?? 0;
},

            get cartCount() {
                return this.cart.reduce((sum, line) => sum + (Number(line.quantity) || 0), 0);
            },

            isSerialTracked(line) {
                return line.has_serials === true;
            },

            requiresSerial(line) {
                return line.has_serials === true;
            },

            stepFor(line) {
                if (this.isSerialTracked(line)) return 1;
                return line.base_unit?.is_fractional ? 0.1 : 1;
            },

            lineGross(line) {
                return (Number(line.quantity) || 0) * (Number(line.price) || 0);
            },

            lineDiscount(line) {
                const gross = this.lineGross(line);
                const value = Number(line.discount_value) || 0;
                if (! line.discount_type || value <= 0 || gross <= 0) return 0;
                const amount = line.discount_type === 'percentage' ? gross * (value / 100) : value * (Number(line.quantity) || 0);
                return Math.min(Math.max(amount, 0), gross);
            },

            // The automatic deal discount on the line, recomputed client-side
            // from the deal the server attached to the product (same formula
            // as DealService::computeDiscount, fixed = per-unit × quantity).
            // Display only — InvoiceService::create() applies the deal
            // authoritatively at checkout, so the badge and totals can never
            // drift from what actually gets charged.
            lineDealDiscount(line) {
                const gross = this.lineGross(line);
                const deal = line.deal;
                if (! deal || gross <= 0) return 0;
                const value = Number(deal.discount_value) || 0;
                if (value <= 0) return 0;
                const amount = deal.discount_type === 'percentage'
                    ? gross * (value / 100)
                    : value * (Number(line.quantity) || 0);
                const maxDeal = Math.max(0, gross - this.lineDiscount(line));
                return Math.round(Math.min(Math.max(amount, 0), maxDeal) * 100) / 100;
            },

            lineNet(line) {
                return this.lineGross(line) - this.lineDiscount(line) - this.lineDealDiscount(line);
            },

            lineDiscountAmount(gross, type, value) {
                if (! type || value <= 0 || gross <= 0) return 0;
                const amount = type === 'percentage' ? gross * (value / 100) : value;
                return Math.min(Math.max(amount, 0), gross);
            },

            get grossSubtotal() {
                return this.cart.reduce((sum, line) => sum + this.lineGross(line), 0);
            },

            get lineDiscountTotal() {
                return this.cart.reduce((sum, line) => sum + this.lineDiscount(line), 0);
            },

            get dealDiscountTotal() {
                return this.cart.reduce((sum, line) => sum + this.lineDealDiscount(line), 0);
            },

            // The rule the current cart clears (highest threshold wins, ties →
            // lowest id — same resolution as CartDiscountService::bestRule()).
            get cartRule() {
                const eligible = this.netSubtotal + this.vatTotal;
                if (eligible <= 0) return null;
                const rules = [...(this.cartRules || [])].sort((a, b) => (Number(b.min_cart_value) || 0) - (Number(a.min_cart_value) || 0));
                return rules.find((r) => eligible >= (Number(r.min_cart_value) || 0)) ?? null;
            },

            // The automatic cart-value rule discount ("Spend & Save"),
            // recomputed client-side from the rules the server attached to
            // the page (same lookup, same formula — percentage of the cart
            // or a flat amount, clamped to the cart and the rule's max).
            // Display only — InvoiceService::create() evaluates the rule
            // against the same eligible base (net subtotal + VAT)
            // authoritatively at checkout, so what the cashier sees is
            // exactly what gets charged.
            get cartRuleDiscount() {
                const rule = this.cartRule;
                if (! rule) return 0;
                const eligible = this.netSubtotal + this.vatTotal;
                const value = Number(rule.discount_value) || 0;
                if (value <= 0) return 0;
                const amount = rule.discount_type === 'percentage' ? eligible * (value / 100) : value;
                let discount = Math.min(Math.max(amount, 0), eligible);
                const max = Number(rule.max_discount_amount);
                if (max > 0) discount = Math.min(discount, max);
                return Math.round(discount * 100) / 100;
            },

            // VAT on GROSS per line, matching InvoiceService/Pos::completeCheckout
            get vatTotal() {
                if (! this.vatEnabled) return 0;
                return this.cart.reduce((sum, line) => sum + this.lineGross(line) * ((Number(line.vat_rate) || 0) / 100), 0);
            },

            get netSubtotal() {
                return this.grossSubtotal - this.lineDiscountTotal - this.dealDiscountTotal;
            },

            get invoiceDiscountAmount() {
                return this.lineDiscountAmount(this.netSubtotal, this.discountType, this.discountValue);
            },

            get netTotal() {
                return Math.max(0, this.grossSubtotal - this.lineDiscountTotal - this.dealDiscountTotal - this.invoiceDiscountAmount - this.cartRuleDiscount + this.vatTotal);
            },

            get voucherDiscount() {
                return Number(this.appliedVoucher?.discount) || 0;
            },

            get creditNotes() {
                return this.customerContextData.credit_notes ?? [];
            },

            get creditBalance() {
                return Number(this.customerContextData.credit_balance) || 0;
            },

            get loyaltyProgram() {
                return this.customerContextData.loyalty_program ?? null;
            },

            get loyaltyBalance() {
                return Number(this.customerContextData.loyalty_balance) || 0;
            },

            get redeemPointsRequested() {
                return Math.min(Number(this.redeemPointsInput) || 0, this.loyaltyBalance);
            },

            get meetsLoyaltyMinimum() {
                return this.redeemPointsRequested === 0
                    || this.redeemPointsRequested >= (Number(this.loyaltyProgram?.minimum_points_to_redeem) || 0);
            },

            get redeemPoints() {
                return this.meetsLoyaltyMinimum ? this.redeemPointsRequested : 0;
            },

            get loyaltyDiscount() {
                if (! this.loyaltyProgram || this.redeemPoints <= 0) return 0;
                return this.redeemPoints / (Number(this.loyaltyProgram.redemption_rate) || 1);
            },

            get selectedCreditTotal() {
                return this.creditNotes
                    .filter((cn) => this.selectedCreditNotes.includes(cn.id))
                    .reduce((sum, cn) => sum + Number(cn.amount), 0);
            },

            get creditNotesAmount() {
                return Math.min(this.selectedCreditTotal, Math.max(0, this.netTotal - this.voucherDiscount - this.loyaltyDiscount), this.creditBalance);
            },

            get creditRemainder() {
                return Math.max(0, this.selectedCreditTotal - this.creditNotesAmount);
            },

            get amountPayable() {
                return Math.max(0, this.netTotal - this.voucherDiscount - this.loyaltyDiscount - this.creditNotesAmount);
            },

            get tendered() {
                return this.payments.reduce((sum, p) => sum + (Number(p.amount) || 0), 0);
            },

            get remaining() {
                return Math.max(0, this.amountPayable - this.tendered);
            },

            get overpaid() {
                return Math.max(0, this.tendered - this.amountPayable);
            },

            get enteredAmount() {
                return this.pmAmount ? parseFloat(this.pmAmount) : this.remaining;
            },

            get cashChange() {
                return this.activeMethod === 'cash' && this.pmAmount
                    ? Math.max(0, parseFloat(this.pmAmount) - this.remaining)
                    : 0;
            },

            get missingRequiredCustomField() {
                return this.activeCustomMethod?.fields?.some(
                    (f) => f.required && ! String(this.customFieldValues[f.key] ?? '').trim(),
                ) ?? false;
            },

            get requiresCustomer() {
                return this.remaining > 0.005 || this.creditNotesAmount > 0;
            },

            get canComplete() {
                return ! this.submitting
                    && this.cart.length > 0
                    && (! this.requiresCustomer || !! this.selectedCustomer)
                    && this.meetsLoyaltyMinimum;
            },

            get cartHasIncompleteTracking() {
                return this.cart.some((line) => this.requiresSerial(line) && line.serial_ids.length !== line.quantity);
            },

            money(value) {
                return 'Rs. ' + (Number(value) || 0).toFixed(2);
            },

            formatQty(value) {
                const n = Number(value) || 0;
                return Number.isInteger(n) ? String(n) : n.toFixed(3);
            },

            formatQtyWithUnit(value, unit) {
                const qty = this.formatQty(value);
                const label = unit?.code || unit?.name;
                return label ? qty + ' ' + label : qty;
            },

            timeAgo(iso) {
                const minutes = Math.max(0, Math.round((Date.now() - new Date(iso).getTime()) / 60000));
                if (minutes < 1) return 'just now';
                if (minutes < 60) return minutes + 'm ago';
                return Math.round(minutes / 60) + 'h ago';
            },

            // ── Cart mutations (client-only) ────────────────────────────────
            // The lot IS the price, so adding a product resolves its sellable
            // lots EVERY time: none → nothing to sell, one → taken silently
            // (growing that lot's line, or creating it), several → the modal
            // asks which consignment to sell from. A product already on the
            // cart is not a guard — the same product may sit on the cart once
            // per lot, at each lot's own price.
            //
            // Resolving them does NOT mean asking the server every time. The
            // page payload already carries each product's sellable-lot count
            // and its front lot (`lot_count` from BatchService::priceRangesFor,
            // `front_lot` from frontLotsPayload), built from the same filters
            // and the same sell order productLots() uses — so for the two
            // outcomes that involve no decision, the answer is already on the
            // client and a round-trip would only re-derive it. Only a genuine
            // choice between lots fetches, which is also the only case where
            // the cashier is about to sit and read the list anyway.
            //
            // That payload is a page-load snapshot, kept current for this
            // terminal's own sales by deductSoldLot(). What it cannot see is
            // another till's sale or a GRN received mid-shift, so on the
            // no-decision paths the server is the backstop rather than the
            // source: InvoiceService reprices from the lot and re-checks the
            // front-lot rule at checkout, so a stale lot is refused, never
            // mis-sold. The barcode path in scanAdd() has always worked this
            // way; this puts the click on the same footing.
            async addProduct(product) {
                if (! product || Number(product.quantity) <= 0) return;

                // Non-batch products: single unified pool — bypass lot picker entirely,
                // pull directly from branch stock at the current active price.
                if (product.is_batch_tracked === false) {
                    if (product.front_lot) {
                        this.addLineWithLot(product, product.front_lot);
                        return;
                    }
                    if (product.price_min !== null && Number(product.price_min) > 0) {
                        // Synthetic unified lot when front_lot not yet resolved but price exists.
                        const synthetic = { id: null, sale_price: product.price_min, available_quantity: product.quantity, internal_batch_number: 'UNIFIED', expiry_date: null, manufacture_date: null };
                        this.addLineWithLot(product, synthetic);
                        return;
                    }
                    this.lotPickerRequest++;
                    this.lotPickerLoading = false;
                    this.lotPickerError = 'No price on file — price this product on a GRN.';
                    this.showLotPicker(product, null, []);
                    this.notifyNoPriceOnFile(product);
                    return;
                }

                const lotCount = Number(product.lot_count) || 0;

                if (lotCount === 0) {
                    // No sellable lot on file — most likely no priced lot: the
                    // lot, not the product, is the price source of truth. Say
                    // so instead of silently doing nothing. Bumping the token
                    // stops an in-flight fetch from filling this modal in.
                    this.lotPickerRequest++;
                    this.lotPickerLoading = false;
                    this.lotPickerError = 'No price on file — price this product on a GRN.';
                    this.showLotPicker(product, null, []);
                    this.notifyNoPriceOnFile(product);
                    return;
                }

                if (lotCount === 1 && product.front_lot) {
                    // Exactly one sellable lot ⇒ the front lot IS that lot,
                    // and it carries every field addLineWithLot() reads.
                    this.addLineWithLot(product, product.front_lot);
                    return;
                }

                // Several lots: open the modal on the click, then fill it in.
                const token = ++this.lotPickerRequest;
                this.lotPickerLoading = true;
                this.lotPickerError = '';
                this.showLotPicker(product, null, []);
                try {
                    const groups = await $wire.productLots(product.id, this.selectedBranchId);
                    // Superseded by a newer click, or the cashier closed the
                    // modal — drop the response rather than reopening on them.
                    if (token !== this.lotPickerRequest) return;
                    // On this path the fetch, not the snapshot, decides: the
                    // count that sent us here may have shrunk since page load,
                    // so handle the none/one outcomes again.
                    const lots = (groups ?? []).flatMap((g) => g.lots ?? []);
                    if (! lots.length) {
                        this.lotPickerGroups = [];
                        this.lotPickerError = 'No price on file — price this product on a GRN.';
                        this.notifyNoPriceOnFile(product);
                        return;
                    }
                    if (lots.length === 1) {
                        this.closeLotPicker();
                        this.addLineWithLot(product, lots[0]);
                        return;
                    }
                    this.lotPickerGroups = groups;
                } catch (e) {
                    // A rejected lookup (dropped connection, 409, server error)
                    // must not fall through to the empty state — "no sellable
                    // lot" would tell the cashier this stock cannot be sold and
                    // send a customer away over a network blip.
                    if (token !== this.lotPickerRequest) return;
                    this.lotPickerGroups = [];
                    this.lotPickerError = 'Could not load lots — check the connection and try again.';
                } finally {
                    if (token === this.lotPickerRequest) this.lotPickerLoading = false;
                }
            },

            // One wording for "this product cannot be sold because no lot
            // carries a price", shared by the click and the barcode paths.
            notifyNoPriceOnFile(product) {
                new FilamentNotification()
                    .title('No price on file')
                    .body(`"${product.name}" has no sellable lot with a price. Receive and price it on a GRN before it can be sold.`)
                    .danger()
                    .send();
            },

            // Creates or mutates a cart line for a chosen lot: the client
            // prices it from the lot for display only — InvoiceService
            // reprices authoritatively from the lot at checkout. One line per
            // (product, lot): picking a lot already on the cart grows that
            // line (up to the lot's stock), picking a different lot adds a
            // new line.
            addLineWithLot(product, lot, targetLine) {
                const serialTracked = !! product.has_serials;
                const lotMeta = {
                    batch_id: lot.id,
                    price: Number(lot.sale_price) || 0,
                    stock: Number(lot.available_quantity) || 0,
                    _lot_number: lot.internal_batch_number,
                    _lot_expiry: lot.expiry_date ?? null,
                    _lot_avail: Number(lot.available_quantity) || 0,
                };
                if (targetLine) {
                    Object.assign(targetLine, lotMeta);
                    targetLine.quantity = Math.min(Number(targetLine.quantity) || 1, targetLine.stock);
                    if (targetLine.quantity <= 0) {
                        this.removeItem(targetLine);
                        return;
                    }
                    if (serialTracked) {
                        targetLine.serial_ids = [];
                        this.loadSerials(targetLine);
                    }
                    return;
                }
                const existing = this.cart.find((line) => line.product_id === product.id && line.batch_id === lot.id) ?? null;
                if (existing) {
                    existing.quantity = Math.min(Number(existing.quantity) + 1, existing.stock || Number(product.quantity) || 1);
                    if (serialTracked) {
                        existing.serial_ids = [];
                        this.loadSerials(existing);
                    }
                    return;
                }
                const line = {
                    key: 'p' + product.id + '-' + Date.now(),
                    product_id: product.id,
                    bundle_id: null,
                    name: product.name,
                    code: product.sku ?? null,
                    vat_rate: Number(product.vat_rate) || 0,
                    quantity: 1,
                    discount_type: null,
                    discount_value: 0,
                    serial_ids: [],
                    has_serials: !! (product.has_serials ?? false),
                    base_unit: product.base_unit ?? null,
                    deal: product.deal ?? null,
                    is_batch_tracked: product.is_batch_tracked ?? true,
                    ...lotMeta,
                };
                this.cart.push(line);
                if (serialTracked) this.loadSerials(line);
            },

            // Enter in the search box, driven by a barcode scanner (scan →
            // Enter, same as a keyboard) or a cashier who just typed the full
            // SKU/barcode. Clears the box first so the next scan starts from a
            // full product list again, then hands off to handleScan() — the
            // same entry point the page-wide scan listener uses (see
            // resources/views/filament/partials/scan-capture-script.blade.php),
            // so a scan works identically whether or not this box has focus.
            async scanAdd() {
                const q = this.search.trim();
                this.search = '';
                if (q) await this.handleScan(q);
            },

            // The one place a scanned (or typed-then-Enter'd) code becomes a
            // cart line.
            //
            // Exact code match wins over an ambiguous text search; otherwise
            // the scan does nothing rather than guessing, unless the current
            // filter has already narrowed to exactly one product.
            //
            // A product match — whether it has one active lot or several — is
            // handed to addProduct(), the SAME path a manual card click takes.
            // A scan used to skip the lot picker and take the front lot
            // silently; it no longer does: which consignment gets sold is
            // always the cashier's call, never the scanner's. addProduct()
            // already adds straight through when there is only one lot to
            // choose from, so a single-lot product is no slower than before.
            async handleScan(code) {
                // A label the shop printed itself outranks everything: it is
                // not a product identifier at all, it is a whole cart line —
                // product, lot, serial and quantity, decided when the label was
                // made. Checked first and by prefix, so an ordinary SKU/barcode
                // scan never pays for the lookup.
                if (await this.scanPrintedLabel(code)) return;

                const q = code.trim().toLowerCase();
                if (! q) return;

                const exact = (this.products || []).find((p) =>
                    (p.barcode && p.barcode.toLowerCase() === q) || (p.sku && p.sku.toLowerCase() === q)
                );
                const target = exact || (this.filteredProducts.length === 1 ? this.filteredProducts[0] : null);
                if (! target) return;

                await this.addProduct(target);
            },

            // A scan of one of our own printed labels. Returns true when the
            // scan was handled here (whether it added a line or was refused),
            // so scanAdd() knows not to also try it as a product code.
            //
            // The prefix test is what keeps ordinary scanning free: only codes
            // in the label namespace ever reach the server, so a supplier
            // barcode is still matched entirely on the client.
            async scanPrintedLabel(code) {
                const prefix = this.labelPrefix || '';
                if (! prefix || ! code.toUpperCase().startsWith(prefix.toUpperCase())) return false;

                this.search = '';

                let result;
                try {
                    result = await $wire.resolveScan(code, this.selectedBranchId);
                } catch (e) {
                    // Same rule as the lot picker's failed fetch: a network
                    // blip must not read as "this label is invalid" and send a
                    // customer away.
                    new FilamentNotification()
                        .title('Could not read that label')
                        .body('Check the connection and scan again.')
                        .danger()
                        .send();

                    return true;
                }

                if (! result?.ok) {
                    new FilamentNotification()
                        .title('Label not usable')
                        .body(result?.message ?? 'That label could not be resolved.')
                        .danger()
                        .send();

                    return true;
                }

                this.addLabelLine(result);

                return true;
            },

            // Puts a resolved label straight on the cart. Everything a manual
            // add would stop to ask is already decided, so nothing here opens a
            // modal: the lot came with the label, the serial came with the
            // label, and the quantity is whatever that label stands for (a
            // 250 g label scanned twice is half a kilo).
            addLabelLine(label) {
                const product = (this.products || []).find((p) => p.id === label.product_id) ?? null;
                const lot = label.lot;
                const stock = Number(lot.available_quantity) || 0;
                const serialIds = label.serial_ids ?? [];
                const serialTracked = serialIds.length > 0 || !! product?.has_serials;

                const existing = this.cart.find(
                    (line) => line.product_id === label.product_id && line.batch_id === lot.id,
                ) ?? null;

                if (existing) {
                    if (serialIds.length) {
                        // One label, one physical item: scanning a second
                        // serial off the same lot grows the same line, and the
                        // quantity is simply how many serials are on it.
                        const merged = [...new Set([...existing.serial_ids, ...serialIds])];
                        existing.serial_ids = merged;
                        existing.quantity = merged.length;
                    } else {
                        existing.quantity = Math.min(
                            Math.round((Number(existing.quantity) + Number(label.quantity)) * 10000) / 10000,
                            stock > 0 ? stock : Infinity,
                        );
                    }

                    return;
                }

                this.cart.push({
                    key: 'p' + label.product_id + '-' + Date.now(),
                    product_id: label.product_id,
                    bundle_id: null,
                    name: product?.name ?? label.product_name,
                    code: product?.sku ?? null,
                    vat_rate: Number(product?.vat_rate) || 0,
                    quantity: serialIds.length ? serialIds.length : Number(label.quantity) || 1,
                    discount_type: null,
                    discount_value: 0,
                    serial_ids: serialIds,
                    has_serials: serialTracked,
                    base_unit: product?.base_unit ?? null,
                    deal: product?.deal ?? null,
                    is_batch_tracked: product?.is_batch_tracked ?? true,
                    batch_id: lot.id,
                    price: Number(lot.sale_price) || 0,
                    stock,
                    _lot_number: lot.internal_batch_number,
                    _lot_expiry: lot.expiry_date ?? null,
                    _lot_avail: stock,
                });
            },

            addBundle(bundle) {
                const existing = this.cart.find((line) => line.bundle_id === bundle.id);
                if (existing) {
                    existing.quantity += 1;
                    return;
                }
                this.cart.push({
                    key: 'b' + bundle.id + '-' + Date.now(),
                    product_id: null,
                    bundle_id: bundle.id,
                    name: bundle.name,
                    price: Number(bundle.sale_price) || 0,
                    vat_rate: 0,
                    quantity: 1,
                    discount_type: null,
                    discount_value: 0,
                    batch_id: null,
                    serial_ids: [],
                    deal: null,
                });
            },

            bumpQty(line, delta) {
                this.setQty(line, (Number(line.quantity) || 0) + delta);
            },

            setQty(line, raw) {
                const parsed = parseFloat(raw);
                let qty = (! raw || isNaN(parsed)) ? 0 : parsed;
                if (this.isSerialTracked(line)) qty = Math.round(qty);
                const max = Number(line.stock) > 0 ? Number(line.stock) : Infinity;
                qty = Math.min(Math.max(qty, 0), max);

                if (qty <= 0) {
                    this.removeItem(line);
                    return;
                }

                if (line.quantity !== qty) {
                    line.quantity = qty;
                    if (this.isSerialTracked(line)) line.serial_ids = [];
                }
            },

            setLineAmount(line, raw) {
                if (Number(line.price) <= 0) return;
                const parsed = parseFloat(raw);
                if (! raw || isNaN(parsed)) {
                    this.setQty(line, 0);
                    return;
                }
                const qty = Math.min(Math.max(parsed / Number(line.price), 0), Number(line.stock) > 0 ? Number(line.stock) : Infinity);
                this.setQty(line, qty);
            },

            removeItem(line) {
                this.cart = this.cart.filter((l) => l.key !== line.key);
            },

            setLineSerials(line, ids) {
                line.serial_ids = ids;
            },

            // ── Lot picker (modal) ──────────────────────────────────────────
            priceLabel(product) {
                const min = Number(product.price_min);
                const max = Number(product.price_max);
                const count = Number(product.lot_count) || 0;
                if (! count || ! (min > 0)) return '—';
                if (count === 1 || min === max) return this.money(min);
                return 'Rs. ' + min.toFixed(2) + ' – ' + max.toFixed(2);
            },

            // "Change lot" / "Select lot" on a cart line: always fetches, since
            // seeing the live lot list IS the point here — unlike addProduct(),
            // there is no single-lot shortcut to take. The modal opens on the
            // click and fills in, so the button never looks dead.
            async openLotPicker(productId, line) {
                const product = (this.products || []).find((p) => p.id === productId) ?? null;
                if (! product) return;
                // Non-batch unified pool has no lots to pick from.
                if (product.is_batch_tracked === false) return;
                const token = ++this.lotPickerRequest;
                this.lotPickerLoading = true;
                this.lotPickerError = '';
                this.showLotPicker(product, line, []);
                try {
                    const groups = await $wire.productLots(productId, this.selectedBranchId);
                    if (token !== this.lotPickerRequest) return;
                    this.lotPickerGroups = groups ?? [];
                } catch (e) {
                    // See addProduct(): an empty modal would read as "nothing
                    // sellable here" rather than "the lookup failed".
                    if (token !== this.lotPickerRequest) return;
                    this.lotPickerGroups = [];
                    this.lotPickerError = 'Could not load lots — check the connection and try again.';
                } finally {
                    if (token === this.lotPickerRequest) this.lotPickerLoading = false;
                }
            },

            // Opens the modal with an already-fetched group list.
            showLotPicker(product, line, groups) {
                this.lotPickerProduct = product;
                this.lotPickerTargetLine = line ?? null;
                this.lotPickerGroups = groups;
                this.lotPickerOpen = true;
            },

            pickLot(group, lot) {
                if (! this.lotPickerProduct) return;
                if (! this.canOverrideBatch && ! lot.is_fefo_front) return; // server enforces too
                this.addLineWithLot(this.lotPickerProduct, lot, this.lotPickerTargetLine);
                this.closeLotPicker();
            },

            closeLotPicker() {
                // Bumping the token abandons any in-flight fetch, so a response
                // that lands after the cashier dismissed the modal cannot pop it
                // back open.
                this.lotPickerRequest++;
                this.lotPickerOpen = false;
                this.lotPickerLoading = false;
                this.lotPickerProduct = null;
                this.lotPickerGroups = [];
                this.lotPickerTargetLine = null;
                this.lotPickerError = '';
            },

            lotNumber(line) {
                return line._lot_number ?? ('Lot #' + line.batch_id);
            },

            lotTitle(line) {
                const parts = [this.lotNumber(line)];
                if (line._lot_expiry) parts.push('exp ' + line._lot_expiry);
                if (line._lot_avail !== undefined) parts.push(this.formatQty(line._lot_avail) + ' available');
                return parts.join(' · ');
            },

            // ── Tracking lookups (cached, fetched once per product) ─
            serialsFor(line) {
                if (line.product_id == null) return [];
                if (! this._serials[line.product_id]) {
                    this._serials[line.product_id] = [];
                    this.loadSerials(line);
                }
                return this._serials[line.product_id];
            },

            // Serial pickers are scoped to the line's selected lot — a serial
            // must come from the same consignment the line is priced from.
            async loadSerials(line) {
                const serials = await $wire.availableSerials(line.product_id, this.selectedBranchId, line.batch_id ?? null);
                this._serials[line.product_id] = serials ?? [];
                if (line.serial_ids.length === 0 && this.requiresSerial(line)) {
                    // nothing to auto-select — serials stay manual
                }
            },

            serialLabel(line) {
                const n = Math.max(1, Math.round(line.quantity));
                return 'Select ' + n + ' serial' + (n === 1 ? '' : 's') + ' (' + line.serial_ids.length + '/' + n + ' selected)';
            },

            serialsSize(line) {
                const count = this.serialsFor(line).length;
                return Math.min(5, Math.max(3, count));
            },

            // ── Server boundaries ───────────────────────────────────────────
            cartPayload() {
                return this.cart.map((line) => ({
                    product_id: line.product_id,
                    bundle_id: line.bundle_id,
                    name: line.name,
                    quantity: Number(line.quantity),
                    price: Number(line.price),
                    vat_rate: Number(line.vat_rate) || 0,
                    batch_id: line.batch_id ?? null,
                    serial_ids: line.serial_ids ?? [],
                    discount_type: line.discount_type ?? null,
                    discount_value: Number(line.discount_value) || 0,
                }));
            },

            /**
             * Branch switcher in the cart sidebar — a full navigation, because
             * the entire catalog, stock, bundles and held-sale list are branch
             * scoped and are rebuilt server-side.
             */
            switchBranch(branchId) {
                if (! branchId) return;
                const url = new URL(window.location.href);
                url.searchParams.set('branch_id', String(branchId));
                window.location.href = url.toString();
            },

            /**
             * Blocking picker: persist the choice server-side first, then
             * reload clean so the catalog and till list come back scoped to it.
             * Going through $wire rather than a query param keeps the branch
             * remembered for the next visit to this device.
             */
            async chooseBranch(branchId) {
                if (! branchId || this.submitting) return;
                this.submitting = true;
                this.errors = [];
                const result = await $wire.selectBranch(branchId);
                if (! result?.ok) {
                    this.submitting = false;
                    this.errors = result?.errors ?? ['Could not select that branch.'];
                    return;
                }
                window.location.reload();
            },

            async assignTill() {
                if (! this.tillId) return;
                await $wire.assignTill(this.tillId);
                window.location.reload();
            },

            // True once the entered amount departs from the carried-forward
            // closing balance — that's when a reason becomes mandatory.
            get needsOpeningReason() {
                if (this.previousClosingBalance === null) return false;
                const amount = Number(this.openingBalance);
                if (!Number.isFinite(amount)) return true;
                return Math.abs(amount - Number(this.previousClosingBalance)) > 0.001;
            },

            async startSession() {
                this.submitting = true;
                this.errors = [];

                const amount = Number(this.openingBalance);

                if (!Number.isFinite(amount) || amount < 0) {
                    this.submitting = false;
                    this.errors = ['Please enter a valid opening balance.'];
                    return;
                }

                if (this.needsOpeningReason && ! this.openingNote.trim()) {
                    this.submitting = false;
                    this.errors = ['Please enter a reason for the different opening balance.'];
                    return;
                }

                const result = await $wire.startSession(
                    amount,
                    this.needsOpeningReason ? this.openingNote.trim() : null
                );

                this.submitting = false;

                if (! result?.ok) {
                    this.errors = result?.errors ?? ['Could not open the session.'];
                    return;
                }

                window.location.reload();
            },

            openCloseTill() {
                this.closeTillOpen = true;
                this.countedCash = null;
                this.closingNote = '';
                this.reconciliation = null;
                this.loadReconciliation();
            },

            async loadReconciliation() {
                const result = await $wire.sessionReconciliation();
                if (result?.ok) {
                    this.reconciliation = result.reconciliation;
                } else {
                    this.closeTillOpen = false;
                }
            },

            get hasVariance() {
                const counted = parseFloat(this.countedCash) || 0;
                return this.countedCash !== null && this.countedCash !== '' && Math.abs(counted - (Number(this.reconciliation?.expected_balance) || 0)) > 0.001;
            },

            get variance() {
                const counted = parseFloat(this.countedCash) || 0;
                return counted - (Number(this.reconciliation?.expected_balance) || 0);
            },

            get canOpenTill() {
                const amount = Number(this.openingBalance);
                if (!Number.isFinite(amount) || amount < 0 || this.openingBalance === '') return false;
                if (this.needsOpeningReason && ! this.openingNote.trim()) return false;
                return true;
            },

            async closeSession() {
                this.submitting = true;

                const countedCash =
                    this.countedCash === null || this.countedCash === ''
                        ? Number(this.reconciliation?.expected_balance) || 0
                        : Number(this.countedCash);

                const result = await $wire.closeSession(
                    countedCash,
                    null
                );

                this.submitting = false;

                if (! result?.ok) {
                    this.errors = result?.errors ?? ['Could not close the till.'];
                    return;
                }

                // The server has closed the session and removed pos_till_id.
                // Update Alpine immediately so the Open Till dialog appears again.
                this.activeSession = null;
                this.closeTillOpen = false;
                this.reconciliation = null;

                // Keep the selected till so the Open Till dialog can use it.
                this.tillId = this.selectedTillId;

                // Immediately refresh the previous closing balance from the database.
                // This prevents Option 2 from showing a stale value until F5.
                const latestBalance = await $wire.latestPreviousClosingBalance(this.selectedTillId);

                this.previousClosingBalance =
                    latestBalance === null ? null : Number(latestBalance);

                // Reset the Open Till dialog back to its default, carried-forward state.
                this.overrideOpeningBalance = false;
                this.openingNote = '';
                this.openingBalance = this.previousClosingBalance !== null
                    ? this.previousClosingBalance
                    : '';
            },

            openWithdraw() {
                this.withdrawOpen = true;
                this.errors = [];
                this.cmType = 'out';
                this.cmAmount = 0;
                this.cmReasonId = null;
                this.cmNote = '';
                this.reconciliation = null;
                this.loadReconciliation();
            },

            get availableCash() {
                return Number(this.reconciliation?.expected_balance) || 0;
            },

            get cmExceedsAvailable() {
                return this.cmType === 'out' && Number(this.cmAmount) > this.availableCash;
            },

            get noteRequired() {
                if (this.cmType !== 'out' || ! this.cmReasonId) return false;
                const reason = (this.cashMovementReasons || []).find((r) => Number(r.id) === Number(this.cmReasonId));
                return reason?.requires_note ?? false;
            },

            get cmCanSubmit() {
                return Number(this.cmAmount) > 0
                    && (this.cmType === 'in' || !! this.cmReasonId)
                    && (! this.noteRequired || this.cmNote.trim().length > 0)
                    && ! this.cmExceedsAvailable;
            },

            async submitCashMovement() {
                this.errors = [];
                const result = await $wire.createCashMovement({
                    type: this.cmType,
                    amount: Number(this.cmAmount),
                    reason_id: this.cmReasonId || null,
                    note: this.cmNote.trim() || null,
                });
                if (! result?.ok) {
                    this.errors = result?.errors ?? ['Could not record the cash movement.'];
                    return;
                }
                this.withdrawOpen = false;

                // Prefer the balance the server already computed (one less round-trip);
                // fallback to full reconciliation when the server didn't return it.
                if (result?.expected_balance !== undefined && result.expected_balance !== null) {
                    const bal = Number(result.expected_balance);
                    if (this.activeSession) {
                        this.activeSession = { ...this.activeSession, expected_cash: bal };
                    }
                    if (this.reconciliation) {
                        this.reconciliation = { ...this.reconciliation, expected_balance: bal };
                    }
                } else {
                    const rec = await $wire.sessionReconciliation();
                    if (rec?.ok && rec.reconciliation) {
                        this.reconciliation = rec.reconciliation;
                        if (this.activeSession) {
                            this.activeSession = { ...this.activeSession, expected_cash: rec.reconciliation.expected_balance };
                        }
                    }
                }

                if (result?.movement?.can_print) {
                    this.cmMovement = result.movement;
                    this.cmReceiptOpen = true;
                    this.cmReceiptLoading = true;
                    const doc = await $wire.cashMovementReceiptDocument(Number(result.movement.id));
                    this.cmReceiptLoading = false;
                    this.cmReceiptHtml = doc?.ok ? doc.html : '';
                }
            },

            closeCmReceipt() {
                this.cmReceiptOpen = false;
                this.cmMovement = null;
                this.cmReceiptHtml = '';
            },

            printCmReceipt() {
                // Same isolated-document path as the sale receipt — a cash
                // movement slip is always the 80mm roll.
                this.printDocument(this.cmReceiptHtml, 'thermal80');
            },

            openAmend() {
                this.amendOpen = true;
                this.amendLoading = true;
                this.amendError = null;
                this.amendLastSale = null;
                this.amendCanAmend = false;
                $wire.lastSale().then((result) => {
                    this.amendLoading = false;
                    if (result?.invoice) {
                        this.amendLastSale = result.invoice;
                        this.amendCanAmend = result.can_amend;
                    }
                });
            },

            async confirmAmend() {
                if (! this.amendLastSale || ! this.amendCanAmend) return;
                this.amendLoading = true;
                const amended = await $wire.amendSale(this.amendLastSale.id);
                this.amendLoading = false;
                if (! amended?.ok) {
                    this.amendError = (amended?.errors ?? ['Could not amend.']).join('\n');
                    return;
                }
                this.hydrateCart(amended.payload);
                this.amendOpen = false;
            },

            // Rehydrates the cart from a held sale / amended sale snapshot
            hydrateCart(payload) {
                const { header, items } = payload;
                this.customerId = header.customer_id ? String(header.customer_id) : '';
                this.discountType = header.discount_type ?? 'fixed';
                this.discountValue = Number(header.discount_value) || 0;
                this.cart = (items ?? []).map((item, index) => {
                    const product = (this.products || []).find((p) => p.id === item.product_id) ?? null;
                    return {
                        key: 'r' + index + '-' + Date.now(),
                        product_id: item.product_id,
                        bundle_id: item.product_bundle_id,
                        name: item.product_name,
                        code: product?.sku ?? null,
                        price: Number(item.unit_price) || 0,
                        vat_rate: Number(item.vat_rate) || 0,
                        quantity: Number(item.quantity) || 0,
                        discount_type: item.discount_type ?? null,
                        discount_value: Number(item.discount_value) || 0,
                        batch_id: item.stock_batch_id ?? null,
                        serial_ids: item.serial_ids ?? [],
                        has_serials: !! (product?.has_serials ?? false),
                        base_unit: product?.base_unit ?? null,
                        stock: Number(product?.quantity) || Number(item.quantity) || 0,
                        // Deals are re-evaluated from the live catalog on
                        // resume, so the display matches what InvoiceService
                        // will authoritatively charge on checkout.
                        deal: product?.deal ?? null,
                    };
                });
                this.loadCustomerContext();
            },

            async selectCustomer(id) {
                this.customerId = id ? String(id) : '';
                this.selectedCreditNotes = [];
                this.overpayAction = 'cash';
                this.appliedVoucher = null;
                this.voucherCode = '';
                this.voucherError = '';
                this.redeemPointsInput = '';
                this.loadCustomerContext();
            },

            async loadCustomerContext() {
                if (! this.customerId) {
                    this.customerContextData = {};
                    return;
                }
                const data = await $wire.customerContext(Number(this.customerId));
                if (data) this.customerContextData = data;
            },

            openCheckout() {
                this.errors = [];
                this.checkoutOpen = true;
                // 'cash' is the default tender, but a tenant may have toggled
                // it off — fall back to whatever's actually accepted.
                if (! this.paymentMethods.some((m) => m.code === this.activeMethod)) {
                    this.activeMethod = this.paymentMethods[0]?.code ?? this.activeMethod;
                }
                this.$nextTick(() => this.$refs.paymentAmountInput?.focus());
            },

            toggleCreditNote(id, checked) {
                this.selectedCreditNotes = checked
                    ? [...this.selectedCreditNotes, id]
                    : this.selectedCreditNotes.filter((x) => x !== id);
            },

            checkVoucher() {
                if (! this.voucherCode.trim()) return;
                this.voucherChecking = true;
                this.voucherError = '';
                $wire.previewVoucher(
                    this.voucherCode.trim(),
                    this.customerId ? Number(this.customerId) : null,
                    this.selectedBranchId,
                    this.netSubtotal + this.vatTotal,
                ).then((res) => {
                    this.voucherChecking = false;
                    if (res?.ok) {
                        this.appliedVoucher = { code: this.voucherCode.trim(), name: res.name, discount: Number(res.discount) || 0 };
                    } else {
                        this.appliedVoucher = null;
                        this.voucherError = res?.message ?? 'Invalid voucher.';
                    }
                });
            },

            clearVoucher() {
                this.appliedVoucher = null;
                this.voucherCode = '';
                this.voucherError = '';
            },

            uploadCustomFieldFile(field, file) {
                if (! file) return;
                this.uploadingField = field.key;
                $wire.upload('paymentAttachment', file, () => {
                    $wire.call('uploadPaymentAttachment').then((res) => {
                        this.uploadingField = null;
                        if (res?.ok) this.customFieldValues[field.key] = res.path;
                    });
                }, () => {
                    this.uploadingField = null;
                });
            },

            addPayment() {
                const amount = this.enteredAmount;
                if (! amount || amount <= 0) return;
                if (this.activeMethod !== 'cash' && amount > this.remaining + 0.001) return;
                if (this.activeCustomMethod && this.missingRequiredCustomField) return;
                if (this.missingRequiredMethodFields) return;

                const payment = { method: this.activeMethod, amount: Math.round(amount * 100) / 100 };
                if (this.activeMethod === 'cheque') {
                    payment.cheque_number = this.pmChequeNumber;
                    payment.cheque_bank = this.pmChequeBank;
                    payment.cheque_branch = this.pmChequeBranch;
                    payment.cheque_date = this.pmChequeDate;
                }
                if (this.activeMethod === 'card' || this.activeMethod === 'bank_transfer' || this.activeMethodConfig?.requiresReference) {
                    payment.reference = this.pmReference;
                }
                if (this.activeCustomMethod) {
                    payment.custom_fields = this.customFieldValues;
                }

                this.payments.push(payment);
                this.pmAmount = '';
                this.pmReference = '';
                this.pmChequeNumber = '';
                this.pmChequeBank = '';
                this.pmChequeBranch = '';
                this.pmChequeDate = '';
                this.customFieldValues = {};
            },

            /**
             * Port of the source checkout-sheet's buildSplits(): each split is
             * capped at what is still payable, so the payload never claims more
             * than the invoice balance. PaymentService::settleInvoice() rejects
             * splits that exceed the balance outright ("Splits exceed invoice
             * balance."), so a cashier tendering 150 on a 100 sale must post a
             * 100 split plus overpay_amount: 50 — never a 150 split.
             */
            buildSplits() {
                let budget = this.amountPayable;
                const out = [];
                for (const p of this.payments) {
                    if (budget <= 0.005) break;
                    const amount = Math.min(Number(p.amount) || 0, budget);
                    out.push({
                        method: p.method,
                        amount: Math.round(amount * 100) / 100,
                        reference: p.reference ?? null,
                        cheque_number: p.cheque_number ?? null,
                        cheque_bank: p.cheque_bank ?? null,
                        cheque_branch: p.cheque_branch ?? null,
                        cheque_date: p.cheque_date ?? null,
                        custom_fields: p.custom_fields ?? null,
                    });
                    budget -= amount;
                }
                return out;
            },

            async completeSale() {
                this.submitting = true;
                this.errors = [];

                const overpay = this.tendered - this.amountPayable;

                const result = await $wire.completeCheckout({
                    items: this.cartPayload(),
                    payments: this.buildSplits(),
                    discount: this.invoiceDiscountAmount,
                    customer_id: this.customerId ? Number(this.customerId) : null,
                    order_notes: this.orderNotes.trim() || null,
                    credit_note_ids: this.selectedCreditNotes,
                    voucher_codes: this.appliedVoucher ? [this.appliedVoucher.code] : [],
                    redeem_points: this.redeemPoints,
                    overpay_action: (overpay > 0.001 && this.selectedCustomer) ? this.overpayAction : null,
                    overpay_amount: overpay > 0.001 ? Number(overpay.toFixed(2)) : null,
                });

                this.submitting = false;

                if (! result?.ok) {
                    this.errors = result?.errors ?? ['Checkout failed.'];
                    return;
                }

                // The server doesn't echo back updated stock, and a full page
                // reload here would re-fetch the entire catalog after every
                // single sale — the biggest source of drag in a high-volume
                // till. The cart already knows exactly what left the shelf,
                // so deduct it locally instead.
                this.deductSoldStock();

                // Refresh the till's expected cash immediately after checkout.
                const session = await $wire.sessionReconciliation();

                if (session?.ok && session.reconciliation) {
                    this.reconciliation = session.reconciliation;

                    if (this.activeSession) {
                        this.activeSession = {
                            ...this.activeSession,
                            expected_cash: session.reconciliation.expected_balance,
                        };
                    }
                }

                this.resetCart();
                this.checkoutOpen = false;
                this.lastReceipt = result.receipt ?? null;
                this.receiptPaper = 'thermal80';
                this.saleCompleteOpen = true;
                this.loadReceipt();
            },

            async loadReceipt() {
                if (! this.lastReceipt?.can_print) {
                    this.receiptHtml = '';
                    return;
                }

                this.receiptLoading = true;
                const result = await $wire.receiptDocument(this.lastReceipt.invoice_id, this.receiptPaper);
                this.receiptLoading = false;
                this.receiptHtml = result?.ok ? result.html : '';
            },

            setReceiptPaper(paper) {
                if (this.receiptPaper === paper) return;
                this.receiptPaper = paper;
                this.loadReceipt();
            },

            /**
             * Prints one sheet as a document of its own.
             *
             * The receipt layouts (invoices.print.{a4,thermal80}) carry their
             * own scoped <style>, so a bare document holding just that HTML
             * prints exactly what the modal previews. Because the frame holds
             * nothing else, the sheet IS the print job — one copy, no trailing
             * blank pages, and nothing depending on how tall the POS screen
             * behind the modal happens to be.
             *
             * Printing out of the main document is what produced duplicates:
             * see the note on the receipt CSS above.
             */
            printDocument(html, paper) {
                if (!html) return;

                const frame = document.createElement('iframe');

                frame.setAttribute('aria-hidden', 'true');

                /*
                * IMPORTANT:
                *
                * Do NOT use:
                *   width: 1px
                *   height: 1px
                *   opacity: 0
                *
                * Chrome can treat the iframe as having no printable layout.
                *
                * Keep the iframe rendered, but place it far outside the viewport.
                */
                const frameWidth = paper === 'thermal80' ? '80mm' : '210mm';

                frame.style.cssText = [
                    'position: fixed',
                    'left: -10000px',
                    'top: 0',
                    `width: ${frameWidth}`,
                    'height: 100vh',
                    'border: 0',
                    'visibility: visible',
                ].join(';');

                const page = paper === 'thermal80'
                    ? '@page { size: 80mm auto; margin: 0; }'
                    : '@page { size: A4 portrait; margin: 0; }';

                let started = false;

                const cleanup = () => {
                    if (frame.parentNode) {
                        frame.remove();
                    }
                };

                const start = () => {
                    if (started) return;
                    started = true;

                    const printWindow = frame.contentWindow;

                    if (!printWindow) {
                        cleanup();
                        return;
                    }

                    printWindow.focus();
                    printWindow.print();

                    printWindow.addEventListener(
                        'afterprint',
                        cleanup,
                        { once: true }
                    );

                    // Fallback for browsers that do not fire afterprint.
                    setTimeout(cleanup, 60000);
                };

                document.body.appendChild(frame);

                const doc = frame.contentDocument;

                if (!doc) {
                    cleanup();
                    return;
                }

                doc.open();

                doc.write(`
                    <!DOCTYPE html>
                    <html lang="en">
                    <head>
                        <meta charset="utf-8">
                        <meta name="viewport" content="width=device-width, initial-scale=1">

                        <base href="${document.baseURI}">

                        <style>
                            ${page}

                            html,
                            body {
                                margin: 0;
                                padding: 0;
                                background: #fff;
                            }

                            html {
                                width: 100%;
                            }

                            body {
                                width: 100%;
                            }

                            @media print {
                                html,
                                body {
                                    margin: 0 !important;
                                    padding: 0 !important;
                                    background: #fff !important;
                                }
                            }
                        </style>
                    </head>

                    <body>
                        ${html}
                    </body>
                    </html>
                `);

                doc.close();

                /*
                * document.write() + document.close() does not always produce a
                * reliable load event in every browser, especially for dynamically
                * generated HTML.
                *
                * Give the browser time to:
                *   - parse the receipt
                *   - apply the receipt CSS
                *   - load the logo
                *   - calculate the final receipt height
                */
                const waitForImages = Array.from(
                    doc.images || []
                );

                Promise.all(
                    waitForImages.map((image) => {
                        if (image.complete) {
                            return Promise.resolve();
                        }

                        return new Promise((resolve) => {
                            image.addEventListener('load', resolve, { once: true });
                            image.addEventListener('error', resolve, { once: true });
                        });
                    })
                ).then(() => {
                    setTimeout(start, 100);
                });

                /*
                * Absolute fallback.
                */
                setTimeout(start, 2000);
            },

            printReceipt() {
                this.printDocument(this.receiptHtml, this.receiptPaper);
            },

            /**
             * Scales the receipt sheet to fit the modal — same technique as the
             * Master Control live preview (invoice-preview.blade.php's fit()):
             * `transform: scale()` shrinks it visually without reflowing the
             * document, so what's previewed here still matches what prints.
             * offsetWidth/offsetHeight are unaffected by a transform, so this
             * measures the sheet's true (unscaled) size on every call.
             */
            fitReceipt() {
                const stage = this.$refs.receiptStage;
                const doc = this.$refs.receiptDoc;
                if (! stage || ! doc) return;

                const pad = 32;
                const natural = doc.offsetWidth;
                const avail = stage.clientWidth - pad;
                if (natural <= 0 || avail <= 0) return;

                const scale = Math.max(0.2, Math.min(1, avail / natural));
                const height = Math.round(doc.offsetHeight * scale + pad);
                const offset = Math.round(Math.max(0, (avail - natural * scale) / 2));

                if (this.receiptScale === scale && this.receiptStageHeight === height && this.receiptOffset === offset) return;

                this.receiptScale = scale;
                this.receiptStageHeight = height;
                this.receiptOffset = offset;
            },

            deductSoldStock() {
                for (const line of this.cart) {
                    if (! line.product_id) continue;
                    const product = this.products.find((p) => p.id === line.product_id);
                    if (! product) continue;
                    product.quantity = Math.max(0, Number(product.quantity) - Number(line.quantity));
                    this.deductSoldLot(product, line);
                }
            },

            // The card's cached front lot is what addProduct()'s no-round-trip
            // path sells from, so it has to age with the shelf exactly as the
            // product's own quantity does. Without this, a second sale of the
            // same single-lot product would still be capped against the lot's
            // page-load quantity — the cashier could ring up units that are no
            // longer there and only find out when checkout rejects the sale.
            //
            // Emptying the front lot clears it rather than guessing at the next
            // one: `front_lot` is the only lot the payload carries, so a null
            // here is what pushes the next click back onto the authoritative
            // server lookup instead of onto a lot we cannot name.
            deductSoldLot(product, line) {
                const lot = product.front_lot;
                if (! lot || lot.id !== line.batch_id) return;
                lot.available_quantity = Math.max(0, Number(lot.available_quantity) - Number(line.quantity));
                if (lot.available_quantity > 0) return;
                product.lot_count = Math.max(0, Number(product.lot_count) - 1);
                product.front_lot = null;
            },

            closeSaleComplete() {
                this.saleCompleteOpen = false;
                this.lastReceipt = null;
                this.receiptHtml = '';
            },

            resetCart() {
                this.cart = [];
                this.payments = [];
                this.discountType = 'fixed';
                this.discountValue = 0;
                this.orderNotes = '';
                this.customerId = null;
                this.customerContextData = {};
                this.selectedCreditNotes = [];
                this.voucherCode = '';
                this.appliedVoucher = null;
                this.voucherError = '';
                this.redeemPointsInput = '';
                this.overpayAction = 'cash';
            },

            async hold() {
                // Prevent holding an empty cart or sending multiple requests.
                if (this.cart.length === 0 || this.holding) {
                    return;
                }

                // Lock immediately, before the Livewire request starts.
                this.holding = true;

                try {
                    const result = await $wire.holdSale({
                        items: this.cartPayload(),
                        customer_id: this.customerId ? Number(this.customerId) : null,
                        discount_type: this.discountType,
                        discount_value: this.discountValue || 0,
                    });

                    if (! result?.ok) {
                        this.errors = result?.errors ?? ['Could not hold the sale.'];
                        return;
                    }

                    this.resetCart();

                    if (result.held_sale) {
                        this.heldSales = [
                            result.held_sale,
                            ...this.heldSales.filter(
                                sale => sale.id !== result.held_sale.id
                            ),
                        ];
                    }
                } finally {
                    // Unlock after the request has completely finished.
                    this.holding = false;
                }
            },

            resumeRequest(sale) {
                if (this.cart.length === 0) {
                    this.resume(sale);
                } else {
                    this.confirmResumeId = sale.id;
                }
            },

            async resume(sale) {
                if (! sale) return;
                const result = await $wire.resumeSale(sale.id);
                if (! result?.ok) return;

                this.hydrateCart(result.payload);
                this.heldSales = this.heldSales.filter((s) => s.id !== sale.id);
                this.heldOpen = false;
            },

            async discard(sale) {
                if (! sale) return;
                const result = await $wire.discardHeldSale(sale.id);
                if (result?.ok) {
                    this.heldSales = this.heldSales.filter((s) => s.id !== sale.id);
                }
            },
        }));
    </script>
    @endscript
</x-filament-panels::page>