{
  "version": 3,
  "sources": ["/cdn/shop/t/226/assets/btk-basket-api.js?cb=70"],
  "sourcesContent": ["/**\n * btk-basket-api.js\n * Better Trade Kitchens - Basket API Abstraction Layer\n *\n * The UI layer (btk-cart-advanced.js) calls ONLY these methods.\n * The adapter below handles the actual implementation.\n *\n * TO SWITCH TO SERVER: change ADAPTER to 'server' and fill in\n * ServerAdapter methods with fetch calls to your Render node.\n * No other files need to change.\n */\n\nconst BTKBasketAPI = (() => {\n\n  // \u2500\u2500\u2500 CONFIG \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n  const STORAGE_PREFIX   = 'btk_project_'; // keyed by customer ID below\n  const GUEST_COOKIE_KEY = 'btk_guest_id';\n  // Logged-in customers persist to the server via the Shopify App Proxy\n  // (same-origin fetch \u2192 Shopify adds the trusted logged_in_customer_id + HMAC,\n  // verified by btk-planner-api). Guests stay on localStorage.\n  // Flip SERVER_ENABLED to false to force everyone onto localStorage.\n  const SERVER_ENABLED   = true;\n  const SERVER_BASE_URL  = '/apps/planner';\n\n  // Staff/deep-link override: a ?cid= in the URL (e.g. Odoo's \"Edit in planner\"\n  // link for a specific customer) makes the store act AS that customer - it\n  // loads/saves their server project even though the viewer isn't the logged-in\n  // customer. Captured once at load (the planner strips the query shortly\n  // after boot). Client-asserted; the App Proxy still HMAC-signs the request.\n  // Staff mode is captured from the URL ONCE and then held in sessionStorage.\n  // It used to live only in this closure, so it survived exactly one page load:\n  // the planner strips the query string on boot, and the moment a salesperson\n  // navigated anywhere - to the cart, to a product, or just refreshed - they\n  // silently stopped acting as the customer and became themselves again.\n  //\n  // sessionStorage alone was NOT enough, though. Browsers COPY a tab's\n  // sessionStorage into any tab opened from it (and into a duplicated tab), so\n  // staff mode spread into the salesperson's other tabs - and \"Exit\" only\n  // cleared the one tab it was pressed in, leaving the copies acting as the\n  // customer. Two guards close that:\n  //   1. The override is bound to THIS tab by a token held in window.name,\n  //      which survives navigation within a tab but is not given to tabs opened\n  //      from it. A copied override finds a different window.name and is\n  //      discarded rather than honoured.\n  //   2. Exiting is broadcast, so any tab still holding the same customer's\n  //      override drops it too.\n  const STAFF_KEY = 'btk-staff-override-v1';\n  const STAFF_TAB_PREFIX = 'btk-staff-tab:';\n  const STAFF_CHANNEL = 'btk-staff-mode';\n  function staffTabToken(create) {\n    let name = '';\n    try { name = String(window.name || ''); } catch (e) {}\n    if (name.indexOf(STAFF_TAB_PREFIX) === 0) return name.slice(STAFF_TAB_PREFIX.length);\n    if (!create) return '';\n    const token = Math.random().toString(36).slice(2) + Date.now().toString(36);\n    try { window.name = STAFF_TAB_PREFIX + token; } catch (e) {}\n    return token;\n  }\n  // A refused one-time link is kept for the sign-in round trip (StaffBlock).\n  const STAFF_PENDING_KEY = 'btk-staff-pending-v1';\n  const STAFF_PENDING_TTL = 10 * 60 * 1000;   // the link's own lifetime\n  function forgetStaffOverride() {\n    try { sessionStorage.removeItem(STAFF_KEY); } catch (e) {}\n    try { sessionStorage.removeItem(STAFF_PENDING_KEY); } catch (e) {}\n    try {\n      if (String(window.name || '').indexOf(STAFF_TAB_PREFIX) === 0) window.name = '';\n    } catch (e) {}\n  }\n  // The \"Switch customer\" address Odoo sends: <odoo base>/web#... Anything\n  // else (a javascript: URL above all) is dropped, never navigated to.\n  function safeStaffBack(v) {\n    try {\n      const u = new URL(String(v || ''));\n      return (/^https?:$/.test(u.protocol) && /^\\/web\\/?$/.test(u.pathname)) ? u.href : '';\n    } catch (e) { return ''; }\n  }\n  const _staff = (() => {\n    const NONE = { cid: '', cidt: '', back: '', name: '' };\n    let fromUrl = { cid: '', cidt: '', back: '' };\n    let role = '';\n    try {\n      const q = new URLSearchParams(window.location.search);\n      // Odoo's one-time links carry the token after '#', which browsers never\n      // send to a server or put in a Referer. Take it, then wipe it from the\n      // address bar at once so it is not left in history or read by a script.\n      const h = new URLSearchParams(String(window.location.hash || '').replace(/^#/, ''));\n      const hashToken = h.get('cidt') || '';\n      if (hashToken) {\n        try { history.replaceState(history.state, '', window.location.pathname + window.location.search); } catch (e) {}\n      }\n      role = String(q.get('role') || '').trim().toLowerCase();\n      const qcid = q.get('cid') || h.get('cid') || '';\n      // A one-time link held across a trip through the website sign-in, which\n      // drops the '#' part of the address (see StaffBlock). Same tab, same\n      // customer, inside its 10 minutes - and planner-api still decides.\n      let pendingLink = '';\n      if (hashToken && /^L\\./.test(hashToken) && qcid) {\n        try { sessionStorage.setItem(STAFF_PENDING_KEY, JSON.stringify({ cid: qcid, link: hashToken, exp: Date.now() + STAFF_PENDING_TTL })); } catch (e) {}\n      } else if (!hashToken && qcid) {\n        try {\n          const p = JSON.parse(sessionStorage.getItem(STAFF_PENDING_KEY) || 'null');\n          if (p && String(p.cid) === String(qcid) && p.exp > Date.now() && /^L\\./.test(p.link || '')) pendingLink = p.link;\n          else if (p) sessionStorage.removeItem(STAFF_PENDING_KEY);\n        } catch (e) {}\n      }\n      fromUrl = {\n        cid:  qcid,\n        cidt: hashToken || pendingLink || q.get('cidt') || '',\n        back: safeStaffBack(q.get('staffBack')),\n        // Display-only, sent by Odoo: the planner API only ever learns an email\n        // address, and an email is the wrong thing to print on a customer's own\n        // plan report. Nothing is authorised by this - the signed token still\n        // decides who may be acted as.\n        name: q.get('cname')     || '',\n        // Odoo's \"New Website Basket\" (?newBasket=1) or \"Edit in Basket\"\n        // (/cart?basket=<id>): a basket session, not a planner one. Held with\n        // the rest, so the bar stays purple as staff browse.\n        basket: q.get('newBasket') === '1' || !!q.get('basket'),\n        // The Odoo opportunity this session was started from (its New Plan /\n        // New Website Basket). Held with the rest, so every basket made in\n        // the session is stamped with it - see STAFF_OPPORTUNITY.\n        opp: /^\\d{1,12}$/.test(q.get('opp') || '') ? q.get('opp') : ''\n      };\n    } catch (e) { /* no URL access */ }\n    // The CUSTOMER watching a live session is never in staff mode, whatever the\n    // link carries. Older viewer links included the salesperson's staff pass,\n    // which put the customer's own browser into staff mode (the green bar, an\n    // Exit button) and made their tab authenticate - and be audited - as the\n    // salesperson. Refuse it outright, and clear anything a copy left behind.\n    if (role === 'viewer') {\n      forgetStaffOverride();\n      return NONE;\n    }\n    // A ONE-TIME link token (L.) is not stored or sent anywhere except the\n    // exchange below, which swaps it for the working token this tab keeps.\n    if (fromUrl.cid && /^L\\./.test(fromUrl.cidt)) {\n      staffTabToken(true);\n      return Object.assign({}, fromUrl, { link: fromUrl.cidt, cidt: '' });\n    }\n    if (fromUrl.cid && fromUrl.cidt) {\n      const tab = staffTabToken(true);\n      try {\n        sessionStorage.setItem(STAFF_KEY, JSON.stringify(Object.assign({}, fromUrl, { tab })));\n      } catch (e) {}\n      return fromUrl;\n    }\n    // A cid with no token is NOT staff mode - the backend rejects it anyway.\n    if (fromUrl.cid) return fromUrl;\n    try {\n      const held = JSON.parse(sessionStorage.getItem(STAFF_KEY) || 'null');\n      if (held && held.cid) {\n        // Honour it only in the tab that entered staff mode. A copy carried\n        // into another tab has no matching window.name token.\n        if (held.tab && held.tab === staffTabToken(false)) return held;\n        forgetStaffOverride();\n      }\n    } catch (e) {}\n    return NONE;\n  })();\n  const OVERRIDE_CID = _staff.cid;\n  // Signed token that AUTHORISES the ?cid override (staff acting as a customer).\n  // Minted by Odoo (holds the app secret) on the Edit-in-Planner / Join-Session\n  // links. Forwarded on every server call; the backend trusts a cid ONLY when a\n  // valid cidt accompanies it - a bare cid is rejected (closes the cid IDOR).\n  // `let`: a one-time link is exchanged for the working token asynchronously.\n  let OVERRIDE_CIDT = _staff.cidt;\n  // Set when planner-api refuses staff mode because the website account signed\n  // in is not the salesperson's own work account (tagged staff, same email as\n  // their Odoo user - planner-api src/staff-identity.js). The banner then says\n  // which account to sign in with, not \"link expired\".\n  let STAFF_REFUSAL = null;\n  // Every request waits for that exchange, so none goes out with no token (and\n  // so none falls back to the salesperson's own account). Resolves at once\n  // when there is nothing to exchange. A refused link leaves OVERRIDE_CIDT\n  // empty; the project load then finds staff mode not honoured and blocks\n  // saving with the \"expired\" banner, exactly as for an old expired link.\n  const STAFF_READY = !_staff.link ? Promise.resolve() : fetch(`${SERVER_BASE_URL}/staff-link/exchange`, {\n    method: 'POST',\n    credentials: 'same-origin',\n    headers: { 'Content-Type': 'application/json' },\n    body: JSON.stringify({ cid: _staff.cid, token: _staff.link }),\n  }).then(async (r) => {\n    const body = await r.json().catch(() => null);\n    if (!r.ok || !body || !body.token || String(body.cid) !== String(_staff.cid)) {\n      if (body && body.error === 'staff_identity') STAFF_REFUSAL = { workEmail: body.workEmail || '' };\n      console.error('BTKBasketAPI: staff link could not be used',\n        (body && body.error) || r.status, '- open the plan again from Odoo.');\n      // Stop here rather than open an ordinary planner a salesperson could\n      // spend an hour in, believing it was the customer's (Arden, 2026-10-02).\n      // A wrong sign-in keeps the link for the round trip; anything else is\n      // spent or expired, so it goes.\n      if (!STAFF_REFUSAL) { try { sessionStorage.removeItem(STAFF_PENDING_KEY); } catch (e) {} }\n      StaffBlock.show(STAFF_REFUSAL ? 'identity' : 'expired');\n      return;\n    }\n    OVERRIDE_CIDT = body.token;\n    try { sessionStorage.removeItem(STAFF_PENDING_KEY); } catch (e) {}\n    try {\n      const held = Object.assign({}, _staff, { cidt: body.token, tab: staffTabToken(true) });\n      delete held.link;\n      sessionStorage.setItem(STAFF_KEY, JSON.stringify(held));\n    } catch (e) {}\n  }).catch((e) => {\n    console.error('BTKBasketAPI: staff link exchange failed', e);\n    StaffBlock.show('failed');\n  });\n  const STAFF_BACK = safeStaffBack(_staff.back);\n  const STAFF_CUSTOMER_NAME = _staff.name || '';\n  // Staff session started from an Odoo opportunity: its id, stamped on the\n  // baskets made in the session as `opportunityId` (Shapes.newBasket, the\n  // arrival basket, and the planner's unsaved draft in ProxyAdapter._save).\n  // Odoo links the plan to the opportunity from it, and the order importer\n  // credits the order to that opportunity's salesperson. Only a hint: both\n  // check the opportunity is this customer's before using it.\n  const STAFF_OPPORTUNITY = (_staff.cid && /^\\d{1,12}$/.test(_staff.opp || '')) ? Number(_staff.opp) : null;\n  function stampOpportunity(basket) {\n    if (STAFF_OPPORTUNITY && basket && !basket.opportunityId) basket.opportunityId = STAFF_OPPORTUNITY;\n    return basket;\n  }\n  // A basket session (Odoo's \"New Website Basket\" / \"Edit in Basket\") rather\n  // than a planner one. In a basket session the browser's Shopify cart is\n  // used as a WORKING COPY of the customer's basket: the style and unit pages\n  // add to that cart first and copy into the basket, and the cart page\n  // reconciles the basket against it, so a cart still holding the\n  // salesperson's own lines would show - and leak - into the customer's\n  // basket. So on arrival the cart is refilled from the customer's basket,\n  // edits keep it in step, and leaving empties it again.\n  const STAFF_BASKET_SESSION = !!(_staff.cid && _staff.basket);\n  // What the Odoo link asked for, read from THE ADDRESS ONLY - never from the\n  // held session - so it happens once, on arrival (ProxyAdapter._arrive).\n  const STAFF_ARRIVAL = (() => {\n    if (!STAFF_BASKET_SESSION) return null;\n    try {\n      const q = new URLSearchParams(window.location.search);\n      if (!q.get('cid')) return null;\n      if (q.get('newBasket') === '1') {\n        // Urgent / Warning chosen in Odoo's New Website Basket popup (Arden,\n        // 2026-10-27): the new basket is marked with it on arrival.\n        const u = String(q.get('urgency') || '').toLowerCase();\n        return { kind: 'new', name: String(q.get('basketName') || '').trim().slice(0, 80) || 'New Quote',\n                 urgency: (u === 'urgent' || u === 'warning') ? u : null };\n      }\n      if (q.get('basket')) return { kind: 'open', id: String(q.get('basket')) };\n    } catch (e) {}\n    return null;\n  })();\n  // The address to reload to once the arrival is done: the same page with the\n  // staff link's own parameters taken off. Staff mode then comes from the\n  // held session (a cid left in the address with no token would read as a\n  // refused link), and a refresh cannot make a second basket.\n  function addressWithoutStaffParams() {\n    try {\n      const q = new URLSearchParams(window.location.search);\n      ['cid', 'cidt', 'staff', 'staffBack', 'cname', 'newBasket', 'basketName', 'basket', 'opp', 'urgency']\n        .forEach(k => q.delete(k));\n      const s = q.toString();\n      return window.location.pathname + (s ? `?${s}` : '');\n    } catch (e) { return window.location.pathname; }\n  }\n  // Leaving a basket session: the browser's cart was the customer's working\n  // copy, so it is emptied and untagged rather than left in the salesperson's\n  // own checkout. Never stops them leaving.\n  function emptyBrowserCart() {\n    if (!STAFF_BASKET_SESSION) return Promise.resolve();\n    return fetch('/cart/clear.js', { method: 'POST' })\n      .then(() => fetch('/cart/update.js', {\n        method: 'POST',\n        headers: { 'Content-Type': 'application/json' },\n        body: JSON.stringify({ attributes: { _btk_basket_id: '', _btk_basket_name: '', _btk_basket_source: '', _btk_basket_ref: '', _btk_basket_rev: '', _btk_order_note: '' } }),\n      }))\n      .catch(e => console.warn('[BTK staff] could not empty the cart on leaving', e));\n  }\n\n  // The PURCHASED-PLAN VIEWER (/pages/kitchen-planner?ordered=<basket id>) only\n  // looks. It loads the planner, and the planner's load path writes - the\n  // active plan pointer, the styles, the cart - so rather than trust every\n  // caller to remember, nothing here writes on that page: saves stay in memory,\n  // the cart is left alone, and no hold or release is sent. Read once, like\n  // the staff override above.\n  // Every choice the pre-checkout page (btk-checkout-wizard.js) writes onto\n  // the cart. /cart/clear.js empties the LINES only, so an abandoned\n  // checkout's delivery week and \"terms accepted\" used to stay attached to\n  // whatever was put in the cart next. Refilling the cart clears them; the\n  // pre-checkout page always writes fresh ones on the way to payment. Shopify\n  // removes a cart attribute that is set to an empty string.\n  const CLEARED_CHECKOUT_CHOICES = {\n    'BK terms accepted': '', '_btk_terms_ids': '',\n    'Delivery week': '', '_btk_delivery_week_start': '',\n    'Delivery method': '', '_btk_delivery_method': '', '_btk_delivery_method_price': '',\n    '_btk_delivery_method_code': '', '_btk_delivery_postcode': '',\n  };\n\n  // STAFF \"MAKE FREE\" (planner-api src/free-item.js). A line staff made free\n  // carries attributes.freeItem = { code, ... }: a one-use Shopify discount\n  // code for this customer and this item. Shopify prices the cart itself, so\n  // the code has to be ON the cart for the line to be \u00A30 at checkout - put\n  // there after every refill. Codes of ours (FREE-...) that this basket no\n  // longer has come off; any other code on the cart is kept. One extra write,\n  // only when something changed.\n  const FREE_CODE = /^FREE-[A-Z0-9]{4,16}$/;\n  // Whether a line's free offer still stands: not past its end, and - where\n  // the line says what the code was made for (standard basket lines do) - the\n  // same product and quantity. A changed line pays again.\n  function freeItemLive(it) {\n    const fi = it && it.attributes && it.attributes.freeItem;\n    if (!fi || !fi.code) return false;\n    if (fi.endsAt && !(Date.parse(fi.endsAt) > Date.now())) return false;\n    if (fi.variantId && String(fi.variantId) !== String(it.shopifyVariantId)) return false;\n    if (fi.quantity && Number(fi.quantity) !== Number(it.quantity || 1)) return false;\n    return true;\n  }\n  // Free delivery (planner-api makeDeliveryFree) is the basket's, not a\n  // line's: basket.freeDelivery = { code, endsAt }. A FREE- code like the\n  // others, so it goes on and comes off the cart with them.\n  function freeDeliveryLive(basket) {\n    const fd = basket && basket.freeDelivery;\n    if (!fd || !fd.code) return false;\n    return !(fd.endsAt && !(Date.parse(fd.endsAt) > Date.now()));\n  }\n  function applyFreeItemCodes(cart, items, basket) {\n    const want = [];\n    (items || []).forEach(it => {\n      if (!freeItemLive(it)) return;\n      const fi = it.attributes.freeItem;\n      const c = fi && String(fi.code || '').toUpperCase();\n      if (c && FREE_CODE.test(c) && want.indexOf(c) === -1) want.push(c);\n    });\n    if (freeDeliveryLive(basket)) {\n      const c = String(basket.freeDelivery.code).toUpperCase();\n      if (FREE_CODE.test(c) && want.indexOf(c) === -1) want.push(c);\n    }\n    const have = ((cart && cart.discount_codes) || []).map(d => String((d && d.code) || '').toUpperCase()).filter(Boolean);\n    const keep = have.filter(c => !FREE_CODE.test(c));\n    const next = keep.concat(want);\n    const same = next.length === have.length && next.every(c => have.indexOf(c) !== -1);\n    // No cart read back (an old response, a failed write): only write when\n    // there is something to put on.\n    if (same || (!cart && !want.length)) return Promise.resolve(cart);\n    return fetch('/cart/update.js', {\n      method:  'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body:    JSON.stringify({ discount: next.join(',') })\n    }).then(r => (r && r.ok ? r.json().catch(() => null) : null)).then(c2 => {\n      const bad = ((c2 && c2.discount_codes) || []).filter(d => d && FREE_CODE.test(String(d.code || '').toUpperCase()) && d.applicable === false);\n      if (bad.length) console.warn('[BTKBasketAPI] free item code(s) not applicable to this cart:', bad.map(d => d.code).join(', '));\n      return c2 || cart;\n    }).catch(e => { console.warn('[BTKBasketAPI] free item codes not applied:', e && e.message); return cart; });\n  }\n\n  const READ_ONLY_VIEW = (() => {\n    try {\n      return /kitchen-planner/.test(window.location.pathname)\n        && !!(new URLSearchParams(window.location.search).get('ordered') || '').trim();\n    } catch (e) { return false; }\n  })();\n\n  // Shown once per session when a cart sync is skipped in staff mode.\n  let _staffCartNoticeShown = false;\n  // Who we are acting as, as a human label - filled once the project loads.\n  let _actingAsLabel = '';\n\n  // Identifies THIS page's own \"exit\" broadcast. A BroadcastChannel message\n  // reaches every other channel object of that name, including the listener\n  // below in this same page - which then sent the page to the planner's own\n  // address, overriding Back to Odoo's trip to the customer record. Hidden\n  // while Odoo opened a new tab (it closed first); in the same tab the\n  // planner reloaded instead of returning to Odoo (Julian, 2026-10-02).\n  const STAFF_PAGE_ID = Math.random().toString(36).slice(2) + Date.now().toString(36);\n\n  function leaveStaffMode() {\n    forgetStaffOverride();\n    // Tell every other tab. Scoped to this customer, so a salesperson who has\n    // deliberately opened a DIFFERENT customer in another tab keeps that one.\n    try {\n      if (typeof BroadcastChannel !== 'undefined' && OVERRIDE_CID) {\n        const ch = new BroadcastChannel(STAFF_CHANNEL);\n        ch.postMessage({ type: 'exit', cid: OVERRIDE_CID, from: STAFF_PAGE_ID });\n        ch.close();\n      }\n    } catch (e) {}\n  }\n\n  // Work to finish before the salesperson leaves (the planner saves the plan\n  // and ends a live session). Each hook resolves false if it could not.\n  const staffExitHooks = [];\n  const STAFF_EXIT_WAIT_MS = 15000;\n\n  // The exit hooks for a plain Exit Staff Mode: best effort, never blocks.\n  async function runStaffExitHooks() {\n    for (const hook of staffExitHooks) {\n      try {\n        await Promise.race([Promise.resolve().then(hook), new Promise((r) => setTimeout(r, STAFF_EXIT_WAIT_MS))]);\n      } catch (e) { console.warn('[BTK staff] a save before leaving failed', e); }\n    }\n  }\n\n  // \"Back to Odoo\": save, leave staff mode, and return to the Odoo record the\n  // planner was opened from (STAFF_BACK, sent by Odoo).\n  async function backToOdoo(button) {\n    if (button) { button.disabled = true; button.textContent = 'Saving\u2026'; }\n    let ok = true;\n    for (const hook of staffExitHooks) {\n      try {\n        const res = await Promise.race([\n          Promise.resolve().then(hook),\n          new Promise((r) => setTimeout(() => r(false), STAFF_EXIT_WAIT_MS)),\n        ]);\n        if (res === false) ok = false;\n      } catch (e) {\n        console.warn('[BTK staff] a save before leaving failed', e);\n        ok = false;\n      }\n    }\n    if (!ok) {\n      const msg = 'The plan could not be saved just now. Leave anyway? Changes since the last save will be lost.';\n      const leave = window.BTKKPDialog && window.BTKKPDialog.confirm\n        ? await window.BTKKPDialog.confirm(msg, { title: 'Not Saved', danger: true, confirmText: 'Leave Anyway', cancelText: 'Stay' })\n        : window.confirm(msg);\n      if (!leave) {\n        if (button) { button.disabled = false; button.textContent = 'Back to Odoo'; }\n        return;\n      }\n    }\n    await emptyBrowserCart();\n    await StaffHold.release();\n    leaveStaffMode();\n    const goBack = () => {\n      try { window.location.href = STAFF_BACK || window.location.pathname; }\n      catch (e) { window.location.reload(); }\n    };\n    // Odoo now opens the planner in the SAME tab, and STAFF_BACK is the record\n    // it came from, so going there reloads it with the plan just saved\n    // (Arden, 2026-10-02: one new tab per customer lost staff in the cycle).\n    let opener = null;\n    try { opener = window.opener && !window.opener.closed ? window.opener : null; } catch (e) {}\n    if (!opener) { goBack(); return; }\n    // An older Odoo link opened a new tab: tell the Odoo tab behind it to\n    // reload the contact (bk_planner_link static/src/js/planner_refresh.js)\n    // and close this one. The message carries no data and can only cause a\n    // reload, so any opener may receive it.\n    try { opener.postMessage({ type: 'btk-planner-back' }, '*'); } catch (e) {}\n    try { window.close(); } catch (e) {}\n    // Still here: the browser kept the tab open.\n    setTimeout(goBack, 300);\n  }\n\n  // Another tab exited staff mode for the customer this tab is acting as:\n  // follow it, rather than carrying on as the customer unseen.\n  try {\n    if (typeof BroadcastChannel !== 'undefined' && OVERRIDE_CID) {\n      new BroadcastChannel(STAFF_CHANNEL).onmessage = (ev) => {\n        const m = ev && ev.data;\n        if (m && m.from === STAFF_PAGE_ID) return;   // our own exit: Back to Odoo is navigating\n        if (m && m.type === 'exit' && String(m.cid) === String(OVERRIDE_CID)) {\n          forgetStaffOverride();\n          try { window.location.href = window.location.pathname; }\n          catch (e) { window.location.reload(); }\n        }\n      };\n    }\n  } catch (e) {}\n  // A staff link the planner API refused. Covers the whole page, so the\n  // salesperson cannot carry on in a planner that is NOT the customer's:\n  //   identity - the website is signed out, or signed in as someone other than\n  //              their staff-tagged work account. Sign in (or out first), and\n  //              the same link is used on the way back - a refusal does not use\n  //              it up, and it is held in this tab for its 10 minutes.\n  //   expired  - the link was used already or ran out: open it again in Odoo.\n  //   failed   - the check itself could not be made.\n  const StaffBlock = {\n    el: null,\n    show(kind) {\n      const paint = () => {\n        if (!document.body || this.el) return;\n        const workEmail = (STAFF_REFUSAL && STAFF_REFUSAL.workEmail) || '';\n        const who = workEmail || 'your work email';\n        const signedIn = !!(window.BTKCustomer && window.BTKCustomer.id);\n        const here = window.location.pathname + window.location.search;\n        const back = safeStaffBack(_staff.back);\n        let title;\n        let text;\n        let primary = null;\n        if (kind === 'identity' && !signedIn) {\n          title = 'Sign In to Use Staff Mode';\n          text = `To work on ${_staff.name || 'this customer'}'s plan, sign in on the website with your work account, ${who}. You will come straight back here.`;\n          const links = window.BTKAccountLinks;\n          const url = links && typeof links.signInUrl === 'function'\n            ? links.signInUrl(here)\n            : '/account/login?return_url=' + encodeURIComponent(here);\n          primary = { label: workEmail ? `Sign In as ${workEmail}` : 'Sign In', go: url };\n        } else if (kind === 'identity') {\n          title = 'Wrong Website Account';\n          text = `You are signed in on the website with a different account. Sign out, then sign in as ${who} to carry on.`;\n          const out = (window.BTKAccountLinks && window.BTKAccountLinks.logoutUrl) || '/account/logout';\n          const enc = encodeURIComponent(here);\n          primary = { label: 'Sign Out', go: `${out}${out.indexOf('?') === -1 ? '?' : '&'}return_to=${enc}&return_url=${enc}` };\n        } else if (kind === 'expired') {\n          title = 'This Link Has Expired';\n          text = 'Staff links work once, for 10 minutes. Go back to Odoo and open the plan again.';\n        } else {\n          title = 'Staff Mode Could Not Start';\n          text = 'Your staff access could not be checked just now. Go back to Odoo and try again.';\n        }\n        const el = document.createElement('div');\n        el.id = 'btk-staff-block';\n        el.setAttribute('role', 'alertdialog');\n        el.setAttribute('aria-modal', 'true');\n        el.style.cssText = 'position:fixed;inset:0;z-index:2147483000;display:flex;align-items:center;'\n          + 'justify-content:center;padding:16px;background:rgba(0,0,0,.6);'\n          + 'font:400 16px/1.5 system-ui,-apple-system,Segoe UI,sans-serif;color:#222;';\n        const card = document.createElement('div');\n        card.style.cssText = 'background:#fff;border-radius:8px;max-width:480px;width:100%;'\n          + 'padding:28px 28px 24px;box-shadow:0 10px 40px rgba(0,0,0,.3);';\n        const h = document.createElement('h2');\n        h.textContent = title;\n        h.setAttribute('aria-live', 'polite');\n        h.style.cssText = 'margin:0 0 12px;font-family:inherit;font-size:22px;font-weight:700;line-height:1.25;color:#111;';\n        const p = document.createElement('p');\n        p.textContent = text;\n        p.style.cssText = 'margin:0 0 22px;';\n        const row = document.createElement('div');\n        row.style.cssText = 'display:flex;flex-wrap:wrap;gap:10px;';\n        const button = (label, fn, green) => {\n          const b = document.createElement('button');\n          b.type = 'button';\n          b.textContent = label;\n          b.style.cssText = 'border-radius:20px;padding:10px 20px;font-family:inherit;font-size:15px;font-weight:600;line-height:1.2;cursor:pointer;'\n            + (green ? 'background:#2d7a3a;color:#fff;border:1px solid #2d7a3a;'\n              : 'background:#fff;color:#222;border:1px solid #bbb;');\n          b.addEventListener('click', fn);\n          row.appendChild(b);\n          return b;\n        };\n        if (primary) button(primary.label, () => { window.location.href = primary.go; }, true);\n        button('Back to Odoo', () => {\n          forgetStaffOverride();\n          window.location.href = back || '/';\n        }, !primary);\n        card.appendChild(h);\n        card.appendChild(p);\n        card.appendChild(row);\n        el.appendChild(card);\n        document.body.appendChild(el);\n        this.el = el;\n        // Nothing behind it may be used: no scrolling, no keyboard focus.\n        try { document.documentElement.style.overflow = 'hidden'; } catch (e) {}\n        const first = row.querySelector('button');\n        if (first) try { first.focus(); } catch (e) {}\n      };\n      if (document.body) paint();\n      else document.addEventListener('DOMContentLoaded', paint, { once: true });\n    },\n  };\n\n  // The \"acting as\" banner. Now that staff mode survives navigation it MUST be\n  // visible on every page: an impersonation you cannot see is exactly how a\n  // plan ends up saved against the wrong person.\n  const StaffBanner = {\n    el: null,\n    // Extra buttons other parts of the storefront can put in the bar, so a\n    // salesperson has ONE strip of controls rather than a second panel floating\n    // over the planner's own menu. Held here and re-added on every repaint,\n    // because the bar rebuilds its contents from scratch each time.\n    actions: [],\n    lastState: null,\n    lastLabel: '',\n    setActions(list) {\n      this.actions = Array.isArray(list) ? list.slice(0, 4) : [];\n      if (this.el && this.lastState) this.show(this.lastState, this.lastLabel);\n    },\n    show(state, label) {\n      if (!OVERRIDE_CID) return;\n      this.lastState = state;\n      this.lastLabel = label;\n      const paint = () => {\n        if (!document.body) return;\n        if (!this.el) {\n          this.el = document.createElement('div');\n          this.el.id = 'btk-staff-banner';\n          this.el.style.cssText = 'position:sticky;top:0;z-index:99999;display:flex;'\n            + 'gap:14px;align-items:center;justify-content:center;padding:8px 14px;'\n            + 'font:600 14px/1.3 system-ui,-apple-system,Segoe UI,sans-serif;color:#fff;';\n          document.body.insertBefore(this.el, document.body.firstChild);\n        }\n        const live = state === 'active';\n        // Light purple for a basket session, so staff can tell it from the\n        // green planner one at a glance. Dark text: white is unreadable on it.\n        const purple = live && STAFF_BASKET_SESSION;\n        this.el.style.background = purple ? '#e4d7f5' : live ? '#2d7a3a' : '#a00';\n        this.el.style.color = purple ? '#3b2566' : '#fff';\n        const text = live\n          ? (purple ? `Staff mode, website basket - acting as ${label || 'customer'}`\n                    : `Staff mode - acting as ${label || 'customer'}`)\n          : STAFF_REFUSAL\n            ? `Staff mode needs your work account. Sign in on the website as ${STAFF_REFUSAL.workEmail || 'your work email'} `\n              + '(tagged staff), then open the plan from Odoo again. Nothing will be saved to the customer.'\n            : 'Staff link expired - you are NOT acting as the customer. Nothing will be saved to their account.';\n        this.el.textContent = '';\n        const span = document.createElement('span');\n        span.textContent = text;\n        this.el.appendChild(span);\n        const btn = (txt, fn) => {\n          const b = document.createElement('button');\n          b.type = 'button';\n          b.textContent = txt;\n          b.style.cssText = purple\n            ? 'background:#fff;border:1px solid #9b7fd1;color:#3b2566;'\n              + 'border-radius:4px;padding:3px 10px;font:inherit;cursor:pointer;'\n            : 'background:rgba(255,255,255,.18);border:1px solid rgba(255,255,255,.5);'\n              + 'color:#fff;border-radius:4px;padding:3px 10px;font:inherit;cursor:pointer;';\n          b.addEventListener('click', fn);\n          this.el.appendChild(b);\n          return b;\n        };\n        // Saves, leaves staff mode and returns to the customer record in Odoo.\n        // It is also how staff change customer: only Odoo holds the app\n        // secret, so only Odoo can mint the token for a different person (a\n        // storefront-side switch would let one leaked token act as ANY\n        // customer). The separate \"Switch customer\" button went (Arden,\n        // 2026-10-01): it left without saving and without leaving staff mode.\n        const back = btn('Back to Odoo', () => backToOdoo(back));\n        // Registered actions last, so they read as \"this session\" rather than\n        // part of the standing staff controls.\n        (this.actions || []).forEach((a) => {\n          if (!a || !a.label || typeof a.onClick !== 'function') return;\n          const b = btn(a.label, a.onClick);\n          if (a.danger) {\n            b.style.background = 'rgba(255,255,255,.95)';\n            b.style.color = '#a00';\n            b.style.borderColor = 'rgba(255,255,255,.95)';\n          }\n        });\n      };\n      if (document.body) paint();\n      else document.addEventListener('DOMContentLoaded', paint, { once: true });\n    }\n  };\n\n  function effectiveCustomerId() {\n    if (OVERRIDE_CID) return String(OVERRIDE_CID);\n    return (window.BTKCustomer && window.BTKCustomer.id) ? String(window.BTKCustomer.id) : '';\n  }\n\n  // An image from planner-api, as a Blob, or null when there is none. Same\n  // cid/cidt query as ProxyAdapter._httpNow, which only speaks JSON.\n  function lifestyleBlob(path) {\n    return STAFF_READY.then(() => {\n      const cid = effectiveCustomerId();\n      let url = `${SERVER_BASE_URL}${path}${cid ? `${path.indexOf('?') === -1 ? '?' : '&'}cid=${encodeURIComponent(cid)}` : ''}`;\n      if (OVERRIDE_CIDT) url += `&cidt=${encodeURIComponent(OVERRIDE_CIDT)}`;\n      return fetch(url, { credentials: 'same-origin' })\n        .then((r) => (r.ok ? r.blob() : null))\n        .catch(() => null);\n    });\n  }\n\n  // \u2500\u2500\u2500 UTILITIES \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n  const Utils = {\n\n    generateId(prefix = 'id') {\n      return `${prefix}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;\n    },\n\n    now() {\n      return new Date().toISOString();\n    },\n\n    getOrCreateGuestId() {\n      const match = document.cookie.match(new RegExp(`(^| )${GUEST_COOKIE_KEY}=([^;]+)`));\n      if (match) return match[2];\n      const newId   = Utils.generateId('guest');\n      const expires = new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toUTCString();\n      document.cookie = `${GUEST_COOKIE_KEY}=${newId}; expires=${expires}; path=/; SameSite=Lax`;\n      return newId;\n    },\n\n    /**\n     * Returns a stable customer key.\n     * Logged-in: 'shopify_12345678'\n     * Guest:     'guest_xxxxx' (cookie-based, persists across browser closes)\n     */\n    getCustomerId() {\n      const cid = effectiveCustomerId();\n      return cid ? `shopify_${cid}` : Utils.getOrCreateGuestId();\n    },\n\n    /**\n     * Returns the localStorage key for the current customer.\n     * Each customer gets completely separate storage.\n     */\n    getStorageKey() {\n      return `${STORAGE_PREFIX}${Utils.getCustomerId()}`;\n    },\n\n    isLoggedIn() {\n      return !!effectiveCustomerId();\n    },\n\n    deepClone(obj) {\n      return JSON.parse(JSON.stringify(obj));\n    },\n\n    // A product sold in ONE variant (panels, trims, appliance doors) has no\n    // options, so Shopify titles that variant with its fixed placeholder\n    // \"Default Title\". Baskets print variantTitle as the line's size, so the\n    // placeholder read as a size. It is Shopify's sentinel for \"no options\",\n    // not a product name, so it is blanked here as a project loads - which\n    // also cleans lines saved before the feeds stopped sending it.\n    realVariantTitle(title) {\n      return title === 'Default Title' ? '' : title;\n    },\n\n    dropPlaceholderVariantTitles(project) {\n      const clean = items => (items || []).forEach(it => {\n        if (it && it.variantTitle != null) it.variantTitle = Utils.realVariantTitle(it.variantTitle);\n      });\n      ((project && project.baskets) || []).forEach(b => {\n        clean(b.items);\n        (b.sections || []).forEach(s => clean(s.items));\n      });\n      return project;\n    },\n\n    // Basket and item ids are printed into data-attributes all over the basket\n    // page, and staff open customers' projects. Real ids are generated here and\n    // never contain markup characters, so one that does is replaced, and a\n    // quantity that is not a number becomes 1.\n    defangIds(project) {\n      const bad = v => v != null && /[<>\"'`&]/.test(String(v));\n      const items = list => (list || []).forEach(it => {\n        if (!it) return;\n        if (bad(it.id)) it.id = Utils.generateId('item');\n        if (it.quantity != null && !Number.isFinite(Number(it.quantity))) it.quantity = 1;\n      });\n      ((project && project.baskets) || []).forEach(b => {\n        if (!b) return;\n        if (bad(b.id)) b.id = Utils.generateId('basket');\n        items(b.items);\n        (b.sections || []).forEach(s => items(s && s.items));\n      });\n      return project;\n    },\n\n    cleanLoadedProject(project) {\n      return Utils.defangIds(Utils.dropPlaceholderVariantTitles(project));\n    }\n  };\n\n  // \u2500\u2500\u2500 DATA SHAPES \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n  const Shapes = {\n\n    newProject(customerId) {\n      const projectId = Utils.generateId('proj');\n      const basketId  = Utils.generateId('basket');\n      return {\n        customerId,\n        id:        projectId,\n        name:      'My Kitchen Project',\n        createdAt: Utils.now(),\n        updatedAt: Utils.now(),\n        // The planner's \"current working plan\" pointer. Decoupled from\n        // isActive (which drives Shopify-cart sync) so that merely\n        // designing in the planner never disturbs the customer's\n        // checkout-active basket - only an explicit \"Add to basket /\n        // transfer\" sets isActive. See createBasketPlanStore().\n        activePlannerBasketId: null,\n        baskets: [\n          Shapes.newBasket(basketId, 'Unsaved Quote', true)\n        ]\n      };\n    },\n\n    // source:        'manual' (PDP / traditional) | 'planner' (3D planner)\n    // plannerDesign: the planner's serialised payload {schemaVersion,\n    //                savedAt, design}, or null for manual baskets.\n    // isDraft:       true for the auto-created unnamed working plan; set\n    //                false once the customer names it via Save As.\n    newBasket(id, name = 'New Kitchen', isActive = false, source = 'manual') {\n      return stampOpportunity({\n        id:             id || Utils.generateId('basket'),\n        name,\n        isActive,\n        source,\n        plannerDesign:  null,\n        isDraft:        false,\n        range:          null,\n        sections:       [],\n        items:          [],\n        checklistState: {},\n        // Set to the planner basket's id once this manual basket has been\n        // converted to a 3D plan - marks it so it can't be re-converted.\n        convertedPlannerId: null,\n        createdAt:      Utils.now(),\n        updatedAt:      Utils.now()\n      });\n    },\n\n    newSection(name) {\n      return {\n        id:    Utils.generateId('section'),\n        name,\n        items: []\n      };\n    },\n\n    newItem(shopifyVariantId, productTitle, variantTitle, quantity, price) {\n      return {\n        id:              Utils.generateId('item'),\n        lineNumber:      null,\n        shopifyVariantId,\n        productTitle,\n        // Every line is made here (planner, listing, cart sync, Extras), so\n        // the placeholder is stopped at the one door - see realVariantTitle.\n        variantTitle:    Utils.realVariantTitle(variantTitle),\n        quantity,\n        price,\n        productType:     null,\n        attributes: {\n          doorStyle:     null,\n          doorColour:    null,\n          carcaseColour: null,\n          hinge:         null,\n          // True Handleless handed tall units: `handed` marks the line as a\n          // handed unit; `hand` carries the L/R (user-perspective) so the Odoo\n          // import resolves the correct LH/RH product. Stamped even when there's\n          // no selectable hinge (flap-&-drawer housings).\n          handed:        null,\n          hand:          null,\n          // Visible customer sign-off value: the GRIP RAIL side (opposite the\n          // hinge). Shown on the basket line so the customer confirms it.\n          rail:          null,\n          assembly:      null,\n          altUnit:       null,\n          notes:         null,\n          // A note sales staff add in staff mode, for our team (warehouse /\n          // production): never shown to the customer. The order import files\n          // it, with the planner's fixedNote, as the line's Item Note.\n          staffNote:     null,\n          sampleColours: null,\n          sampleStyle:   null,\n          bespokeColour: null,\n          // Which group this line totals into in the embedded planner basket's\n          // Pricing bar: units | panels | trims | handles | worktops |\n          // appliances | other. Stamped by the planner when it transfers a plan\n          // to the basket; left null on manual /cart lines, which the basket\n          // classifies from the line markers + section name instead. Never\n          // pushed to the Shopify cart (properties are enumerated explicitly).\n          quoteGroup:    null\n        },\n        addedAt: Utils.now()\n      };\n    }\n  };\n\n  // \u2500\u2500\u2500 SESSION ADAPTER \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n  const SessionAdapter = {\n\n    /**\n     * Read project from localStorage using customer-specific key.\n     * Logged-in customers get their own key, guests get their own key.\n     * Switching from guest to logged-in will start a fresh project for that customer\n     * (guest-to-login merge can be added when Render server is ready).\n     */\n    _read() {\n      try {\n        const raw = localStorage.getItem(Utils.getStorageKey());\n        return raw ? Utils.cleanLoadedProject(JSON.parse(raw)) : null;\n      } catch(e) {\n        console.warn('BTKBasketAPI: localStorage read failed', e);\n        return null;\n      }\n    },\n\n    _write(project) {\n      if (READ_ONLY_VIEW) return project;   // see READ_ONLY_VIEW\n      try {\n        project.updatedAt = Utils.now();\n        localStorage.setItem(Utils.getStorageKey(), JSON.stringify(project));\n        return project;\n      } catch(e) {\n        console.warn('BTKBasketAPI: localStorage write failed', e);\n        return null;\n      }\n    },\n\n    _getOrCreateProject() {\n      const existing = this._read();\n      if (existing) return existing;\n      const fresh = Shapes.newProject(Utils.getCustomerId());\n      this._write(fresh);\n      return fresh;\n    },\n\n    _getActiveBasket(project) {\n      return project.baskets.find(b => b.isActive) || project.baskets[0] || null;\n    },\n\n    // \u2500\u2500 Public methods \u2500\u2500\n\n    getProject() {\n      return Promise.resolve(this._getOrCreateProject());\n    },\n\n    getBaskets() {\n      const project = this._getOrCreateProject();\n      return Promise.resolve(project.baskets);\n    },\n\n    getActiveBasket() {\n      const project = this._getOrCreateProject();\n      return Promise.resolve(this._getActiveBasket(project));\n    },\n\n    // The active MANUAL basket - used by /cart + the cart drawer, which\n    // must never show planner baskets. If the active basket is a planner\n    // one (or there's no manual basket), pick/create a manual basket and\n    // activate it. The planner keeps its own baskets via activePlannerBasketId.\n    getActiveManualBasket() {\n      const project = this._getOrCreateProject();\n      const active  = project.baskets.find(b => b.isActive);\n      if (active && active.source !== 'planner') return Promise.resolve(active);\n      let manual = project.baskets.find(b => b.source !== 'planner');\n      if (!manual) {\n        manual = Shapes.newBasket(Utils.generateId('basket'), 'My Quote', false, 'manual');\n        project.baskets.push(manual);\n      }\n      project.baskets.forEach(b => { b.isActive = (b.id === manual.id); });\n      this._write(project);\n      return Promise.resolve(manual);\n    },\n\n    saveBasket(basketData) {\n      const project = this._getOrCreateProject();\n      const index   = project.baskets.findIndex(b => b.id === basketData.id);\n      basketData.updatedAt = Utils.now();\n      if (index > -1) {\n        project.baskets[index] = basketData;\n      } else {\n        project.baskets.push(basketData);\n      }\n      this._write(project);\n      return Promise.resolve(basketData);\n    },\n\n    duplicateBasket(basketId) {\n      const project = this._getOrCreateProject();\n      const source  = project.baskets.find(b => b.id === basketId);\n      if (!source) return Promise.reject(new Error(`Basket ${basketId} not found`));\n      const copy     = Utils.deepClone(source);\n      copy.id        = Utils.generateId('basket');\n      delete copy.ref;                 // numbers are the server's (basket-refs)\n      copy.name      = `${source.name} (Copy)`;\n      copy.isActive  = false;\n      copy.createdAt = Utils.now();\n      copy.updatedAt = Utils.now();\n      project.baskets.push(copy);\n      this._write(project);\n      return Promise.resolve(copy);\n    },\n\n    createBasket(name, source = 'manual') {\n      const project = this._getOrCreateProject();\n      const basket  = Shapes.newBasket(Utils.generateId('basket'), name, false, source);\n      project.baskets.push(basket);\n      this._write(project);\n      return Promise.resolve(basket);\n    },\n\n    getActivePlannerBasketId() {\n      const project = this._getOrCreateProject();\n      return Promise.resolve(project.activePlannerBasketId || null);\n    },\n\n    // The basket the PLANNER is working in, resolved by id.\n    //\n    // There are two independent notions of \"current basket\" and they do not\n    // agree: `isActive` (what getActiveBasket reads) and `activePlannerBasketId`\n    // (where the planner actually puts units). createBasket pushes new baskets\n    // with isActive false and never activates them, while getActiveManualBasket\n    // forces isActive onto a MANUAL basket - so a planner basket is essentially\n    // never the isActive one. The in-planner basket panel used getActiveBasket\n    // and so rendered an empty manual quote while the units sat safely in the\n    // planner basket.\n    //\n    // Deliberately does NOT call setActiveBasket: that would flip isActive onto\n    // the planner basket and break /cart, which must never show planner\n    // baskets. This is a read, not a change of state.\n    getActivePlannerBasket() {\n      const project = this._getOrCreateProject();\n      const id = project.activePlannerBasketId;\n      const byId = id ? project.baskets.find(b => b.id === id) : null;\n      // Fall back to the newest planner basket, then to the generic active one,\n      // so a project saved before activePlannerBasketId existed still resolves.\n      const planner = byId\n        || project.baskets.filter(b => b.source === 'planner').slice(-1)[0]\n        || null;\n      return Promise.resolve(planner || this._getActiveBasket(project));\n    },\n\n    setActivePlannerBasketId(basketId) {\n      const project = this._getOrCreateProject();\n      project.activePlannerBasketId = basketId || null;\n      this._write(project);\n      return Promise.resolve(project.activePlannerBasketId);\n    },\n\n    deleteBasket(basketId) {\n      const project = this._getOrCreateProject();\n      if (project.baskets.length === 1) {\n        return Promise.reject(new Error('Cannot delete the only basket in a project'));\n      }\n      const wasActive  = project.baskets.find(b => b.id === basketId)?.isActive;\n      project.baskets  = project.baskets.filter(b => b.id !== basketId);\n      if (wasActive && project.baskets.length > 0) {\n        project.baskets[0].isActive = true;\n      }\n      this._write(project);\n      return Promise.resolve({ deleted: basketId });\n    },\n\n    setActiveBasket(basketId) {\n      const project = this._getOrCreateProject();\n      project.baskets.forEach(b => { b.isActive = (b.id === basketId); });\n      const active = project.baskets.find(b => b.id === basketId);\n      if (!active) return Promise.reject(new Error(`Basket ${basketId} not found`));\n      this._write(project);\n      return Promise.resolve(active);\n    },\n\n    // FILL THE SHOPIFY CART from a basket.\n    //\n    // `basketId` is optional and should be given wherever the caller knows\n    // which basket it means. Without it this falls back to the `isActive`\n    // basket, which forces the caller to SET that flag first - and setting it\n    // costs a whole-project PUT (~500 kB) to move one boolean: 5.1s of a 17.9s\n    // checkout (Arden, 2026-09-28). It is also the wrong flag to touch, because\n    // `isActive` is /cart's notion of the current basket and a planner basket\n    // must never be it (see the note on getActivePlannerBasket).\n    syncToShopifyCart(basketId) {\n      // ACTING AS A CUSTOMER? Then stop here. The Shopify cart belongs to the\n      // BROWSER SESSION, which is the salesperson's - so pushing lines into it\n      // would file the customer's kitchen in the staff member's own checkout,\n      // where the customer never sees it and the salesperson is left holding\n      // someone else's order. The plan is already saved to the customer's\n      // basket server-side; they check out from their own account.\n      //\n      // This resolves rather than rejects so the eight or so callers' promise\n      // chains carry on - but it is NEVER silent: a save that quietly did\n      // nothing is the failure mode this whole area keeps producing.\n      // A basket session is the exception: there the cart IS the customer's\n      // working copy (see STAFF_BASKET_SESSION), so it is kept in step.\n      if (OVERRIDE_CID && !STAFF_BASKET_SESSION) {\n        const msg = 'You are working on a customer\u2019s plan. It has been saved to '\n          + 'their basket, but the items have NOT been added to a shopping cart - '\n          + 'the cart in this browser is yours, not theirs. They complete checkout '\n          + 'from their own account.';\n        console.warn('[BTKBasketAPI] staff mode: skipped Shopify cart sync -', msg);\n        try { window.dispatchEvent(new CustomEvent('btk:staff-cart-skipped')); } catch (_) {}\n        // Once per session: these callers fire on every quantity tweak, and a\n        // dialog on each one would train people to dismiss it unread.\n        if (!_staffCartNoticeShown) {\n          _staffCartNoticeShown = true;\n          if (window.BTKKPDialog && typeof window.BTKKPDialog.alert === 'function') {\n            window.BTKKPDialog.alert(msg, { title: 'Working on a customer\u2019s plan' });\n          }\n        }\n        return Promise.resolve({ skipped: 'staff-mode' });\n      }\n      // An id that cannot be found is REJECTED, never quietly swapped for the\n      // active basket: pushing the wrong basket into the cart is what the\n      // customer would be charged for.\n      return (basketId\n        ? this.getBaskets().then(bs => (bs || []).find(b => b.id === basketId) || null)\n        : this.getActiveBasket()\n      ).then(basket => {\n        if (!basket) {\n          return Promise.reject(new Error(basketId\n            ? 'Basket ' + basketId + ' not found'\n            : 'No active basket'));\n        }\n\n        const allItems = [];\n        let lineNumber = 1;\n\n        basket.sections.forEach(section => {\n          section.items.forEach(item => {\n            allItems.push({ ...item, lineNumber, sectionName: section.name });\n            lineNumber++;\n          });\n        });\n        basket.items.forEach(item => {\n          allItems.push({ ...item, lineNumber, sectionName: '' });\n          lineNumber++;\n        });\n\n        // \u2500\u2500 Checkout-visible labels \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n        // Shopify hides any property whose name starts with '_', which is why\n        // the checkout order summary showed no item number and no colours. The\n        // fix is ADDITIVE: the '_' properties stay exactly as they are (the\n        // cart thumbnails in snippets/btk-cart-item.liquid and the Odoo import\n        // both read them by those names), and we add display-only twins.\n        //\n        // The '_' values are HANDLES ('antique-rose'), so they cannot be shown\n        // as-is - that would put a slug on the customer's payment page. Resolve\n        // the real name from the colour data the basket pages already load, and\n        // if that is not on this page fall back to title-casing the handle,\n        // which never shows a slug even when the lookup misses.\n        const _prettyHandle = (h) => String(h || '')\n          .replace(/[-_]+/g, ' ')\n          .replace(/\\b\\w/g, (c) => c.toUpperCase())\n          .trim();\n        const _colourName = (handle, type) => {\n          if (!handle) return '';\n          try {\n            if (type === 'door' && Array.isArray(window.BTKStyles)) {\n              for (const s of window.BTKStyles) {\n                const cols = s && s.doorColourGroup && s.doorColourGroup.colours;\n                if (!Array.isArray(cols)) continue;\n                const f = cols.find((c) => c.handle === handle)\n                  || cols.find((c) => c.name && c.name.toLowerCase() === String(handle).toLowerCase());\n                if (f && f.name) return f.name;\n              }\n            }\n            if (type === 'carcase' && Array.isArray(window.BTKCarcaseColours)) {\n              const f = window.BTKCarcaseColours.find((c) => c.handle === handle)\n                || window.BTKCarcaseColours.find((c) => c.name && c.name.toLowerCase() === String(handle).toLowerCase());\n              if (f && f.name) return f.name;\n            }\n          } catch (e) { /* colour data absent on this page - fall through */ }\n          return _prettyHandle(handle);\n        };\n\n        // A product with no carcase (a door, drawer front, trim - capability flag\n        // `carcase` false, or a planner part in the door colour) goes to the cart\n        // with no carcase colour: it showed at checkout and was copied into the\n        // Odoo order (Shopify/Planner Issues row 50, 2026-10-30).\n        const _carcaseOf = (item) => {\n          const a = item.attributes || {};\n          if (a.extraColour === 'door') return '';\n          const caps = (window.BTKProductCapabilities || {})[item.shopifyVariantId];\n          return (caps && caps.carcase === false) ? '' : (a.carcaseColour || '');\n        };\n\n        const shopifyItems = allItems\n          .filter(item => !item.isChecklistItem)\n          .map(item => ({\n          id:       item.shopifyVariantId,\n          quantity: item.quantity,\n          properties: {\n  _btk_item_id:          item.id,\n  _btk_line_id:          item.id,\n  // The number the customer sees - on the basket card and as the badge on the\n  // plan - is planLineNumber, falling back to the basket's own 1..n. The Odoo\n  // import orders the sale order lines on this, so it has to be the DISPLAYED\n  // number: sending item.lineNumber put Odoo in the planner's internal\n  // section-build order (Handles, walls, Worktops, Trims, Extras) instead of\n  // the plan order.\n  _btk_line_number:      (item.planLineNumber != null ? item.planLineNumber : item.lineNumber),\n            // Display-only twins. Shopify renders visible properties in the\n            // order they are declared, so these lead: Item, then the colours,\n            // then the handing/assembly below. Omitted when empty (|| undefined)\n            // so accessories and sample packs get no blank rows.\n            'Item':                (item.planLineNumber != null ? item.planLineNumber : item.lineNumber) || undefined,\n            'Door Colour':         _colourName(item.attributes.doorColour, 'door')       || undefined,\n            'Carcase Colour':      _colourName(_carcaseOf(item), 'carcase') || undefined,\n            _btk_section:          item.sectionName              || '',\n            _btk_door_style:       item.attributes.doorStyle     || '',\n            // The unit's RANGE. Hidden; the basket page reads it back when it\n            // has to rebuild a line from the cart, which otherwise lost it.\n            _btk_style:            item.attributes.style         || '',\n            _btk_door_colour:      item.attributes.doorColour    || '',\n            _btk_carcase:          _carcaseOf(item),\n            // Hinge handing + assembly choice - needed by Odoo's \"Add Subitems\"\n            // explosion. Written as displayed properties (matches the advanced\n            // basket); omitted when unset so we never push an empty value.\n            'Hinge':               item.attributes.hinge         || undefined,\n            // True Handleless handing - hidden props read by the Odoo import to\n            // pick the correct LH/RH product. `_btk_hand` covers the flap-&-drawer\n            // case where there's no visible Hinge value.\n            '_btk_handed':         item.attributes.handed        || undefined,\n            '_btk_hand':           item.attributes.hand          || undefined,\n            // Visible - customer signs off the grip rail side (opposite hinge).\n            'Rail':                item.attributes.rail          || undefined,\n            'Assembly':            item.attributes.assembly      || undefined,\n            _btk_alt_unit:         item.attributes.altUnit       || '',\n            // Added from the free-gift picker (btk-cart-advanced.js): the basket\n            // takes this line out again if the units drop under the threshold,\n            // and never touches a tap the customer added themselves.\n            _btk_free_gift:        item.attributes.freeGift ? '1' : undefined,\n            // The line's notes, kept apart so the order import files each\n            // where it belongs (planner-api applyLineNotes): the planner's\n            // fixed note (e.g. \"Add Rail to both sides\") and a staff note ->\n            // the line's Item Note, for the warehouse; the customer's own ->\n            // their private note.\n            _btk_fixed_note:       item.attributes.fixedNote     || undefined,\n            _btk_staff_note:       item.attributes.staffNote     || undefined,\n            // Staff Send Direct: the order import puts the line on Odoo's Drop\n            // Shipping route (planner-api applySendDirect).\n            _btk_send_direct:      item.attributes.sendDirect ? '1' : undefined,\n            // A manager's goodwill discount on this line (planner-api\n            // src/gwg.js): its key, which the checkout's gwg-discount function\n            // checks against the customer's own before taking anything off.\n            _btk_gwg:              (item.attributes.gwg && item.attributes.gwg.key) || undefined,\n            _btk_customer_note:    item.attributes.notes         || undefined,\n            // Made-to-measure worktop specification, one item per line. HIDDEN:\n            // it is for the factory and the supplier's PO, and a long list under\n            // the line would undo the checkout tidy-up. The Odoo import drops it\n            // on the sale order line's item_note.\n            '_btk_worktop_spec':   item.attributes.worktopSpec   || undefined,\n            _btk_bespoke_colour:   item.attributes.bespokeColour\n                                     ? JSON.stringify(item.attributes.bespokeColour)\n                                     : '',\n            '_Door Colours':       item.attributes.doorColours    || '',\n            '_Cabinet Colours':    item.attributes.cabinetColours || '',\n            '_Style':              item.attributes.sampleStyle    || '',\n            // Order Extras: the signed link to the order these add to. On a\n            // LINE because checkout's delivery rates only ever see lines\n            // (planner-api src/order-extras.js).\n            '_btk_extras':         (basket.extras && basket.extras.token) || undefined\n          }\n        }));\n\n        // \u2500\u2500 Aggregate addon lines \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst addonMap = {};\nallItems.forEach(function(item) {\n  const addons = (item.attributes && item.attributes.addons) ? item.attributes.addons : [];\n  addons.forEach(function(addon) {\n    if (!addon.handle || !addon.variantId) return;\n    // One line per add-on PRODUCT: one handle can cover several products (the\n    // glass drawerside upgrade is a 1 pair pack on one-pan-drawer units and a\n    // 2 pairs pack on two-pan-drawer units), and keying on the handle alone\n    // charged them all as whichever came first (Arden, 2026-10-11). The line\n    // still carries the handle in _btk_addon.\n    const key = addon.handle + '|' + addon.variantId;\n    if (!addonMap[key]) {\n      addonMap[key] = {\n        handle:      addon.handle,\n        variantId:   addon.variantId,\n        displayName: addon.displayName || addon.handle,\n        totalQty:    0,\n        unitTitles:  []\n      };\n    }\n    addonMap[key].totalQty  += (addon.qtyPerUnit || 1) * (item.quantity || 1);\n    addonMap[key].unitTitles.push(item.productTitle || 'Unit');\n  });\n});\n\n// The basket sections are built per WALL, so adding the lines in that order put\n// the cart page, the checkout summary and the Odoo sale order in the planner's\n// internal build order rather than the order the customer reads. Sort on the\n// number shown on the card and on the plan; anything unnumbered or tied keeps\n// its basket order. The Odoo import sorts on the same number, so a reshuffle\n// between cart and order cannot undo this.\nconst _btkLineNum = (it) => {\n  // Number('') is 0, not NaN, so a blank number would sort to the FRONT of the\n  // basket rather than the back. Rule it out before converting.\n  const raw = it.properties._btk_line_number;\n  const v = (raw == null || String(raw).trim() === '') ? NaN : Number(raw);\n  return isNaN(v) ? Number.MAX_SAFE_INTEGER : v;\n};\nconst orderedItems = shopifyItems\n  .map((it, i) => ({ it, i }))\n  .sort((a, b) => (_btkLineNum(a.it) - _btkLineNum(b.it)) || (a.i - b.i))\n  .map((e) => e.it);\n\n// Addons are their own Shopify lines but carry no basket number of their own,\n// so they continue from the highest number already sent. A blank number would\n// sort them to the bottom of the Odoo sale order (see _btk_line_number above).\nlet _addonLineNumber = shopifyItems.reduce(function (max, it) {\n  const n = Number(it.properties._btk_line_number);\n  return isNaN(n) ? max : Math.max(max, n);\n}, 0);\n\nconst addonItems = Object.values(addonMap)\n  .filter(a => a.variantId && a.totalQty > 0)\n  .map(a => {\n    // One number, used twice: the hidden one the Odoo import sorts on and the\n    // visible twin the customer sees. `++` must not run twice or they diverge.\n    const lineNo = ++_addonLineNumber;\n    return {\n      id:       a.variantId,\n      quantity: a.totalQty,\n      properties: {\n        '_btk_line_number': lineNo,\n        'Item':             lineNo,\n        '_btk_addon':       a.handle || '',\n        '_btk_addon_units': a.unitTitles.join(', ')\n      }\n    };\n  });\n\n// WARNING: Shopify REVERSES a multi-item /cart/add.js. Post [1..N] and the cart\n// comes back [N..1]. That is what put the original test order (WKIT-SO242308)\n// into Odoo back to front, and it flipped the checkout summary the moment we\n// started sorting. So post the list reversed: the cart, the basket page and the\n// checkout summary then all read 1..N, addons last. Reversing the WHOLE array\n// is what keeps the addons on the end.\n// If a future Shopify change drops the reversal this flips again. The tell is\n// the checkout summary counting DOWN. Odoo is safe either way, because the\n// import sorts on _btk_line_number rather than on the order lines arrive in.\nconst allShopifyItems = [...orderedItems, ...addonItems].reverse();\n\n// WHICH of the three writes is slow. Together they cost 12.8s of a 17.9s\n// checkout for a three-unit plan (Arden, 2026-09-28) and the single total said\n// nothing about which one. The planner exposes BTKKPMark under ?btktime=1;\n// without it every call here is a no-op.\nconst _ckMark = (l) => { try { if (window.BTKKPMark) window.BTKKPMark(l); } catch (e) {} };\n_ckMark('basket read, ' + allShopifyItems.length + ' lines to write');\n// Built once so its SIZE can be reported with the add's own timing below.\nconst _addBody = JSON.stringify({ items: allShopifyItems });\n\nreturn fetch('/cart/clear.js', { method: 'POST' })\n  .then(() => {\n    _ckMark('cart cleared');\n    // An empty basket empties the cart; there is nothing to add.\n    if (!allShopifyItems.length) return null;\n    return fetch('/cart/add.js', {\n      method:  'POST',\n      headers: { 'Content-Type': 'application/json' },\n      body:    _addBody\n    });\n  })\n  .then(r => {\n    // \u26A0 COUNT AND SIZE HERE, not in a mark that fires when the push starts.\n    // A push already running when the customer clicks fires that earlier mark\n    // while timing is still off, so it is simply missing from the log - which\n    // is exactly what happened to Arden's 2026-09-28 run. On the add's own\n    // mark it cannot be lost, and it is what says whether the six seconds\n    // scales with the number of lines or is flat.\n    _ckMark('lines added (' + allShopifyItems.length + ' lines, '\n            + Math.round(_addBody.length / 1024) + ' kB)');\n    if (!r) return { items: [] };\n    // A refused add (422: a variant gone or out of stock) has ALREADY cleared\n    // the cart. It used to be read as success and carry on to stamp this\n    // basket's id onto the empty cart, so checkout believed the cart was ready\n    // and sent the customer on to pre-checkout with nothing in it. Fail\n    // instead, so the caller can say so.\n    if (!r.ok) {\n      return r.json().catch(() => ({})).then(body => {\n        const err = new Error('BTKBasketAPI: the cart refused these items ('\n          + r.status + (body && body.description ? ': ' + body.description : '') + ')');\n        err.status = r.status;\n        err.cartCleared = true;\n        err.description = (body && body.description) || '';\n        err.variantIds = [...new Set(allShopifyItems.map(i => i.id))];\n        throw err;\n      });\n    }\n    return r.json();\n  })\n  .then(result => {\n    const cartAttributes = {\n              _btk_basket_id:   basket.id,\n              _btk_basket_name: basket.name,\n              // The basket's number (planner-api src/basket-refs.js), so the\n              // order and Odoo carry the number the customer has been quoting.\n              _btk_basket_ref:  basket.ref || '',\n              // Which kind of basket the cart now holds. The standard basket\n              // owns the cart; a plan only BORROWS it for checkout, and the\n              // hand-back (handBackBorrowedCart) reads this to know it is one.\n              _btk_basket_source: basket.source === 'planner' ? 'planner' : 'manual',\n              _btk_range:       basket.range ? JSON.stringify(basket.range) : '',\n              // Order Extras: the same link, for the order import - it files\n              // the Odoo order as \"Extras for <the main order>\".\n              _btk_extras:      (basket.extras && basket.extras.token) || '',\n              // The basket's note for the whole order (Delivery notes, on the\n              // Estimated Delivery card) - the order import files it as the\n              // Odoo order's Main Delivery Comment. '' removes it.\n              _btk_order_note:  String(basket.orderNote || '').slice(0, 2000),\n              _btk_synced_at:   Utils.now(),\n              // WHICH SAVE of the basket this cart holds. A basket saved\n              // somewhere else since - staff building it for the customer,\n              // another device - leaves this browser's cart an old copy, and\n              // the basket page must not cut the basket down to match it\n              // (btk-cart-advanced.js syncShopifyItemsToBasket).\n              _btk_basket_rev:  basket.updatedAt || '',\n              ...CLEARED_CHECKOUT_CHOICES\n            };\n            return fetch('/cart/update.js', {\n              method:  'POST',\n              headers: { 'Content-Type': 'application/json' },\n              body:    JSON.stringify({ attributes: cartAttributes })\n            })\n            .then(r => (r && r.ok ? r.json().catch(() => null) : null))\n            .then(cart => applyFreeItemCodes(cart, allItems, basket))\n            .then(() => {\n              _ckMark('attributes written');\n              // The header's basket count is rendered by the server, so a cart\n              // changed here left it showing the OLD number until the next page\n              // load - e.g. a basket emptied by this sync still showed units.\n              // Every sync redraws it (btk-cart-count.js, loaded on every page).\n              try { if (typeof window.btkRefreshCartCount === 'function') window.btkRefreshCartCount(); } catch (e) {}\n              return result;\n            });\n          });\n      });\n    }\n  };\n\n  // \u2500\u2500\u2500 STAFF HOLD (Arden, 2026-10-16) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  // While a salesperson works on a customer's plans in staff mode, the plans\n  // are theirs: every save PUTs the WHOLE project, so the customer's own page\n  // saving at the same time overwrote the salesperson's work (and the other\n  // way round). planner-api keeps the hold (src/staff-hold.js) and refuses the\n  // customer's saves and checkout while it stands.\n  //   Staff page:    takes it on load, refreshes it on activity (at most every\n  //                  2 minutes; their saves refresh it too), hands it back on\n  //                  Back to Odoo / Exit. Left in a closed tab, it lapses 30\n  //                  minutes after their last activity.\n  //   Customer page: checks every 30 seconds (planner, basket, pre-checkout)\n  //                  and shows a message while it stands; when it ends the\n  //                  page reloads, so they carry on from the salesperson's\n  //                  version, not an old copy that would overwrite it. In a\n  //                  live co-design session they are only watching, so no\n  //                  message covers the plan they are watching.\n  //   Another salesperson opening the same customer is refused the same way\n  //   (review of #106): a message worded for staff, no saves, and their page\n  //   reloads - and takes the plan - once the first one finishes.\n  const STAFF_HOLD_PING_MS = 2 * 60 * 1000;\n  const STAFF_HOLD_POLL_MS = 30 * 1000;\n  const STAFF_HOLD_PAGES = /kitchen-planner|^\\/cart(\\/|$)|pre-checkout/;\n  const LIVE_VIEWER = (() => {\n    try {\n      const p = new URLSearchParams(window.location.search);\n      return !!(p.get('session') || '').trim() && (p.get('role') || '').trim().toLowerCase() === 'viewer';\n    } catch (e) { return false; }\n  })();\n  const StaffHold = {\n    held: null,         // customer side: { until } while staff hold the plans\n    el: null,\n    _lastPing: 0,\n    _polling: false,\n    // PER PLAN (Arden, 2026-10-20): the hold is on the plan this page has open\n    // - the planner's current plan, or on the basket pages the basket being\n    // checked out - so the customer and other staff keep the rest of the\n    // customer's plans. None known yet: the whole customer, as before. An API\n    // from before per-plan holds ignores the plan and holds everything.\n    _heldPlan: undefined,\n    planId() {\n      const p = ProxyAdapter._project;\n      if (!p) return null;\n      if (/kitchen-planner/.test(window.location.pathname)) return p.activePlannerBasketId ? String(p.activePlannerBasketId) : null;\n      const a = (p.baskets || []).find(b => b && b.isActive);\n      return a ? String(a.id) : null;\n    },\n    // The planner has moved to another plan: hand the last one back, hold this one.\n    planChanged() {\n      if (!OVERRIDE_CIDT || this._heldPlan === undefined || this.planId() === this._heldPlan) return;\n      const was = this._heldPlan;\n      ProxyAdapter._http('/my/staff-hold/release', { method: 'POST', body: JSON.stringify(was ? { basketId: was } : {}) })\n        .catch(() => null)\n        .then(() => this.take());\n    },\n    take() {\n      this._lastPing = Date.now();\n      const plan = this.planId();\n      this._heldPlan = plan;\n      return ProxyAdapter._http('/my/staff-hold', { method: 'POST', body: JSON.stringify(plan ? { basketId: plan } : {}) })\n        .catch(e => {\n          if (!this.noteRefusal(e)) console.warn('[BTK staff] could not take the plan over', e && e.status);\n        });\n    },\n    // Never holds the salesperson up for long: the hold lapses by itself anyway.\n    // Once handed back, this page saves nothing more (see _send): a save is\n    // staff work, so the server took the hold straight back - Julian's Exit\n    // Staff Mode on Karen Pratt was followed 2s later by a queued autosave\n    // that held her plans for another 30 minutes (Arden, 2026-10-22).\n    _leaving: false,\n    release() {\n      this._leaving = true;\n      return Promise.race([\n        ProxyAdapter._http('/my/staff-hold/release', { method: 'POST', body: '{}' }).catch(() => null),\n        new Promise(r => setTimeout(r, 3000)),\n      ]);\n    },\n    startStaff() {\n      // After the project has loaded, so the hold is on the plan they opened.\n      STAFF_READY.then(() => (OVERRIDE_CIDT ? ProxyAdapter._ensureLoaded().catch(() => null) : null)).then(() => {\n        if (!OVERRIDE_CIDT) return;     // staff mode refused: nothing to hold\n        this.take();\n        const ping = () => { if (Date.now() - this._lastPing >= STAFF_HOLD_PING_MS) this.take(); };\n        ['pointerdown', 'keydown', 'wheel', 'touchstart'].forEach(t =>\n          window.addEventListener(t, ping, { passive: true, capture: true }));\n      });\n    },\n    // Watches the hold: the customer's page from load, a refused salesperson's\n    // from the refusal. Held = held by someone else (`mine` is this\n    // salesperson's own hold).\n    startCustomer() {\n      if (this._polling) return;\n      this._polling = true;\n      const check = () => {\n        if (document.visibilityState === 'hidden') return;\n        const plan = this.planId();\n        ProxyAdapter._http('/my/staff-hold' + (plan ? '?basketId=' + encodeURIComponent(plan) : ''))\n          .then(r => this.set(r && r.held && !r.mine ? {\n            until: r.until || null, other: !!OVERRIDE_CID,\n            // A live session the salesperson has open for this customer - the\n            // notice offers to join it (show).\n            session: (!OVERRIDE_CID && typeof r.session === 'string' && /^[A-Za-z0-9-]{8,64}$/.test(r.session)) ? r.session : null,\n          } : null))\n          .catch(() => {});             // can't tell: leave things as they are\n      };\n      check();\n      setInterval(check, STAFF_HOLD_POLL_MS);\n      document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible') check(); });\n    },\n    set(hold) {\n      const was = !!this.held;\n      const wasSession = (this.held && this.held.session) || null;\n      this.held = hold || null;\n      // Still held, but a session has opened (or closed) since the notice\n      // went up: draw it again with (or without) the way in.\n      if (was && this.held && (this.held.session || null) !== wasSession) { this.hide(); this.show(); }\n      if (this.held && !was) {\n        this.show();\n        try { window.dispatchEvent(new CustomEvent('btk:staff-hold', { detail: { held: true } })); } catch (_) {}\n      } else if (!this.held && was) {\n        this.hide();\n        // The salesperson's version is the plan now: load it, rather than let\n        // this page's older copy be saved over it.\n        if (!LIVE_VIEWER) window.location.reload();\n      }\n    },\n    show() {\n      if (LIVE_VIEWER || this.el) return;\n      const paint = () => {\n        if (!document.body || this.el || !this.held) return;\n        const el = document.createElement('div');\n        el.id = 'btk-staff-hold';\n        el.setAttribute('role', 'alertdialog');\n        el.setAttribute('aria-modal', 'true');\n        el.style.cssText = 'position:fixed;inset:0;z-index:2147483000;display:flex;align-items:center;'\n          + 'justify-content:center;padding:16px;background:rgba(0,0,0,.6);'\n          + 'font:400 16px/1.5 system-ui,-apple-system,Segoe UI,sans-serif;color:#222;';\n        const card = document.createElement('div');\n        card.style.cssText = 'background:#fff;border-radius:8px;max-width:480px;width:100%;'\n          + 'padding:28px 28px 24px;box-shadow:0 10px 40px rgba(0,0,0,.3);';\n        const staff = !!OVERRIDE_CID;\n        const h = document.createElement('h2');\n        h.textContent = staff ? 'Someone Else Is Working on This Plan' : 'We\\'re Updating Your Plan';\n        h.setAttribute('aria-live', 'polite');\n        h.style.cssText = 'margin:0 0 12px;font-family:inherit;font-size:22px;font-weight:700;line-height:1.25;color:#111;';\n        const p = document.createElement('p');\n        p.textContent = staff\n          ? 'Another member of staff has this customer\\'s plan open in staff mode, so it can\\'t be changed from here for the moment. '\n            + 'This will close by itself, and the plan will open for you, as soon as they\\'ve finished - or 30 minutes after their last change.'\n          : 'One of our kitchen designers is working on your plan right now, so it can\\'t be changed or '\n            + 'checked out for the moment. This message will close by itself and your plan will update as soon as they\\'ve finished.';\n        p.style.cssText = 'margin:0;';\n        card.appendChild(h);\n        card.appendChild(p);\n        // Their designer has a live session open: join it from here, on this\n        // page, rather than wait for a link (Arden, 2026-10-19).\n        const sid = !staff && this.held.session;\n        if (sid) {\n          const p2 = document.createElement('p');\n          p2.textContent = 'They have a live session open, so you can watch the changes as they make them.';\n          p2.style.cssText = 'margin:12px 0 0;';\n          const join = document.createElement('button');\n          join.type = 'button';\n          join.textContent = 'Join Live Session';\n          join.style.cssText = 'margin-top:18px;background:#2d7a3a;color:#fff;border:1px solid #2d7a3a;'\n            + 'border-radius:6px;padding:10px 18px;font:600 15px/1.2 inherit;font-family:inherit;cursor:pointer;';\n          join.addEventListener('click', () => {\n            const path = /kitchen-planner/.test(window.location.pathname) ? window.location.pathname : '/pages/kitchen-planner';\n            window.location.href = path + '?session=' + encodeURIComponent(sid) + '&role=viewer&view3d=1';\n          });\n          card.appendChild(p2);\n          card.appendChild(join);\n        }\n        el.appendChild(card);\n        document.body.appendChild(el);\n        this.el = el;\n      };\n      if (document.body) paint(); else document.addEventListener('DOMContentLoaded', paint, { once: true });\n    },\n    hide() {\n      if (this.el && this.el.parentNode) this.el.parentNode.removeChild(this.el);\n      this.el = null;\n    },\n    // A save or checkout hold the server refused because staff hold the plan.\n    noteRefusal(e) {\n      if (!(e && e.status === 409 && e.body && e.body.error === 'staff_editing')) return false;\n      this.set({ until: e.body.until || null, other: !!e.body.other });\n      // A salesperson shut out by a colleague watches for the plan to come free.\n      if (OVERRIDE_CID) this.startCustomer();\n      return true;\n    },\n  };\n  // Started once this script has finished defining everything it uses.\n  setTimeout(() => {\n    try {\n      if (OVERRIDE_CID) StaffHold.startStaff();\n      else if (SERVER_ENABLED && Utils.isLoggedIn() && !READ_ONLY_VIEW\n               && STAFF_HOLD_PAGES.test(window.location.pathname)) StaffHold.startCustomer();\n    } catch (e) { console.warn('[BTK] staff hold check not started', e); }\n  }, 0);\n\n  // \u2500\u2500\u2500 PROXY (SERVER) ADAPTER \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  // Logged-in customers only. Loads the customer's whole project once from the\n  // server (via the App Proxy), mutates it in memory (same logic as the session\n  // adapter), and PUTs the whole project back. Identity = the proxy's trusted\n  // logged_in_customer_id (no client-supplied id). Falls back to a fresh project\n  // if the server is unreachable so the planner never hard-fails.\n\n  const ProxyAdapter = {\n\n    _project: null,\n    _loaded:  null,\n    // A load that FAILED, as distinct from a customer who simply has nothing\n    // saved yet. Conflating the two is what loses people their plans - see\n    // _ensureLoaded.\n    _loadFailed: false,\n    // Set when we carry a staff cid the server refused to honour.\n    _staffRejected: false,\n    // The PUT currently in the air, and whether more changes landed while it\n    // was flying. See _save.\n    _saving: null,\n    _saveQueued: false,\n\n    _http(path, options = {}) {\n      return STAFF_READY.then(() => this._httpNow(path, options));\n    },\n\n    _httpNow(path, options = {}) {\n      // Pass the Shopify-rendered customer id so the backend can identify the\n      // customer even when New customer accounts omit logged_in_customer_id from\n      // the App Proxy request. Shopify signs all query params, so cid is covered\n      // by the proxy signature (client-asserted; see Option A note in the API).\n      const cid = effectiveCustomerId();\n      const sep = path.indexOf('?') === -1 ? '?' : '&';\n      let url = `${SERVER_BASE_URL}${path}${cid ? `${sep}cid=${encodeURIComponent(cid)}` : ''}`;\n      // Forward the staff-override signature so the backend will honour the cid.\n      if (OVERRIDE_CIDT) url += `${url.indexOf('?') === -1 ? '?' : '&'}cidt=${encodeURIComponent(OVERRIDE_CIDT)}`;\n      return fetch(url, {\n        ...options,\n        credentials: 'same-origin',\n        headers: { 'Content-Type': 'application/json' }\n      }).then(async (r) => {\n        if (!r.ok) {\n          // Keep the status and body: a 409 from the plan lock is a rule, not\n          // a fault, and the planner has to tell them apart.\n          const err = new Error(`BTKBasketAPI proxy error: ${r.status}`);\n          err.status = r.status;\n          try { err.body = await r.json(); } catch (_) { err.body = null; }\n          // A staff token used from a different signed-in account than the one\n          // it was issued to (e.g. signed into a personal account since).\n          if (r.status === 403 && err.body && err.body.error === 'staff_account' && OVERRIDE_CID) {\n            STAFF_REFUSAL = STAFF_REFUSAL || { workEmail: '' };\n            this._staffRejected = true;\n            try { StaffBanner.show('expired'); } catch (_) {}\n          }\n          // Refused outright (signed out, and the token no good): the bar\n          // drawn early from the held session must not go on saying \"acting as\".\n          if (r.status === 401 && OVERRIDE_CID) {\n            this._staffRejected = true;\n            try { StaffBanner.show('expired'); } catch (_) {}\n          }\n          throw err;\n        }\n        return r.json();\n      });\n    },\n\n    // A plan that has been bought is frozen server-side (basket_locks). The\n    // planner needs to know BEFORE it lets anyone touch one, so the list comes\n    // down with the project. A build of the API without this route answers 404,\n    // which simply means \"nothing is locked\" - never a reason to fail the load.\n    _loadLocks() {\n      return this._http('/baskets/locks')\n        .then((res) => {\n          this._locks = new Map(((res && res.locks) || [])\n            .map(l => [String(l.basketId), { status: l.status, orderRef: l.orderRef || null }]));\n        })\n        .catch(() => { if (!this._locks) this._locks = new Map(); });\n    },\n\n    // Read the project, retrying a couple of times before giving up. Most\n    // failures here are a momentary blip, and the cost of mistaking one for\n    // \"this customer has nothing saved\" is severe enough to be worth the wait.\n    async _fetchProject(attempts = 3) {\n      let lastErr;\n      for (let i = 0; i < attempts; i++) {\n        try {\n          return await this._http('/project');\n        } catch (e) {\n          lastErr = e;\n          if (i < attempts - 1) await new Promise(r => setTimeout(r, 300 * (i + 1)));\n        }\n      }\n      throw lastErr;\n    },\n\n    _ensureLoaded() {\n      if (this._loaded) return this._loaded;\n      this._loaded = this._fetchProject()\n        .then(res => {\n          this._loadFailed = false;\n          // The SERVER decides whether staff mode is real. If we are carrying a\n          // cid that was NOT honoured - an expired link, almost always - then\n          // the request fell back to whoever is actually signed in, which for a\n          // salesperson is themselves. Saving now would file the customer's\n          // kitchen under the staff member's own account, so block it and say\n          // so, rather than letting it look like it worked.\n          // Three states, not two. An API build that predates this field says\n          // NOTHING about staff mode - and treating silence as \"refused\" would\n          // block every salesperson the moment the theme shipped ahead of the\n          // API. Only an explicit null is a refusal.\n          const told = res && Object.prototype.hasOwnProperty.call(res, 'actingAs');\n          if (OVERRIDE_CID && told && !res.actingAs) {\n            this._staffRejected = true;\n            console.error('BTKBasketAPI: staff override was NOT accepted - the link has '\n              + 'expired. Saving is disabled so nothing lands on the wrong account.');\n            StaffBanner.show('expired');\n          } else if (OVERRIDE_CID) {\n            _actingAsLabel = STAFF_CUSTOMER_NAME\n              || (res.project && res.project.email)\n              || (told && res.actingAs && res.actingAs.customerId)\n              || OVERRIDE_CID;\n            StaffBanner.show('active', _actingAsLabel);\n          }\n          // `project: null` is a REAL answer - a customer who has not saved\n          // anything yet. That, and only that, starts a fresh project.\n          this._project = res.project\n            ? Utils.cleanLoadedProject(res.project)\n            : Shapes.newProject(Utils.getCustomerId());\n          // What the server has, plan by plan, so a save sends only the plans\n          // this page has changed since (see _changesBody). A new project has\n          // nothing stored: every plan in it is new.\n          this._snapAll(res.project ? this._project : null);\n          // Each plan's version as the server has it (planner-api, PLAN\n          // VERSIONS). A save says which version it started from, so a copy\n          // this page has held while someone else changed the plan cannot be\n          // saved over theirs (bug sheet row 67). Kept apart from the plans:\n          // the planner holds copies of them, which would carry an old number.\n          this._revs = Object.assign({}, (res.project && res.project._revs) || {});\n          delete this._project._revs;\n          const arriving = STAFF_ARRIVAL && OVERRIDE_CIDT && !this._staffRejected;\n          // A fresh basket after an order is the page's own decision, so not\n          // when arriving from Odoo (that makes or picks its own), not on a\n          // view that only looks, and not for a refused staff link.\n          const mayStartFresh = !arriving && !READ_ONLY_VIEW && !this._staffRejected;\n          return this._loadLocks().then(() => this._dropOrderedCurrent(this._project, mayStartFresh)).then(() => (arriving\n            ? this._arrive(STAFF_ARRIVAL, !res.project)\n            : this._project));\n        })\n        .catch(e => {\n          // A FAILED LOAD IS NOT AN EMPTY PROJECT. The server upsert replaces\n          // the whole row, so carrying on with a blank project and then saving\n          // it would delete every plan the customer has. Keep working in memory\n          // so the page still functions, but refuse to WRITE until a load has\n          // actually succeeded.\n          console.error('BTKBasketAPI: project load FAILED - saving is disabled for '\n            + 'this session so the stored project cannot be overwritten', e);\n          this._loadFailed = true;\n          this._loaded = null;   // never cache the failure; let the next call retry\n          this._project = Shapes.newProject(Utils.getCustomerId());\n          try { window.dispatchEvent(new CustomEvent('btk:project-load-failed')); } catch (_) {}\n          return this._project;\n        });\n      return this._loaded;\n    },\n\n    _save() {\n      if (!this._project) return Promise.resolve(null);\n      // See READ_ONLY_VIEW. Resolves, so callers carry on as if saved.\n      if (READ_ONLY_VIEW) return Promise.resolve(this._project);\n      // A live-session viewer only looks (LIVE_VIEWER, below): its copy is\n      // re-read from the server as the salesperson changes it, never written back.\n      if (LIVE_VIEWER) return Promise.resolve(this._project);\n      // Staff are working on these plans (see STAFF HOLD): nothing from this\n      // page may land on top of theirs. The page reloads when they finish.\n      if (StaffHold.held) return Promise.resolve(this._project);\n      // A newer copy of a plan is on its way in (_planChanged): nothing more\n      // from this page's old one.\n      if (this._changedElsewhere) return Promise.resolve(this._project);\n      // Staff link expired: we are not who we think we are. See _ensureLoaded.\n      if (this._staffRejected) {\n        return Promise.reject(new Error(\n          'BTKBasketAPI: staff link expired - refusing to save this plan against '\n          + 'the wrong account'));\n      }\n      // Never write over a project we never managed to read. See _ensureLoaded.\n      if (this._loadFailed) {\n        return Promise.reject(new Error(\n          'BTKBasketAPI: project never loaded - refusing to save over it'));\n      }\n      // New Plan from an opportunity can land on an unsaved draft the planner\n      // made before this session (engine-storage getWorkingBasket reuses it),\n      // which newBasket never stamped. The draft is the plan being started.\n      if (STAFF_OPPORTUNITY) {\n        (this._project.baskets || []).forEach((b) => {\n          if (b && b.isDraft && b.source === 'planner') stampOpportunity(b);\n        });\n      }\n      this._project.updatedAt = Utils.now();\n      // Stamp the Shopify-rendered customer email so Odoo can match this\n      // project to a res.partner (see bk_planner_link). Identity is still the\n      // proxy's trusted logged_in_customer_id; email is only the human bridge.\n      // Don't clobber the owner's email when a staff member is editing via a\n      // ?cid override - only the real logged-in customer stamps their email.\n      if (!OVERRIDE_CID && window.BTKCustomer && window.BTKCustomer.email) {\n        this._project.email = window.BTKCustomer.email;\n      }\n      // Coalesce concurrent writes. EVERY mutation PUTs the whole project -\n      // ~500 kB for a real kitchen - and the planner mutates in bursts, so a\n      // drag used to cost a stack of full-project uploads. While one PUT is in\n      // the air, later calls just mark the project dirty and share a single\n      // follow-up write. No timer is involved, so unlike a debounce this can\n      // never leave the last edit unsaved when the page goes away.\n      if (this._saving) {\n        this._saveQueued = true;\n        return this._saving;\n      }\n      return this._flushSave();\n    },\n\n    // The server accepts a whole-project save but PUTS BACK any basket that is\n    // held for checkout or already ordered, and lists them in `kept`. Without\n    // this the page never knew: it said \"Saved\" while those edits were\n    // discarded. Record the locks (they may have been placed from another tab\n    // after this page loaded) and tell whoever is showing that basket.\n    _noteKept(body) {\n      const kept = (body && Array.isArray(body.kept)) ? body.kept : [];\n      if (!kept.length) return;\n      if (!this._locks) this._locks = new Map();\n      kept.forEach((k) => {\n        if (k && k.basketId) this._locks.set(String(k.basketId), { status: k.status || 'ordered', orderRef: null });\n      });\n      try { window.dispatchEvent(new CustomEvent('btk:plan-kept', { detail: { kept } })); } catch (_) {}\n    },\n\n    // Basket numbers (planner-api src/basket-refs.js): the server stamps one\n    // on every basket and says which on each save. Kept on the baskets this\n    // page holds, so the next save and the cart carry them, and the page is\n    // told so it can show a number that has just arrived.\n    _noteRefs(body) {\n      const refs = body && body.refs;\n      if (!refs || !this._project || !Array.isArray(this._project.baskets)) return;\n      let changed = false;\n      this._project.baskets.forEach((b) => {\n        const r = b && refs[String(b.id)];\n        if (r && b.ref !== r) { b.ref = r; changed = true; }\n      });\n      if (changed) { try { window.dispatchEvent(new CustomEvent('btk:basket-refs', { detail: { refs } })); } catch (_) {} }\n    },\n\n    // \u2500\u2500\u2500 SAVING ONLY THE CHANGED PLANS (Arden, 2026-10-20) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n    // A whole-project save wrote every plan each time, so two people in two of\n    // a customer's plans overwrote each other and a salesperson's hold had to\n    // cover all of them. planner-api's POST /project/changes takes just the\n    // plans this page changed (and the ones it deleted) and merges them into\n    // what is stored. Until that API is live it answers 404, and the page goes\n    // on with the whole-project PUT for the rest of the visit.\n    //   _basketSnap: plan id -> the JSON the server last had from this page.\n    //   _fieldSnap:  the project's own fields, likewise (less updatedAt).\n    _basketSnap: null,\n    _fieldSnap: null,\n    _changesUnsupported: false,\n    _fieldsJson(project) {\n      const f = {};\n      Object.keys(project || {}).forEach((k) => {\n        if (k !== 'baskets' && k !== 'updatedAt' && k !== 'email' && k !== '_revs') f[k] = project[k];\n      });\n      return JSON.stringify(f);\n    },\n    _snapAll(project) {\n      this._basketSnap = new Map();\n      ((project && project.baskets) || []).forEach((b) => {\n        if (b && b.id != null) this._basketSnap.set(String(b.id), JSON.stringify(b));\n      });\n      this._fieldSnap = project ? this._fieldsJson(project) : null;\n    },\n    // The request for what changed, or null when there is no snapshot to\n    // compare with (then the whole project goes). `empty`: nothing to send.\n    _changesBody() {\n      const p = this._project;\n      if (!p || !this._basketSnap) return null;\n      const baskets = [], snaps = [], seen = new Set();\n      (p.baskets || []).forEach((b) => {\n        if (!b || b.id == null) return;\n        const id = String(b.id), json = JSON.stringify(b);\n        seen.add(id);\n        if (this._basketSnap.get(id) !== json) { baskets.push(b); snaps.push([id, json]); }\n      });\n      const removed = [...this._basketSnap.keys()].filter(id => !seen.has(id));\n      const fieldsJson = this._fieldsJson(p);\n      const empty = !baskets.length && !removed.length && fieldsJson === this._fieldSnap;\n      const fields = JSON.parse(fieldsJson);\n      fields.updatedAt = p.updatedAt;\n      // The version each plan this page had from the server started at (0: it\n      // has none yet). A plan made on this page and not yet saved has none to give.\n      const revs = {};\n      baskets.map(b => String(b.id)).concat(removed).forEach((id) => {\n        if (this._basketSnap.has(id)) revs[id] = (this._revs && this._revs[id]) || 0;\n      });\n      return { empty, snaps, removed, fieldsJson,\n               body: JSON.stringify({ baskets, removed, fields, revs }) };\n    },\n    // The versions the server counted for what it has just stored.\n    _revs: null,\n    _noteRevs(body) {\n      if (!body || !body.revs || typeof body.revs !== 'object') return;\n      this._revs = Object.assign(this._revs || {}, body.revs);\n    },\n    // The server refused a save because a plan has changed since this page\n    // loaded it - a salesperson, or this customer in another tab. This page's\n    // copy is the old one: it says so and loads the new one, saving nothing more.\n    _changedElsewhere: false,\n    _planChanged(body) {\n      if (this._changedElsewhere) return;\n      this._changedElsewhere = true;\n      console.warn('BTKBasketAPI: this page had an older copy of the plan; loading the newer one', body);\n      try { window.dispatchEvent(new CustomEvent('btk:plan-changed', { detail: body })); } catch (_) {}\n      const reload = () => { try { window.location.reload(); } catch (_) {} };\n      const D = window.BTKKPDialog;\n      if (!D || typeof D.alert !== 'function') return reload();\n      Promise.resolve(D.alert('This plan has been changed since this page opened it, by our team or in another window. '\n        + 'The latest version will now load.', { title: 'Plan updated' })).then(reload, reload);\n    },\n    _noteSaved(req) {\n      if (!this._basketSnap) this._basketSnap = new Map();\n      req.snaps.forEach(([id, json]) => this._basketSnap.set(id, json));\n      req.removed.forEach(id => this._basketSnap.delete(id));\n      this._fieldSnap = req.fieldsJson;\n    },\n    _putWhole() {\n      const p = this._project;\n      const req = { snaps: (p.baskets || []).filter(b => b && b.id != null).map(b => [String(b.id), JSON.stringify(b)]),\n                    removed: [], fieldsJson: this._fieldsJson(p) };\n      return this._http('/project', { method: 'PUT', body: JSON.stringify(p) })\n        .then((body) => { this._basketSnap = new Map(); this._noteSaved(req); return body; });\n    },\n    _send() {\n      // The salesperson has handed the plan back and is leaving: a late\n      // autosave would take the hold again (StaffHold.release).\n      if (OVERRIDE_CIDT && StaffHold._leaving) return Promise.resolve({ ok: true, skipped: true });\n      const req = this._changesUnsupported ? null : this._changesBody();\n      if (!req) return this._putWhole();\n      if (req.empty) return Promise.resolve({ ok: true });\n      return this._http('/project/changes', { method: 'POST', body: req.body })\n        .then((body) => { this._noteSaved(req); return body; })\n        .catch((e) => {\n          if (e && (e.status === 404 || e.status === 405)) {\n            this._changesUnsupported = true;    // an API from before - the whole project, as before\n            return this._putWhole();\n          }\n          throw e;\n        });\n    },\n\n    _flushSave() {\n      this._saving = this._send()\n        .then((body) => {\n          this._noteKept(body);\n          this._noteRefs(body);\n          this._noteRevs(body);\n          this._saving = null;\n          if (this._saveQueued) {\n            this._saveQueued = false;\n            return this._flushSave();   // write whatever changed mid-flight\n          }\n          return this._project;\n        })\n        .catch(e => {\n          this._saving = null;\n          this._saveQueued = false;\n          // 409 staff_editing: a salesperson took the plans over (see STAFF\n          // HOLD). The message says so; this page's copy is replaced when they\n          // finish, so it is not a failed save to cry about.\n          if (StaffHold.noteRefusal(e)) return this._project;\n          // 409 plan_changed: this page's copy of a plan is older than the\n          // stored one. Not saved over it: the newer one is loaded instead.\n          if (e && e.status === 409 && e.body && e.body.error === 'plan_changed') {\n            this._planChanged(e.body);\n            return this._project;\n          }\n          // 409 plan_locked: the customer tried to change a plan they have\n          // already bought. That is the rule working, not a broken save, so it\n          // records the lock and says so rather than crying \"save failed\".\n          if (e && e.status === 409 && e.body && e.body.error === 'plan_locked') {\n            const id = String(e.body.basketId || '');\n            if (!this._locks) this._locks = new Map();\n            if (id) this._locks.set(id, { status: e.body.status || 'ordered', orderRef: null });\n            console.warn('BTKBasketAPI: that plan has been ordered and cannot be changed', e.body);\n            try {\n              window.dispatchEvent(new CustomEvent('btk:plan-locked', { detail: e.body }));\n            } catch (_) {}\n            throw e;\n          }\n          // Propagated, not swallowed: a save that quietly did nothing is how a\n          // customer loses an afternoon's work believing it was stored. The\n          // planner's save paths already turn this into a visible error.\n          console.error('BTKBasketAPI: project save failed', e);\n          try { window.dispatchEvent(new CustomEvent('btk:project-save-failed')); } catch (_) {}\n          throw e;\n        });\n      return this._saving;\n    },\n\n    // An ORDERED basket can come back still flagged current beside the basket\n    // that really is. The server puts back its stored copy of an ordered\n    // basket on every save (see _noteKept), flag and all, so once someone\n    // moved on to a new basket there were two \"current\" ones - and the first,\n    // the one already bought, won: its lines were refilled into the cart and\n    // the new basket looked empty (Arden, 2026-10-10). The other one is the\n    // real choice, so the ordered one's flag is dropped here. A HELD basket\n    // (being paid for right now) is never touched.\n    //\n    // And when the ONLY current basket is an ordered standard basket, the\n    // customer has bought it and come back: like a shop, they get a fresh\n    // empty basket (`mayStartFresh`). Left as it was, everything they added\n    // next went into the cart but not into any saved basket - the server puts\n    // the bought one back on every save (Arden, 2026-10-11). The ordered one\n    // stays in their list as the record of what they bought.\n    _dropOrderedCurrent(p, mayStartFresh) {\n      const ordered = (b) => { const l = this.getBasketLock(b.id); return !!(l && l.status === 'ordered'); };\n      const current = p.baskets.filter(b => b.isActive);\n      if (current.length > 1 && current.some(b => !ordered(b))) {\n        current.forEach((b) => { if (ordered(b)) b.isActive = false; });\n      } else if (mayStartFresh && current.length\n                 && current.every(ordered) && current.some(b => b.source !== 'planner')) {\n        current.forEach((b) => { b.isActive = false; });\n        p.baskets.push(Shapes.newBasket(Utils.generateId('basket'), 'My Quote', true, 'manual'));\n        // Saved now, so the fresh basket is the current one on every page\n        // and device from here on. A failed save is reported by _save itself.\n        this._save().catch(() => {});\n      }\n      return p;\n    },\n\n    // Arriving from Odoo in a basket session (STAFF_ARRIVAL): make the new\n    // basket, or select the one asked for, as the customer's current basket;\n    // save; refill the browser's cart from it; then reload the page without\n    // the link's parameters. Runs INSIDE the first project load and never\n    // resolves on success, so nothing on this page - above all the cart\n    // page's reconcile, which would read the salesperson's own cart into the\n    // customer's basket - acts before the reload. A failure says so and\n    // carries on with the project as loaded.\n    _arrive(arrival, freshProject) {\n      const p = this._project;\n      let target = null;\n      if (arrival.kind === 'new') {\n        // A customer with nothing saved gets a project holding one empty\n        // \"Unsaved Quote\": name that one rather than leave a stray beside it.\n        const only = freshProject && p.baskets.length === 1 ? p.baskets[0] : null;\n        if (only && only.source !== 'planner' && !(only.items || []).length) {\n          only.name = arrival.name;\n          // Made before this session, so newBasket never stamped it.\n          target = stampOpportunity(only);\n        } else {\n          target = Shapes.newBasket(Utils.generateId('basket'), arrival.name, false, 'manual');\n          p.baskets.push(target);\n        }\n      } else {\n        target = p.baskets.find(b => String(b.id) === arrival.id && b.source !== 'planner') || null;\n      }\n      const tell = (msg) => {\n        try {\n          if (window.BTKKPDialog && typeof window.BTKKPDialog.alert === 'function') {\n            window.BTKKPDialog.alert(msg, { title: 'Basket Not Ready' });\n          } else { window.alert(msg); }\n        } catch (_) {}\n      };\n      if (!target) {\n        tell('That basket is no longer in this customer\u2019s account. Go back to Odoo and refresh the Shopping Carts tab.');\n        return emptyBrowserCart().then(() => p);\n      }\n      p.baskets.forEach(b => { b.isActive = (b.id === target.id); });\n      const own = { getBaskets: () => Promise.resolve(p.baskets), getActiveBasket: () => Promise.resolve(target) };\n      return this._save()\n        .then(() => SessionAdapter.syncToShopifyCart.call(own, target.id))\n        // The urgency from Odoo, kept with the basket by the planner service.\n        // Not set is said, and the basket still opens: it can be set again\n        // from Sales Tools.\n        .then(() => (arrival.urgency\n          ? this._http('/my/basket-urgency', { method: 'POST', body: JSON.stringify({ basketId: target.id, urgency: arrival.urgency }) })\n              .catch((e) => {\n                console.warn('BTKBasketAPI: could not mark the basket ' + arrival.urgency, e);\n                tell('The basket is ready, but it could not be marked ' + (arrival.urgency === 'urgent' ? 'Urgent' : 'Warning')\n                  + ' just now. Set it from Sales Tools > Set Urgency.');\n              })\n          : null))\n        .then(() => {\n          window.location.replace(addressWithoutStaffParams());\n          return new Promise(() => {});\n        })\n        .catch((e) => {\n          console.error('BTKBasketAPI: could not set up the customer\u2019s basket', e);\n          tell('The basket could not be set up just now. Go back to Odoo and try again.');\n          return emptyBrowserCart().then(() => p);\n        });\n    },\n\n    _getActiveBasket(p) {\n      return p.baskets.find(b => b.isActive) || p.baskets[0] || null;\n    },\n\n    // { status: 'held' | 'ordered', orderRef } for a bought plan, else null.\n    getBasketLock(basketId) {\n      const m = this._locks;\n      return (m && basketId && m.get(String(basketId))) || null;\n    },\n    isBasketLocked(basketId) { return !!this.getBasketLock(basketId); },\n\n    // The plan behind an order, exactly as it was bought. The server answers\n    // from its own snapshot, so this can never show a later edit.\n    getOrderedBasket(basketId) {\n      return this._http(`/baskets/${encodeURIComponent(basketId)}/ordered`).catch(() => null);\n    },\n\n    // My Kitchen Manual for a bought plan. A customer gets its status only;\n    // staff get the written guide to build and check it.\n    kitchenManual(basketId) {\n      return this._http(`/my/kitchen-manual/${encodeURIComponent(basketId)}`);\n    },\n    approveKitchenManual(basketId, dataBase64) {\n      return this._http(`/my/kitchen-manual/${encodeURIComponent(basketId)}/approve`,\n        { method: 'POST', body: JSON.stringify({ dataBase64 }) });\n    },\n    rewriteKitchenManual(basketId) {\n      return this._http(`/my/kitchen-manual/${encodeURIComponent(basketId)}/rewrite`,\n        { method: 'POST', body: '{}' });\n    },\n\n    // One earlier version of a plan, from the server's plan history, for the\n    // planner to open as a NEW plan (Odoo's \"Restore as Copy\"). Null if gone.\n    getPlanVersion(versionId) {\n      return this._http(`/plan-history/${encodeURIComponent(versionId)}`)\n        .then((r) => (r && r.version) || null)\n        .catch(() => null);\n    },\n\n    // Checkout is starting: hold the plan so it cannot move under an order\n    // being paid for. Released by releaseBasket if they come back unpaid.\n    holdBasket(basketId) {\n      if (READ_ONLY_VIEW) return Promise.resolve(null);\n      return this._http(`/baskets/${encodeURIComponent(basketId)}/hold`, { method: 'POST', body: '{}' })\n        .catch((e) => { StaffHold.noteRefusal(e); return null; });\n    },\n    releaseBasket(basketId) {\n      if (READ_ONLY_VIEW) return Promise.resolve(null);\n      return this._http(`/baskets/${encodeURIComponent(basketId)}/release`, { method: 'POST', body: '{}' })\n        .then((r) => {\n          // Keep this page's copy of the locks in step, or it goes on treating\n          // the basket as held until the next load. Only a hold is dropped.\n          const m = this._locks;\n          const cur = m && m.get(String(basketId));\n          if (cur && cur.status === 'held') m.delete(String(basketId));\n          return r;\n        })\n        .catch(() => null);\n    },\n\n    getProject() { return this._ensureLoaded(); },\n\n    getBaskets() { return this._ensureLoaded().then(p => p.baskets); },\n\n    getActiveBasket() { return this._ensureLoaded().then(p => this._getActiveBasket(p)); },\n\n    getActiveManualBasket() {\n      return this._ensureLoaded().then(p => {\n        const active = p.baskets.find(b => b.isActive);\n        if (active && active.source !== 'planner') return active;\n        let manual = p.baskets.find(b => b.source !== 'planner');\n        if (!manual) {\n          manual = Shapes.newBasket(Utils.generateId('basket'), 'My Quote', false, 'manual');\n          p.baskets.push(manual);\n        }\n        p.baskets.forEach(b => { b.isActive = (b.id === manual.id); });\n        return this._save().then(() => manual);\n      });\n    },\n\n    saveBasket(basketData) {\n      return this._ensureLoaded().then(p => {\n        const i = p.baskets.findIndex(b => b.id === basketData.id);\n        basketData.updatedAt = Utils.now();\n        if (i > -1) p.baskets[i] = basketData; else p.baskets.push(basketData);\n        return this._save().then(() => basketData);\n      });\n    },\n\n    // The same, but resolves as soon as THIS PAGE's copy holds the basket and\n    // lets the server write carry on behind, for a screen that only has to SHOW\n    // the basket (the planner's basket panel): waiting on the whole-project\n    // upload is what made it slow to open.\n    // Resolves { basket, saved }: `saved` settles when the server has the write\n    // (it covers any write queued behind it too) and REJECTS if it failed. Code\n    // that treats the basket as stored must wait on `saved`, not assume it -\n    // checkout skips its own save when the plan looks stored, so an unconfirmed\n    // write taken as stored would send an order to payment with no basket on\n    // the server behind it. A failure also raises btk:project-save-failed.\n    saveBasketSoon(basketData) {\n      return this._ensureLoaded().then(p => {\n        const i = p.baskets.findIndex(b => b.id === basketData.id);\n        basketData.updatedAt = Utils.now();\n        if (i > -1) p.baskets[i] = basketData; else p.baskets.push(basketData);\n        const saved = this._save().then(() => basketData);\n        saved.catch(() => {});   // handled by whoever awaits it; never unhandled\n        return { basket: basketData, saved };\n      });\n    },\n\n    // Settles once every server write this page has started has finished\n    // (including one queued behind it). Never rejects.\n    whenSaved() {\n      const pending = this._saving;\n      return pending ? pending.then(() => this.whenSaved(), () => this.whenSaved()) : Promise.resolve();\n    },\n\n    duplicateBasket(basketId) {\n      return this._ensureLoaded().then(p => {\n        const src = p.baskets.find(b => b.id === basketId);\n        if (!src) return Promise.reject(new Error(`Basket ${basketId} not found`));\n        const copy     = Utils.deepClone(src);\n        copy.id        = Utils.generateId('basket');\n        // The server gives the copy its own number on save (basket-refs).\n        delete copy.ref;\n        copy.name      = `${src.name} (Copy)`;\n        copy.isActive  = false;\n        copy.createdAt = Utils.now();\n        copy.updatedAt = Utils.now();\n        p.baskets.push(copy);\n        return this._save().then(() => copy);\n      });\n    },\n\n    createBasket(name, source = 'manual') {\n      return this._ensureLoaded().then(p => {\n        const b = Shapes.newBasket(Utils.generateId('basket'), name, false, source);\n        p.baskets.push(b);\n        return this._save().then(() => b);\n      });\n    },\n\n    deleteBasket(basketId) {\n      return this._ensureLoaded().then(p => {\n        if (p.baskets.length === 1) return Promise.reject(new Error('Cannot delete the only basket in a project'));\n        const wasActive = p.baskets.find(b => b.id === basketId)?.isActive;\n        p.baskets = p.baskets.filter(b => b.id !== basketId);\n        if (wasActive && p.baskets.length > 0) p.baskets[0].isActive = true;\n        return this._save().then(() => ({ deleted: basketId }));\n      });\n    },\n\n    setActiveBasket(basketId) {\n      return this._ensureLoaded().then(p => {\n        const active = p.baskets.find(b => b.id === basketId);\n        if (!active) return Promise.reject(new Error(`Basket ${basketId} not found`));\n        p.baskets.forEach(b => { b.isActive = (b.id === basketId); });\n        return this._save().then(() => active);\n      });\n    },\n\n    getActivePlannerBasketId() {\n      return this._ensureLoaded().then(p => p.activePlannerBasketId || null);\n    },\n\n    // Server-backed twin of the session adapter's version - same reasoning,\n    // same deliberate absence of any isActive mutation. See the note there.\n    getActivePlannerBasket() {\n      return this._ensureLoaded().then((p) => {\n        const id = p.activePlannerBasketId;\n        const byId = id ? p.baskets.find(b => b.id === id) : null;\n        const planner = byId\n          || p.baskets.filter(b => b.source === 'planner').slice(-1)[0]\n          || null;\n        return planner || SessionAdapter._getActiveBasket(p);\n      });\n    },\n\n    setActivePlannerBasketId(basketId) {\n      return this._ensureLoaded().then(p => {\n        p.activePlannerBasketId = basketId || null;\n        try { StaffHold.planChanged(); } catch (_) {}\n        return this._save().then(() => p.activePlannerBasketId);\n      });\n    },\n\n    // Reuse the session adapter's cart sync, but bound to THIS adapter so it\n    // reads the server-backed active basket.\n    syncToShopifyCart(basketId) { return SessionAdapter.syncToShopifyCart.call(this, basketId); }\n  };\n\n  // \u2500\u2500\u2500 HELPERS \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n  const Helpers = {\n\n    recalculateLineNumbers(basket) {\n      let n = 1;\n      basket.sections.forEach(section => {\n        section.items.forEach(item => { item.lineNumber = n++; });\n      });\n      basket.items.forEach(item => { item.lineNumber = n++; });\n      return basket;\n    },\n\n    moveItemToSection(basket, itemId, sectionId) {\n      const item = Helpers.extractItem(basket, itemId);\n      if (!item) return basket;\n      if (sectionId) {\n        const section = basket.sections.find(s => s.id === sectionId);\n        if (section) section.items.push(item);\n      } else {\n        basket.items.push(item);\n      }\n      return Helpers.recalculateLineNumbers(basket);\n    },\n\n    moveItemToPosition(basket, itemId, targetSectionId, targetIndex) {\n      const item = Helpers.extractItem(basket, itemId);\n      if (!item) return basket;\n      const container = targetSectionId\n        ? basket.sections.find(s => s.id === targetSectionId)?.items\n        : basket.items;\n      if (!container) return basket;\n      container.splice(targetIndex, 0, item);\n      return Helpers.recalculateLineNumbers(basket);\n    },\n\n    extractItem(basket, itemId) {\n      const i = basket.items.findIndex(i => i.id === itemId);\n      if (i > -1) return basket.items.splice(i, 1)[0];\n      for (const section of basket.sections) {\n        const idx = section.items.findIndex(i => i.id === itemId);\n        if (idx > -1) return section.items.splice(idx, 1)[0];\n      }\n      return null;\n    },\n\n    updateItemAttribute(basket, itemId, attributeKey, value) {\n      const item = Helpers.findItem(basket, itemId);\n      if (item && item.attributes.hasOwnProperty(attributeKey)) {\n        item.attributes[attributeKey] = value;\n      }\n      return basket;\n    },\n\n    findItem(basket, itemId) {\n      // Search by BTK item ID first\n      const flat = basket.items.find(i => i.id === itemId);\n      if (flat) return flat;\n      for (const section of basket.sections) {\n        const found = section.items.find(i => i.id === itemId);\n        if (found) return found;\n      }\n      // Also support lookup by shopifyVariantId for convenience\n      const byVariant = basket.items.find(i => i.shopifyVariantId === itemId);\n      if (byVariant) return byVariant;\n      for (const section of basket.sections) {\n        const found = section.items.find(i => i.shopifyVariantId === itemId);\n        if (found) return found;\n      }\n      return null;\n    },\n\n    createItem:    Shapes.newItem,\n    createSection: Shapes.newSection,\n\n    duplicateItem(basket, itemId) {\n      // Search sections first\n      for (const section of basket.sections) {\n        const idx = section.items.findIndex(i => i.id === itemId);\n        if (idx > -1) {\n          const duplicate        = JSON.parse(JSON.stringify(section.items[idx]));\n          duplicate.id           = Utils.generateId('item');\n          duplicate.shopifyLineKey = null;\n          duplicate.addedAt      = Utils.now();\n          section.items.splice(idx + 1, 0, duplicate);\n          return Helpers.recalculateLineNumbers(basket);\n        }\n      }\n      // Fall back to unsectioned items\n      const idx = basket.items.findIndex(i => i.id === itemId);\n      if (idx > -1) {\n        const duplicate        = JSON.parse(JSON.stringify(basket.items[idx]));\n        duplicate.id           = Utils.generateId('item');\n        duplicate.shopifyLineKey = null;\n        duplicate.addedAt      = Utils.now();\n        basket.items.splice(idx + 1, 0, duplicate);\n        return Helpers.recalculateLineNumbers(basket);\n      }\n      return basket;\n    },\n\n    /**\n     * Exposed so btk-cart-advanced.js can check login state\n     * and show appropriate messaging in the My Kitchens panel.\n     */\n    isLoggedIn: Utils.isLoggedIn\n  };\n\n  // \u2500\u2500\u2500 ADAPTER SELECTION \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n  // Logged-in customers use the server (App Proxy); guests use localStorage.\n  // Evaluated per-call so it reflects window.BTKCustomer (set inline before this).\n  function pickAdapter() {\n    return (SERVER_ENABLED && Utils.isLoggedIn()) ? ProxyAdapter : SessionAdapter;\n  }\n\n  // \u2500\u2500\u2500 PROJECT POSTCODE \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  // One postcode prices delivery (the basket's Estimated Delivery card) and a\n  // made-to-measure worktop's templating zone (the planner). The page only\n  // knows the postcode of whoever is signed in to the shop, so it is read from\n  // the planner API instead: what was typed into the basket, else the\n  // customer's default Shopify address, else their Odoo contact - and for\n  // staff, the CUSTOMER's, never their own (Arden, 2026-10-12). A guest's is\n  // kept in this browser. Every change is announced as `btk:postcode:changed`\n  // ({ detail: { postcode } }) so the planner and the basket follow it.\n  const POSTCODE_KEY = 'btk-postcode-v1';\n  const UK_POSTCODE = /^([A-Z]{1,2}[0-9][A-Z0-9]?)([0-9][A-Z]{2})$/;\n  let _postcodeP = null;\n  function normalisePostcode(v) {\n    const m = String(v == null ? '' : v).toUpperCase().replace(/[^A-Z0-9]/g, '').match(UK_POSTCODE);\n    return m ? `${m[1]} ${m[2]}` : null;\n  }\n  // A guest's postcode lives in their browser; staff never use the browser's\n  // (it would be the salesperson's own, or the last customer's).\n  function localPostcode() {\n    if (OVERRIDE_CID) return null;\n    try { return normalisePostcode(localStorage.getItem(POSTCODE_KEY)); } catch (e) { return null; }\n  }\n  // { postcode, source } or null. Asked once per page.\n  function getPostcode() {\n    if (!_postcodeP) {\n      const fromServer = (SERVER_ENABLED && Utils.isLoggedIn())\n        ? ProxyAdapter._http('/my/postcode').then(r => {\n            const pc = r && normalisePostcode(r.postcode);\n            return pc ? { postcode: pc, source: r.source || 'saved' } : null;\n          }).catch(() => null)\n        : Promise.resolve(null);\n      _postcodeP = fromServer.then(found => {\n        if (found) return found;\n        const local = localPostcode();\n        return local ? { postcode: local, source: 'browser' } : null;\n      });\n    }\n    return _postcodeP;\n  }\n  // Save a typed postcode. opts.opportunityId: the basket's Odoo opportunity\n  // (a hint the API checks). Resolves { postcode, odoo } or rejects with\n  // Error('postcode') when it is not a UK postcode.\n  function setPostcode(value, opts = {}) {\n    const postcode = normalisePostcode(value);\n    if (!postcode) return Promise.reject(new Error('postcode'));\n    if (!OVERRIDE_CID) { try { localStorage.setItem(POSTCODE_KEY, postcode); } catch (e) {} }\n    const save = (SERVER_ENABLED && Utils.isLoggedIn())\n      ? ProxyAdapter._http('/my/postcode', { method: 'POST', body: JSON.stringify({ postcode, opportunityId: opts.opportunityId || null }) })\n          .then(r => (r && r.odoo) || 'skipped')\n          .catch(() => 'failed')\n      : Promise.resolve('guest');\n    return save.then(odoo => {\n      _postcodeP = Promise.resolve({ postcode, source: 'saved' });\n      try { window.dispatchEvent(new CustomEvent('btk:postcode:changed', { detail: { postcode } })); } catch (e) {}\n      return { postcode, odoo };\n    });\n  }\n\n  // \u2500\u2500\u2500 CART PUSHES RUN ONE AT A TIME \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  // A push is clear-the-cart then add-every-line. Two pushes overlapping\n  // interleave as clear, clear, add, add - and the online cart ends up holding\n  // every line TWICE (identical lines merge, so each shows qty 2), which is what\n  // the customer would be charged at checkout. There are several callers (the\n  // planner's rebuild, its deferred push when the basket opens, the basket's own\n  // autosave, empty basket, range change...), and the planner fires its deferred\n  // push without waiting. So queue them here, at the single public entry point:\n  // each push starts only when the previous one has finished, and reads the\n  // basket as it is THEN, so the last push always reflects the final basket.\n  // A failed push never blocks the queue.\n  // ACROSS TABS TOO. The queue above is per page, but the cart is shared by\n  // every tab. On 2026-10-14 a plan went to checkout with the standard\n  // basket's three units added on top (+\u00A3820.20): one tab filled the cart with\n  // the plan while another handed it back to the standard basket, and the two\n  // interleaved as clear, clear, add, add. A Web Lock is held by one tab at a\n  // time, so a push now waits for any push running in another tab. Browsers\n  // without it keep the per-page queue.\n  const CART_WRITE_LOCK = 'btk-cart-write';\n  let _cartSyncChain = Promise.resolve();\n  function queuedCartSync(basketId) {\n    if (READ_ONLY_VIEW) return Promise.resolve(null);   // see READ_ONLY_VIEW\n    const push = () => {\n      // A push waits behind any push already in the air, and that wait is\n      // invisible in a total measured by the caller - so a slow checkout got\n      // blamed on Shopify when it may have been queueing. ?btktime=1 only.\n      try { if (window.BTKKPMark) window.BTKKPMark('cart queue clear, push begins'); } catch (e) {}\n      return pickAdapter().syncToShopifyCart(basketId);\n    };\n    const run = () => {\n      try {\n        if (navigator.locks && typeof navigator.locks.request === 'function') {\n          return navigator.locks.request(CART_WRITE_LOCK, push);\n        }\n      } catch (e) { /* fall through to the unlocked push */ }\n      return push();\n    };\n    const p = _cartSyncChain.then(run, run);\n    _cartSyncChain = p.catch(() => {});\n    return p;\n  }\n\n  // \u2500\u2500\u2500 DOES THE ONLINE CART HOLD THIS BASKET? \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  // By its contents, not just its id. The cart used to count as \"already right\"\n  // whenever it was tagged with the basket's id and not empty - so a product\n  // added straight into the cart from a product page, while a plan was in it,\n  // went through checkout as part of that plan's order. Every line syncToShopifyCart\n  // writes carries its basket item's id (_btk_item_id); add-on lines are totals\n  // derived from those, so they follow if the item lines match. Any doubt = false:\n  // a needless refill costs seconds, a wrong cart costs the order.\n  function expectedCartLines(basket) {\n    const want = new Map();\n    const add = (it) => {\n      if (!it || it.isChecklistItem) return;   // the same lines syncToShopifyCart writes\n      const id = String(it.id);\n      want.set(id, (want.get(id) || 0) + (Number(it.quantity) || 0));\n    };\n    (basket.sections || []).forEach(s => (s.items || []).forEach(add));\n    (basket.items || []).forEach(add);\n    return want;\n  }\n  function cartHoldsBasket(basketId) {\n    if (!basketId) return Promise.resolve(false);\n    return Promise.all([\n      fetch('/cart.js', { headers: { Accept: 'application/json' } }).then(r => (r.ok ? r.json() : null)),\n      pickAdapter().getBaskets(),\n    ]).then(([cart, baskets]) => {\n      if (!cart || String((cart.attributes || {})._btk_basket_id || '') !== String(basketId)) return false;\n      const basket = (baskets || []).find(b => b && b.id === basketId);\n      if (!basket) return false;\n      const want = expectedCartLines(basket);\n      const got = new Map();\n      for (const line of (cart.items || [])) {\n        const p = line.properties || {};\n        if (p._btk_addon) continue;\n        if (!p._btk_item_id) return false;     // a line no basket put there\n        const id = String(p._btk_item_id);\n        got.set(id, (got.get(id) || 0) + (Number(line.quantity) || 0));\n      }\n      if (got.size !== want.size) return false;\n      for (const [id, qty] of want) if (got.get(id) !== qty) return false;\n      return true;\n    }).catch(() => false);\n  }\n\n  // \u2500\u2500\u2500 HAND THE CART BACK TO THE STANDARD BASKET \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  // The standard basket OWNS the online cart (and so the header count); a plan\n  // only BORROWS it for checkout. Borrowed, it used to stay borrowed until /cart\n  // happened to be opened: the header counted the kitchen, and a product page\n  // add landed in the kitchen's order. So on any ordinary page load, a cart\n  // still holding a plan that is NOT mid-payment is refilled from the standard\n  // basket. Done on the next load rather than as the planner is left, because a\n  // closing page cannot be relied on to finish a request.\n  // Never: while the plan is HELD (checkout in progress), on the pages that\n  // manage the cart themselves (planner, /cart, pre-checkout, checkout), for a\n  // guest (a guest cannot check out a plan) or for staff acting as a customer.\n  function handBackBorrowedCart() {\n    try {\n      if (READ_ONLY_VIEW || OVERRIDE_CID || !Utils.isLoggedIn()) return Promise.resolve(false);\n      const path = String(window.location.pathname || '');\n      if (/kitchen-planner|pre-checkout|^\\/cart(\\/|$)|^\\/checkout/.test(path)) return Promise.resolve(false);\n    } catch (e) { return Promise.resolve(false); }\n    const adapter = pickAdapter();\n    // Load the project first, then decide INSIDE the cart lock (see\n    // queuedCartSync) on a cart read there: a cart read before waiting could\n    // be one the planner has since filled for checkout. The push itself is\n    // called directly, because queuedCartSync would wait on the lock this\n    // already holds.\n    const decideAndPush = () => fetch('/cart.js', { headers: { Accept: 'application/json' } })\n      .then(r => (r.ok ? r.json() : null))\n      .then(cart => {\n        const a = (cart && cart.attributes) || {};\n        if (a._btk_basket_source !== 'planner' || !a._btk_basket_id) return false;\n        const lock = adapter.getBasketLock && adapter.getBasketLock(a._btk_basket_id);\n        if (lock && lock.status === 'held') return false;   // being paid for right now\n        // Filled with the plan in the last few minutes: it is very likely on\n        // its way to checkout, and the hold is only taken AFTER the fill, so\n        // a hand-back in that gap swapped the order for the standard basket.\n        // The next page load after that hands it back.\n        const syncedAt = Date.parse(a._btk_synced_at || '');\n        if (syncedAt && (Date.now() - syncedAt) < HAND_BACK_GRACE_MS) return false;\n        return adapter.getActiveManualBasket()\n          .then(b => (b && b.id ? adapter.syncToShopifyCart(b.id).then(() => true) : false));\n      });\n    return adapter.getProject()\n      .then(() => {\n        const run = () => decideAndPush();\n        const queued = _cartSyncChain.then(() => {\n          try {\n            if (navigator.locks && typeof navigator.locks.request === 'function') {\n              return navigator.locks.request(CART_WRITE_LOCK, run);\n            }\n          } catch (e) { /* fall through */ }\n          return run();\n        });\n        _cartSyncChain = queued.catch(() => {});\n        return queued;\n      })\n      .catch(() => false);\n  }\n  const HAND_BACK_GRACE_MS = 10 * 60 * 1000;\n\n  // Arriving from Odoo in a basket session: load the project now, which runs\n  // the arrival (ProxyAdapter._arrive). The styles page may never ask for the\n  // project itself. Only once staff mode is confirmed - an expired or refused\n  // link makes nothing (_ensureLoaded checks, and _save refuses anyway). The\n  // parameters stay in the address until then, so a sign-in round trip\n  // (StaffBlock) still arrives.\n  // And on EVERY page of a staff session, not just the arrival: the bar is\n  // only drawn once the project load confirms staff mode, and the home page,\n  // collections, appliance and info pages never ask for the project - so the\n  // purple bar vanished a few clicks in while staff mode carried on unseen\n  // (Arden, 2026-10-12). A session held from an earlier page was confirmed\n  // there, so its bar is drawn at once (no jump as the page loads); this\n  // page's own check then repaints it, red if the link has since been refused.\n  if (OVERRIDE_CID && !READ_ONLY_VIEW) {\n    if (!STAFF_ARRIVAL && !_staff.link && OVERRIDE_CIDT) StaffBanner.show('active', STAFF_CUSTOMER_NAME);\n    STAFF_READY.then(() => ProxyAdapter.getProject()).catch(() => {});\n  }\n\n  // \u2500\u2500\u2500 PUBLIC API \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n  return {\n    getProject:             ()           => pickAdapter().getProject(),\n    // Read the project from the server again (a live-session viewer, whose\n    // salesperson is changing it). Kept as it is while a save of this page's\n    // own is under way, so nothing in flight is lost.\n    refreshProject:         ()           => {\n      const a = pickAdapter();\n      if (a && '_loaded' in a && a._loaded && !a._saving && !a._loadFailed) a._loaded = null;\n      return a.getProject();\n    },\n    getBaskets:             ()           => pickAdapter().getBaskets(),\n    getActiveBasket:        ()           => pickAdapter().getActiveBasket(),\n    getActiveManualBasket:  ()           => pickAdapter().getActiveManualBasket(),\n    saveBasket:             (basketData)      => pickAdapter().saveBasket(basketData),\n    duplicateBasket:        (basketId)        => pickAdapter().duplicateBasket(basketId),\n    createBasket:           (name, source)    => pickAdapter().createBasket(name, source),\n    deleteBasket:           (basketId)        => pickAdapter().deleteBasket(basketId),\n    setActiveBasket:        (basketId)        => pickAdapter().setActiveBasket(basketId),\n    getActivePlannerBasketId: ()              => pickAdapter().getActivePlannerBasketId(),\n    getActivePlannerBasket: ()                => pickAdapter().getActivePlannerBasket(),\n    setActivePlannerBasketId: (basketId)      => pickAdapter().setActivePlannerBasketId(basketId),\n    syncToShopifyCart:      (basketId)        => queuedCartSync(basketId),\n    // A bought plan is frozen (see basket_locks on the API). Local storage has\n    // no orders behind it, so its adapter simply never locks anything.\n    getOrderedBasket:       (basketId)        => (pickAdapter().getOrderedBasket\n                                                   ? pickAdapter().getOrderedBasket(basketId) : Promise.resolve(null)),\n    kitchenManual:          (basketId)        => (pickAdapter().kitchenManual\n                                                   ? pickAdapter().kitchenManual(basketId) : Promise.reject(new Error('unavailable'))),\n    approveKitchenManual:   (basketId, b64)   => pickAdapter().approveKitchenManual(basketId, b64),\n    rewriteKitchenManual:   (basketId)        => pickAdapter().rewriteKitchenManual(basketId),\n    getPlanVersion:         (versionId)       => (pickAdapter().getPlanVersion\n                                                   ? pickAdapter().getPlanVersion(versionId) : Promise.resolve(null)),\n    getBasketLock:          (basketId)        => (pickAdapter().getBasketLock\n                                                   ? pickAdapter().getBasketLock(basketId) : null),\n    isBasketLocked:         (basketId)        => (pickAdapter().isBasketLocked\n                                                   ? pickAdapter().isBasketLocked(basketId) : false),\n    refreshLocks:           ()                => (pickAdapter()._loadLocks\n                                                   ? pickAdapter()._loadLocks() : Promise.resolve()),\n    holdBasket:             (basketId)        => (pickAdapter().holdBasket\n                                                   ? pickAdapter().holdBasket(basketId) : Promise.resolve(null)),\n    releaseBasket:          (basketId)        => (pickAdapter().releaseBasket\n                                                   ? pickAdapter().releaseBasket(basketId) : Promise.resolve(null)),\n    // CHECKOUT'S HOLD, for the basket actually going to checkout: the one the\n    // Shopify cart carries (_btk_basket_id), which both checkout routes pass\n    // in. Never allowed to stop someone paying, so it gives up after `ms`: a\n    // stalled connection used to leave the Checkout button doing nothing.\n    // Resolves true only when the hold is known to have been taken.\n    holdForCheckout:        (basketId, ms = 3000) => {\n      if (!basketId) return Promise.resolve(false);\n      const a = pickAdapter();\n      if (!a.holdBasket) return Promise.resolve(false);   // a guest has no server plan to hold\n      return Promise.race([\n        a.holdBasket(basketId).then(r => {\n          // Remember that THIS browser started a checkout of this plan, and\n          // when. The planner treats \"held + empty cart\" as just bought only\n          // with this record (heldHereRecently): an empty cart on ANOTHER\n          // device, or hours later, says nothing about an order.\n          if (r) { try { localStorage.setItem('btk_hold_at:' + basketId, String(Date.now())); } catch (e) {} }\n          return !!r;\n        }, () => false),\n        new Promise(res => setTimeout(() => res(false), ms)),\n      ]);\n    },\n    // When this browser took a checkout hold on the basket (ms), or 0.\n    heldHereAt:             (basketId)        => {\n      try { return Number(localStorage.getItem('btk_hold_at:' + basketId)) || 0; } catch (e) { return 0; }\n    },\n    // Settles once every cart push already queued has finished, so checkout\n    // never leaves while one is half-written (cleared but not yet refilled).\n    whenCartSynced:         ()                => _cartSyncChain,\n    // See ProxyAdapter.saveBasketSoon. On the guest store every save is\n    // already immediate, so these are the ordinary ones.\n    saveBasketSoon:         (basket)          => (pickAdapter().saveBasketSoon\n                                                   ? pickAdapter().saveBasketSoon(basket)\n                                                   : pickAdapter().saveBasket(basket)\n                                                       .then(b => ({ basket: b, saved: Promise.resolve(b) }))),\n    whenSaved:              ()                => (pickAdapter().whenSaved\n                                                   ? pickAdapter().whenSaved() : Promise.resolve()),\n    cartHolds:              (basketId)        => cartHoldsBasket(basketId),\n    // ORDER EXTRAS (Arden, 2026-10-20): a new basket linked to one of the\n    // customer's orders - `link` is planner-api's /my/order-extras answer\n    // ({ order: { id, number, delivery_date }, token }). Delivery for it is\n    // worked out without the minimum order, and can go free with the main\n    // order when the goods can be in for it. Becomes the active basket.\n    startOrderExtras:       (link)            => startOrderExtras(link),\n    // Staff \"Link to an order\" on an existing basket (btk-cart-advanced.js):\n    // the customer's placed orders that can take extras, newest first ...\n    extrasOrders:           ()                => Promise.all([\n                                                   ProxyAdapter._http('/my/site-orders').catch(() => null),\n                                                   ProxyAdapter._http('/my/orders').catch(() => null),\n                                                 ]).then(([site, legacy]) => {\n                                                   const seen = new Set();\n                                                   return [...((site && site.orders) || []), ...((legacy && legacy.orders) || [])]\n                                                     .filter(o => o && o.can_order_extras && !seen.has(o.id) && seen.add(o.id))\n                                                     .sort((a, b) => String(b.date || '').localeCompare(String(a.date || '')));\n                                                 }),\n    // ... the link for one of them (planner-api /my/order-extras) ...\n    orderExtrasLink:        (orderId)         => ProxyAdapter._http('/my/order-extras?id=' + encodeURIComponent(orderId)),\n    // ... and linking a basket to it (or `link` null to unlink). Saves the\n    // basket and puts the link on the cart.\n    setBasketExtras:        (basket, link)    => {\n                                                   if (link && link.token && link.order) {\n                                                     basket.extras = { token: link.token, orderId: link.order.id,\n                                                       number: link.order.number || '', deliveryDate: link.order.delivery_date || null };\n                                                   } else delete basket.extras;\n                                                   return pickAdapter().saveBasket(basket)\n                                                     .then(b => queuedCartSync(b.id).catch(() => {}).then(() => b));\n                                                 },\n    orderExtrasOf:          (basket)          => (basket && basket.extras && basket.extras.token) ? basket.extras : null,\n    handBackBorrowedCart:   ()                => handBackBorrowedCart(),\n\n    recalculateLineNumbers: Helpers.recalculateLineNumbers,\n    moveItemToSection:      Helpers.moveItemToSection,\n    moveItemToPosition:     Helpers.moveItemToPosition,\n    updateItemAttribute:    Helpers.updateItemAttribute,\n    findItem:               Helpers.findItem,\n    extractItem:            Helpers.extractItem,\n    duplicateItem:          (basket, itemId) => Helpers.duplicateItem(basket, itemId),\n    createItem:             Helpers.createItem,\n    createSection:          Helpers.createSection,\n    isLoggedIn:             Helpers.isLoggedIn,\n\n    // Staff mode (a salesperson acting as a customer via an Odoo-minted link).\n    isStaffMode:            ()                => !!OVERRIDE_CID,\n    // A standard-basket staff session (Odoo's New Website Basket / Edit in\n    // Basket), as opposed to a planner one.\n    isStaffBasketSession:   ()                => STAFF_BASKET_SESSION,\n    // Save, leave staff mode and return to the Odoo record staff came from\n    // (the green bar's Back to Odoo; also the ordered viewer's Close).\n    backToOdoo:             ()                => backToOdoo(),\n    // Put session controls in the staff bar (see StaffBanner.setActions).\n    setStaffBannerActions:  (list)            => StaffBanner.setActions(list),\n    staffCustomerId:        ()                => OVERRIDE_CID || null,\n    // The working staff token, once a one-time Odoo link has been exchanged.\n    // Anything else calling the API as the customer (the live session) must\n    // wait for staffReady() and send this, never a token read from the URL.\n    staffReady:             ()                => STAFF_READY,\n    staffToken:             ()                => OVERRIDE_CIDT || '',\n    // Lifestyle photos (planner-api src/lifestyle-photo.js). Making, keeping\n    // and removing are staff only; the kept photo is read by the plan's own\n    // customer too, for the Plan report. Images come back as Blobs.\n    lifestylePhotoStart:    (body)            => (OVERRIDE_CID\n                                                   ? ProxyAdapter._http('/my/lifestyle-photo', { method: 'POST', body: JSON.stringify(body || {}) })\n                                                   : Promise.reject(new Error('staff only'))),\n    lifestylePhotoJob:      (jobId)           => ProxyAdapter._http(`/my/lifestyle-photo/job/${encodeURIComponent(jobId)}`),\n    lifestylePhotoKeep:     (id, planName)    => ProxyAdapter._http('/my/lifestyle-photo/keep', { method: 'POST', body: JSON.stringify({ id, planName }) }),\n    lifestylePhotoRemove:   (basketId)        => ProxyAdapter._http('/my/lifestyle-photo/remove', { method: 'POST', body: JSON.stringify({ basketId }) }),\n    lifestylePhotoImage:    (id)              => lifestyleBlob(`/my/lifestyle-photo/image/${encodeURIComponent(id)}`),\n    lifestylePhotoKept:     (basketId)        => lifestyleBlob(`/my/lifestyle-photo/kept?basketId=${encodeURIComponent(basketId)}`),\n    // Staff \"Make free\" on one basket line, and its Undo (planner-api\n    // src/free-item.js). Staff only: the server refuses anyone else.\n    makeLineFree:          (body)            => (OVERRIDE_CID\n                                                   ? ProxyAdapter._http('/my/free-item', { method: 'POST', body: JSON.stringify(body || {}) })\n                                                   : Promise.reject(new Error('staff only'))),\n    // Free item codes onto the cart for these basket lines (see\n    // applyFreeItemCodes); `cart` is the cart as it stands (/cart.js). Used\n    // before checkout, when the cart may not have been refilled since.\n    // Staff free delivery (planner-api makeDeliveryFree); Undo is\n    // removeLineFree with its code.\n    makeDeliveryFree:       (body)            => (OVERRIDE_CID\n                                                   ? ProxyAdapter._http('/my/free-delivery', { method: 'POST', body: JSON.stringify(body || {}) })\n                                                   : Promise.reject(new Error('staff only'))),\n    applyFreeItemCodes:     (cart, items, basket) => applyFreeItemCodes(cart, items, basket),\n    freeItemLive:           (item)            => freeItemLive(item),\n    freeDeliveryLive:       (basket)          => freeDeliveryLive(basket),\n    removeLineFree:         (code)            => (OVERRIDE_CID\n                                                   ? ProxyAdapter._http('/my/free-item/remove', { method: 'POST', body: JSON.stringify({ code }) })\n                                                   : Promise.reject(new Error('staff only'))),\n    // Urgent / Warning on a basket (planner-api src/basket-urgency.js): put on\n    // the Odoo order when the customer pays for it. Staff mode only - the\n    // customer never sees it. Resolves 'urgent' | 'warning' | null.\n    getBasketUrgency:       (basketId)        => (OVERRIDE_CID && basketId\n                                                   ? ProxyAdapter._http('/my/basket-urgency?basketId=' + encodeURIComponent(basketId))\n                                                       .then(r => (r && r.urgency) || null)\n                                                   : Promise.resolve(null)),\n    setBasketUrgency:       (basketId, urgency) => (OVERRIDE_CID && basketId\n                                                   ? ProxyAdapter._http('/my/basket-urgency', { method: 'POST',\n                                                       body: JSON.stringify({ basketId, urgency: urgency || 'none' }) })\n                                                       .then(r => (r && r.urgency) || null)\n                                                   : Promise.reject(new Error('staff only'))),\n    // The customer's staff discount offer (planner-api src/staff-offer.js):\n    // { offer: {group, percent, title, endsAt} | null, maxPercent, groups }, or\n    // null for a guest / when the server can't be reached. Staff give and\n    // remove it; the server refuses anyone else.\n    getStaffOffer:          ()                => ((OVERRIDE_CID || (SERVER_ENABLED && Utils.isLoggedIn()))\n                                                   ? ProxyAdapter._http('/my/staff-offer').catch(() => null)\n                                                   : Promise.resolve(null)),\n    giveStaffOffer:         (body)            => (OVERRIDE_CID\n                                                   ? ProxyAdapter._http('/my/staff-offer', { method: 'POST', body: JSON.stringify(body || {}) })\n                                                   : Promise.reject(new Error('staff only'))),\n    // The offer of one basket (each basket has its own: bug sheet row 64).\n    removeStaffOffer:       (basketId)        => (OVERRIDE_CID\n                                                   ? ProxyAdapter._http('/my/staff-offer/remove', { method: 'POST', body: JSON.stringify(basketId ? { basketId } : {}) })\n                                                   : Promise.reject(new Error('staff only'))),\n    // Goodwill (GWG) discounts on basket lines (planner-api src/gwg.js):\n    // { canGive, title, lines: [{ key, basketId, itemId, variantId, qty,\n    // percent, amount, endsAt }] }, or null for a guest / when the server\n    // can't be reached. Only managers acting for the customer give or remove.\n    getGwg:                 ()                => ((OVERRIDE_CID || (SERVER_ENABLED && Utils.isLoggedIn()))\n                                                   ? ProxyAdapter._http('/my/gwg').catch(() => null)\n                                                   : Promise.resolve(null)),\n    giveGwg:                (body)            => (OVERRIDE_CID\n                                                   ? ProxyAdapter._http('/my/gwg', { method: 'POST', body: JSON.stringify(body || {}) })\n                                                   : Promise.reject(new Error('staff only'))),\n    removeGwg:              (basketId)        => (OVERRIDE_CID\n                                                   ? ProxyAdapter._http('/my/gwg/remove', { method: 'POST', body: JSON.stringify({ basketId }) })\n                                                   : Promise.reject(new Error('staff only'))),\n    // Who the work is FOR, for anything that prints an owner (the plan report\n    // prints this on every page). Falls back to the customer id when the\n    // project carries no email - never to the signed-in staff member's name.\n    actingAsLabel:          ()                => (OVERRIDE_CID\n                                                   ? (_actingAsLabel || OVERRIDE_CID)\n                                                   : null),\n    // The customer's NAME, for anything printed on a document. Deliberately\n    // returns null rather than falling back to an email or a customer id:\n    // a plan report carries a person's name or nothing at all.\n    actingAsName:           ()                => (OVERRIDE_CID\n                                                   ? (STAFF_CUSTOMER_NAME || null)\n                                                   : null),\n    // Leave staff mode here, without going back to Odoo: reload WITHOUT the\n    // staff params so the page comes back as the real visitor.\n    // The plan is saved first, as Back to Odoo does - nothing is saved after\n    // the hold is handed back (StaffHold.release).\n    exitStaffMode:          ()                => runStaffExitHooks().then(() => emptyBrowserCart()).then(() => StaffHold.release()).then(() => {\n                                                   leaveStaffMode();\n                                                   try { window.location.href = window.location.pathname; }\n                                                   catch (e) { window.location.reload(); }\n                                                 }),\n    // Work to finish before \"Back to Odoo\" leaves: fn resolves false if it\n    // could not (the salesperson is then asked before leaving).\n    onStaffExit:            (fn)              => { if (typeof fn === 'function') staffExitHooks.push(fn); },\n    // A salesperson is working on this customer's plans (see STAFF HOLD):\n    // nothing can be saved or checked out here until they finish.\n    // A basket's number (PLN100001 / BSK100001), once the server has given it.\n    basketRef:              (basketId)        => {\n      const p = ProxyAdapter._project;\n      const b = p && Array.isArray(p.baskets) ? p.baskets.find(x => x && String(x.id) === String(basketId)) : null;\n      return (b && b.ref) || null;\n    },\n    staffHeld:              ()                => !!StaffHold.held,\n    showStaffHold:          ()                => StaffHold.show(),\n    // The project's postcode (see PROJECT POSTCODE above).\n    getPostcode:            ()                => getPostcode(),\n    setPostcode:            (value, opts)     => setPostcode(value, opts),\n    normalisePostcode:      (value)           => normalisePostcode(value),\n    // A support ticket for the signed-in customer (or, for staff, the customer\n    // they act for): the planner API files it in Odoo. Used by the basket's\n    // Request a Price on a bespoke worktop. Rejects when not signed in.\n    createTicket:           (subject, message) => ((SERVER_ENABLED && Utils.isLoggedIn())\n                                                   ? ProxyAdapter._http('/my/tickets', { method: 'POST', body: JSON.stringify({ subject, message }) })\n                                                   : Promise.reject(new Error('not signed in'))),\n  };\n\n})();\n\n// Expose on window so ES-module consumers can reach it. Classic\n// scripts (cart-advanced, cart-drawer) use the bare `BTKBasketAPI`\n// lexical binding; the kitchen planner is a type=\"module\" script, and\n// module scope cannot see a classic script's top-level `const`, so it\n// needs the global property explicitly.\nif (typeof window !== 'undefined') window.BTKBasketAPI = BTKBasketAPI;\n\n// ORDER EXTRAS. The account page's Order Extras either calls startOrderExtras\n// itself or, where this script has not loaded yet, leaves the link under\n// ORDER_EXTRAS_KEY for the next page to pick up.\nconst ORDER_EXTRAS_KEY = 'btk_order_extras_pending';\nfunction startOrderExtras(link) {\n  const o = (link && link.order) || {};\n  if (!link || !link.token || !o.id) return Promise.reject(new Error('order extras link'));\n  return BTKBasketAPI.createBasket('Extras for ' + (o.number || 'your order'), 'manual')\n    .then(b => {\n      b.extras = { token: link.token, orderId: o.id, number: o.number || '', deliveryDate: o.delivery_date || null };\n      return BTKBasketAPI.saveBasket(b);\n    })\n    .then(b => BTKBasketAPI.setActiveBasket(b.id).then(() => b))\n    // The cart follows the active basket, so the header count is right.\n    .then(b => BTKBasketAPI.syncToShopifyCart(b.id).catch(() => {}).then(() => b));\n}\ntry {\n  const _btkExtrasPickUp = () => {\n    let link = null;\n    try { link = JSON.parse(localStorage.getItem(ORDER_EXTRAS_KEY) || 'null'); localStorage.removeItem(ORDER_EXTRAS_KEY); } catch (e) {}\n    // A link left more than an hour ago is a stale click, not this visit.\n    if (!link || !(Date.now() - (link.at || 0) < 60 * 60 * 1000)) return;\n    startOrderExtras(link).catch(e => console.warn('[BTK] order extras basket not started', e));\n  };\n  if (typeof document !== 'undefined' && document.readyState !== 'loading') setTimeout(_btkExtrasPickUp, 0);\n  else if (typeof document !== 'undefined') document.addEventListener('DOMContentLoaded', _btkExtrasPickUp);\n} catch (e) { /* never let this stop the basket API loading */ }\n\n// See handBackBorrowedCart. As soon as the page is parsed (this script is\n// deferred, so that is now), NOT on 'load': waiting for every image left the\n// header without its count for about five seconds (Arden, 2026-09-29).\ntry {\n  const _btkHandBack = () => { try { BTKBasketAPI.handBackBorrowedCart(); } catch (e) {} };\n  if (typeof document !== 'undefined' && document.readyState !== 'loading') setTimeout(_btkHandBack, 0);\n  else if (typeof document !== 'undefined' && typeof document.addEventListener === 'function') {\n    document.addEventListener('DOMContentLoaded', _btkHandBack);\n  }\n} catch (e) { /* never let this stop the basket API loading */ }"],
  "mappings": "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",
  "names": ["i", "idx"]
}
